def main(data_choice='random_vertical_boundary'):

    # For Moons dataset, noise = 0.05 is added.

    data_train, data_test, true_labels_train, true_labels_test = generate_data(
        data_choice, num_points=1024, split=True)
    true_labels_test_flip = []
    for label in true_labels_test:
        if label == 0: true_labels_test_flip.append(1)
        elif label == 1: true_labels_test_flip.append(0)
        else: raise ValueError('This label does not exist')

    plot_params_train = {
        'colors': ['blue', 'magenta'],
        'alpha': 0.5,
        'size': 80
    }
    scatter(data_train, true_labels_train, **plot_params_train, show=False)
    plot_params_test = {
        'colors': ['blue', 'magenta'],
        'alpha': 1,
        'size': 40,
        'linewidth': 1.5
    }
    scatter(data_test,
            true_labels_test,
            true_labels_test_flip,
            **plot_params_test,
            show=False)
    plt.show()
def main(encoding='denseangle_param'):

    qc_name = '1q-qvm'
    qc = get_qc(qc_name)
    num_shots = 1024
    device_qubits = qc.qubits()
    classifier_qubits = device_qubits
    if encoding.lower() == 'wavefunction_param':
        params = np.array([0.45811744, 0.2575122, 0.52902198])
    else:
        params = np.random.rand(3)
    n_layers = 1
    if encoding.lower() == 'denseangle_param':
        encoding_choice = 'denseangle_param'
        init_encoding_params = [np.pi, 2*np.pi]
    elif encoding.lower() == 'wavefunction_param':
        encoding_choice = 'wavefunction_param'
        init_encoding_params = [0]
    elif encoding.lower() == 'superdenseangle_param':
        encoding_choice = 'superdenseangle_param'
        init_encoding_params = [np.pi, 2*np.pi]
    else: raise NotImplementedError
    '''
        # Generate Grid of datapoints to determine and visualise ideal decision boundary
    '''
    data_choice = 'full_vertical_boundary'
    num_grid_points = 2000
    data_grid, grid_true_labels = generate_data(data_choice, num_grid_points)
    data_grid, grid_true_labels = remove_zeros(data_grid, grid_true_labels)

    predicted_labels_grid = ClassificationCircuit(classifier_qubits, data_grid).make_predictions(params, n_layers, encoding_choice, init_encoding_params, \
                                                                num_shots, qc)
    plot_params = {'colors': ['blue', 'orange'], 'alpha': 1, 'size': 70}
    scatter(data_grid, predicted_labels_grid, **plot_params)
    plt.show()
def main(train=False,
         retrain=False,
         data_choice='moons',
         noise_choice='amp_damp_before_measurement',
         noise_values=0.3):

    ### Firstly, generate the dataset:

    data_train, data_test, true_labels_train, true_labels_test = generate_data(
        data_choice, num_points=500, split=True)

    data_train, true_labels_train = remove_zeros(data_train, true_labels_train)
    data_test, true_labels_test = remove_zeros(data_test, true_labels_test)

    encodings = [
        'denseangle_param', 'wavefunction_param', 'superdenseangle_param'
    ]

    minimal_costs, ideal_costs, noisy_costs, noisy_costs_uncorrected = [
        np.ones(len(encodings)) for _ in range(4)
    ]

    qc_name = '1q-qvm'
    n_layers = 1
    qc = get_qc(qc_name)
    num_shots = 1024
    classifier_qubits = qc.qubits()
    init_params = np.random.rand(3)
    ideal_params = []
    ideal_encoding_params = []
    init_encoding_params = []

    for ii, encoding_choice in enumerate(encodings):

        print('\n**********************************')
        print('\nThe encoding is:', encoding_choice)
        print('\n**********************************')

        if encoding_choice.lower() == 'wavefunction_param':
            init_encoding_params.append(
                [0]
            )  # Generalized Wavefunction Encoding initialised to Wavefunction encoding
        else:
            init_encoding_params.append([np.pi, 2 * np.pi])

        if train:

            optimiser = 'Powell'
            params, result_unitary_param = train_classifier(
                qc, num_shots, init_params, encoding_choice,
                init_encoding_params[ii], optimiser, data_train,
                true_labels_train)

            print('The optimised parameters are:', result_unitary_param.x)
            print(
                'These give a cost of:',
                ClassificationCircuit(classifier_qubits,
                                      data_train).build_classifier(
                                          result_unitary_param.x,
                                          encoding_choice,
                                          init_encoding_params[ii], num_shots,
                                          qc, true_labels_train))
            ideal_params.append(result_unitary_param.x)
        else:

            if data_choice.lower() == 'moons':
                if encoding_choice.lower() == 'denseangle_param':
                    ideal_params.append([2.19342064, 1.32972029, -0.18308298])
                elif encoding_choice.lower() == 'superdenseangle_param':
                    ideal_params.append([-0.27365492, 0.83278854, 3.00092961])
                elif encoding_choice.lower() == 'wavefunction':
                    ideal_params.append([0.81647273, 0.41996708, 2.20603541])
                elif encoding_choice.lower() == 'wavefunction_param':
                    ideal_params.append([0.81647273, 0.41996708, 2.20603541])

            elif data_choice.lower() == 'random_vertical_boundary':
                if encoding_choice.lower() == 'denseangle_param':
                    ideal_params.append([1.67814786, 1.56516469, 1.77820848])
                elif encoding_choice.lower() == 'superdenseangle_param':
                    ideal_params.append([1.60642225, 0.23401504, 5.69422628])
                elif encoding_choice.lower() == 'wavefunction':
                    ideal_params.append([0.96291484, 0.18133714, 0.35436732])
                elif encoding_choice.lower() == 'wavefunction_param':
                    ideal_params.append([0.96291484, 0.18133714, 0.35436732])

            elif data_choice.lower() == 'random_diagonal_boundary':
                if encoding_choice.lower() == 'denseangle_param':
                    ideal_params.append([0.8579214, 1.22952647, 4.99408074])
                elif encoding_choice.lower() == 'superdenseangle_param':
                    ideal_params.append([2.0101407, 1.05916291, 1.14570489])
                elif encoding_choice.lower() == 'wavefunction':
                    ideal_params.append([0.69409285, 0.0862859, 0.42872711])
                elif encoding_choice.lower() == 'wavefunction_param':
                    ideal_params.append([0.69409285, 0.0862859, 0.42872711])

            else:
                print('THIS DATASET HAS NOT BEEN TRAINED FOR')
                if encoding_choice.lower() == 'denseangle_param':
                    ideal_params.append(init_params)
                elif encoding_choice.lower() == 'superdenseangle_param':
                    ideal_params.append(init_params)
                elif encoding_choice.lower() == 'wavefunction':
                    ideal_params.append(init_params)
                elif encoding_choice.lower() == 'wavefunction_param':
                    ideal_params.append(init_params)

        ideal_costs[ii] = ClassificationCircuit(
            classifier_qubits, data_test,
            qc).build_classifier(ideal_params[ii], n_layers, encoding_choice,
                                 init_encoding_params[ii], num_shots,
                                 true_labels_test)

        print('In the ideal case, the cost is:', ideal_costs[ii])

        predicted_labels_ideal = ClassificationCircuit(
            classifier_qubits, data_test,
            qc).make_predictions(ideal_params[ii], n_layers, encoding_choice,
                                 init_encoding_params[ii], num_shots)

        noisy_costs_uncorrected[ii] = ClassificationCircuit(classifier_qubits, data_test, qc, noise_choice, noise_values).build_classifier(ideal_params[ii], n_layers,\
                                                                                                                            encoding_choice, init_encoding_params[ii],\
                                                                                                                            num_shots, true_labels_test)
        print('\nWithout encoding training, the noisy cost is:',
              noisy_costs_uncorrected[ii])


        noisy_predictions, number_classified_same = generate_noisy_classification(ideal_params[ii], n_layers, noise_choice, noise_values, \
                                                                                    encoding_choice, init_encoding_params[ii],\
                                                                                    qc, classifier_qubits, num_shots, data_test, predicted_labels_ideal)

        print('The proportion classified differently after noise is:',
              1 - number_classified_same)

        if retrain:
            if encoding_choice.lower() == 'wavefunction_param':
                optimiser = 'L-BFGS-B'
            else:
                optimiser = 'Powell'

            encoding_params, result_encoding_param = train_classifier_encoding(
                qc, noise_choice, noise_values, num_shots, ideal_params[ii],
                encoding_choice, init_encoding_params[ii], optimiser,
                data_train, true_labels_train)
            print('The optimised encoding parameters with noise are:',
                  result_encoding_param.x)
            ideal_encoding_params.append(result_encoding_param.x)

        else:
            if data_choice.lower() == 'moons' and noise_choice.lower(
            ) == 'amp_damp_before_measurement' and isclose(
                    noise_values, 0.3, abs_tol=1e-8):
                if encoding_choice.lower() == 'denseangle_param':
                    ideal_encoding_params.append([2.23855329, 7.57781576])
                elif encoding_choice.lower() == 'superdenseangle_param':
                    ideal_encoding_params.append([3.31296568, 6.34142188])
                elif encoding_choice.lower() == 'wavefunction_param':
                    ideal_encoding_params.append([0.02884417])

            elif data_choice.lower() == 'random_vertical_boundary':
                if encoding_choice.lower() == 'denseangle_param':
                    ideal_encoding_params.append([2.26042559, 8.99138928])
                elif encoding_choice.lower() == 'superdenseangle_param':
                    ideal_encoding_params.append([3.1786475, 8.36712745])
                elif encoding_choice.lower() == 'wavefunction_param':
                    ideal_encoding_params.append([0.01503151])

            elif data_choice.lower() == 'random_diagonal_boundary':
                if encoding_choice.lower() == 'denseangle_param':
                    ideal_encoding_params.append([2.11708966, 5.69354627])
                elif encoding_choice.lower() == 'superdenseangle_param':
                    ideal_encoding_params.append([0.08689283, 6.21166815])
                elif encoding_choice.lower() == 'wavefunction_param':
                    ideal_encoding_params.append([0.0])

            else:
                print('THIS DATASET HAS NOT BEEN TRAINED FOR')
                if encoding_choice.lower() == 'denseangle_param':
                    ideal_encoding_params.append(init_encoding_params[ii])
                elif encoding_choice.lower() == 'superdenseangle_param':
                    ideal_encoding_params.append(init_encoding_params[ii])
                elif encoding_choice.lower() == 'wavefunction_param':
                    ideal_encoding_params.append(init_encoding_params[ii])

        noisy_costs[ii] = ClassificationCircuit(classifier_qubits, data_test, qc, noise_choice, noise_values).build_classifier(ideal_params[ii], n_layers,\
                                                                                                                encoding_choice, ideal_encoding_params[ii],\
                                                                                                                num_shots, true_labels_test)

        print('\nWith encoding training, the noisy cost is:', noisy_costs[ii])

        noisy_predictions, number_classified_same = generate_noisy_classification(ideal_params[ii], n_layers, noise_choice, noise_values, \
                                                                                encoding_choice, ideal_encoding_params[ii], qc, classifier_qubits, num_shots,\
                                                                                data_test, predicted_labels_ideal)
        print(
            'The proportion classified differently after noise with learned encoding is:',
            1 - number_classified_same)

    for ii, encoding_choice in enumerate(encodings):
        print('\nThe encoding is:', encodings[ii])
        print('The ideal params are:', ideal_params[ii])
        print('The ideal encoding params are', ideal_encoding_params[ii])

        print('The ideal cost for encoding', ideal_costs[ii])
        print('The noisy cost with untrained encoding',
              noisy_costs_uncorrected[ii])
        print('The noisy cost with trained encoding', noisy_costs[ii])

    return encodings, ideal_params, init_encoding_params, ideal_encoding_params, ideal_costs, noisy_costs_uncorrected, noisy_costs
Example #4
0
def main(train=False, encoding_choice='denseangle_param', retrain=False, data_choice='moons', noise=False):
    
    ### Firstly, generate for dataset:
    '''
    # We use the transpose of the (scaled to unit square) Moons dataset in order to see a non-linear decision boundary
    '''
    data_train, data_test, true_labels_train, true_labels_test = generate_data(data_choice, num_points=500, split=True)

    # data_train, true_labels_train   = remove_zeros(data_train, true_labels_train)
    # data_test, true_labels_test     = remove_zeros(data_test, true_labels_test)

    ### Next, generate correct classification parameters for dataset (perfect classification):
    '''
    # Define parameters of model. Start with DenseAngle encoding with fixed parameters.
    '''

    qc_name = '1q-qvm'
    qc = get_qc(qc_name)
    num_shots = 1024
    qubits = qc.qubits()
    init_params = np.random.rand(3)
    if encoding_choice.lower() == 'wavefunction_param':
        init_encoding_params = [ 0 ] # Generalized Wavefunction Encoding initialised to Wavefunction encoding 
    else: 
        init_encoding_params = [np.pi, 2*np.pi]

    if train:
         
        optimiser = 'Powell' 
        params, result_unitary_param = train_classifier(qc, num_shots, init_params, encoding_choice, init_encoding_params, optimiser, data_train, true_labels_train)

        print('The optimised parameters are:', result_unitary_param.x)
        print('These give a cost of:', ClassificationCircuit(qubits, data_train).build_classifier(result_unitary_param.x, encoding_choice, init_encoding_params, num_shots, qc, true_labels_train))
        ideal_params =  result_unitary_param.x
    else:
        if data_choice.lower() == 'moons':
            ### Define Ideal parameters for trained model. Simple model can acheieve classification of about 90 %
            '''
            # 90% Classification parameters for dense angle encoding
            '''
            if encoding_choice.lower() == 'denseangle_param': ideal_params= [ 2.19342064 , 1.32972029, -0.18308298]

            ### Define Ideal parameters for trained model. Simple model can acheieve classification of about 75 %
            '''
            # 73% Classification parameters for superdense angle encoding
            '''
            if encoding_choice.lower() == 'superdenseangle_param': ideal_params =  [-0.27365492,  0.83278854,  3.00092961]

            ### Define Ideal parameters for trained model. Simple model can acheieve classification of about  %
            '''
            # 85% Classification parameters for wavefunction encoding
            '''
            if encoding_choice.lower() == 'wavefunction': ideal_params = [0.81647273, 0.41996708, 2.20603541]
            if encoding_choice.lower() == 'wavefunction_param': ideal_params = [0.81647273, 0.41996708, 2.20603541]
        
        elif data_choice.lower() == 'random_vertical_boundary':
            if encoding_choice.lower() == 'superdenseangle_param': ideal_params = [1.606422245361118, 0.23401504261014927, 5.694226283697996]
        
        elif data_choice.lower() == 'random_diagonal_boundary':

            ### Define Ideal parameters for trained model. Simple model can acheieve classification of about 90 %
            '''
            # 90% Classification parameters for dense angle encoding
            '''
            if encoding_choice.lower() == 'denseangle_param':   ideal_params = [0.8579214,  1.22952647, 4.99408074]
            
            ### Define Ideal parameters for trained model. Simple model can acheieve classification of about  %
            '''
            # % Classification parameters for superdense angle encoding
            '''
            if encoding_choice.lower() == 'superdenseangle_param': ideal_params = [2.0101407,  1.05916291, 1.14570489]
            
            ### Define Ideal parameters for trained model. Simple model can acheieve classification of about  97%
            '''
            # 97% Classification parameters for wavefunction encoding
            '''
            if encoding_choice.lower() == 'wavefunction':           ideal_params = [0.69409285 0.0862859  0.42872711]
            if encoding_choice.lower() == 'wavefunction_param':     ideal_params =  [0.69409285 0.0862859  0.42872711]

        
    print('These give a cost of:', ClassificationCircuit(qubits, data_test).build_classifier(ideal_params, encoding_choice, init_encoding_params, num_shots, qc, true_labels_test))
    predicted_labels_ideal = ClassificationCircuit(qubits, data_test).make_predictions(ideal_params, encoding_choice, init_encoding_params, num_shots, qc)

    # nisqai.visual.scatter(data_test, true_labels_test, predicted_labels)


    ### Overlay decision bounday
    '''
    # Generate Grid of datapoints to determine and visualise ideal decision boundary
    '''
    num_points = 400
    data_grid, grid_true_labels = generate_data('full_vertical_boundary', num_points)
    data_grid, grid_true_labels = remove_zeros(data_grid, grid_true_labels)

    predicted_labels = ClassificationCircuit(qubits, data_test).make_predictions(ideal_params, encoding_choice, init_encoding_params, num_shots, qc)
    plot_params = {'colors': ['blue', 'orange'], 'alpha': 1}
    scatter(data_test, true_labels_test, predicted_labels, **plot_params)

    predicted_labels_grid = ClassificationCircuit(qubits, data_grid).make_predictions(ideal_params, encoding_choice, init_encoding_params, num_shots, qc)

    plot_params = {'colors': ['red', 'green'], 'alpha': 0.2}

    scatter(data_grid, predicted_labels_grid, **plot_params)
    plt.show()


    ## Define noise parameters
    '''
    # Define noise parameters to add to model to determine how classification is affected.
    '''
    if noise: 
        noise_choice = 'amp_damp_before_measurement'
        noise_values = 0.3

    ### Add noise to circuit and classify
    '''
    # Add noise to circuit, and determine number of points classified differently (not mis-classified since we can't achieve perfect classification)
    '''
    if noise:
        noisy_predictions, number_classified_same = generate_noisy_classification(ideal_params, noise_choice, noise_values, encoding_choice, init_encoding_params, qc, num_shots, data_test, predicted_labels_ideal)
        print('The proportion classified differently after noise is:', 1- number_classified_same)

    ## Overlay decision boundary
    '''
    # Generate Grid of datapoints to determine and visualise ideal decision boundary WITH noise added
    '''

    if noise:
        print(noise_choice)
        predicted_labels = ClassificationCircuit(qubits, data_test, noise_choice, noise_values).make_predictions(ideal_params, encoding_choice, init_encoding_params, num_shots, qc)
        plot_params = {'colors': ['blue', 'orange'], 'alpha': 1}
        scatter(data_test, true_labels_test, predicted_labels, **plot_params)

        predicted_labels_grid = ClassificationCircuit(qubits, data_grid, noise_choice, noise_values).make_predictions(ideal_params, encoding_choice, init_encoding_params, num_shots, qc)
        plot_params = {'colors': ['red', 'green'], 'alpha': 0.2}
        scatter(data_grid, predicted_labels_grid, **plot_params)

        plt.show()

    ### Retrain circuit with noise
    '''
    # Given the noise in the circuit, train the parameters of encoding unitary to account for noise. Parameterised unitary parameters are fixed as the ideal ones learned.
    '''
    if retrain:

        if encoding_choice.lower() == 'wavefunction_param': optimiser = 'L-BFGS-B' 
        else:                                               optimiser = 'Powell' 

        if noise:
            encoding_params, result_encoding_param = train_classifier_encoding(qc, noise_choice, noise_values, num_shots, ideal_params, encoding_choice, init_encoding_params, optimiser, data_train, true_labels_train)
            print('The optimised encoding parameters with noise are:', result_encoding_param.x)
            ideal_encoding_params = result_encoding_param.x
        else:
            encoding_params, result_encoding_param = train_classifier_encoding(qc, None, None, num_shots, ideal_params, encoding_choice, init_encoding_params, optimiser, data_train, true_labels_train)
            print('The optimised encoding parameters without noise are:', result_encoding_param.x)
            ideal_encoding_params = result_encoding_param.x
    else:
        ### Define Ideal ENCODING parameters for trained model. Simple model can acheieve classification of about 90 with noise, 93% without noise %
        '''
        # 90% Classification parameters for dense angle encoding
        '''
        if data_choice.lower() == 'moons' and encoding_choice.lower() == 'denseangle_param' and noise:  
            ideal_encoding_params = [2.23855329, 7.57781576]
            '''
            # 93% Classification parameters for dense angle encoding without noise
            '''
        elif data_choice.lower() == 'moons' and encoding_choice.lower() == 'denseangle_param':
            ideal_encoding_params =  [3.05615259, 7.61215138]  # No noise

        ### Define Ideal ENCODING parameters for trained model. Simple model can acheieve classification of about 90 %
        '''
        # NO NOISE  - 74-77% Classification parameters with training for superdense angle encoding  
        # NOISE     - Classification parameters for superdense angle encoding (0.3 amp damp = 20% different classification - 69% accuracy with noise before encoding training)
        #             With learned encoding - 
        '''
        if data_choice.lower() == 'moons' and encoding_choice.lower() == 'superdenseangle_param' and noise: 
            ideal_encoding_params =  [3.31296568, 6.34142188]

        elif data_choice.lower() == 'moons' and encoding_choice.lower() == 'superdenseangle_param':
            ideal_encoding_params = [2.86603822, 6.14328274] # No noise
        
        ### Define Ideal ENCODING parameters for trained model. Simple model can acheieve classification of about 90 %
        '''
        # NO NOISE  - 82-84% Classification parameters with training for generalised wavefunction encoding  
        # NOISE     - Classification parameters for superdense angle encoding (0.3 amp damp = 20% different classification - 78% accuracy with noise before encoding training)
        #             With learned encoding - 
        '''
        print(data_choice.lower(), encoding_choice.lower())
        if data_choice.lower() == 'moons' and encoding_choice.lower() == 'wavefunction_param' and noise: 
            ideal_encoding_params =  [0.02884417]
        elif data_choice.lower() == 'moons' and encoding_choice.lower() == 'wavefunction_param':
            ideal_encoding_params = [0.01582773] # No noise
            

    if noise:
        print('These give a cost with the noisy circuit of:',\
            ClassificationCircuit(qubits, data_test, noise_choice, noise_values).build_classifier(ideal_params, encoding_choice, ideal_encoding_params , num_shots, qc, true_labels_test) )
    else:       
        print('These give a cost with the ideal circuit of:',\
            ClassificationCircuit(qubits, data_test).build_classifier(ideal_params, encoding_choice, ideal_encoding_params , num_shots, qc, true_labels_test) )

    ### Add noise to circuit and classify
    '''
    # Using learned encoding parameters, check again proportion misclassified
    '''
    if noise:
        noisy_predictions, number_classified_same = generate_noisy_classification(ideal_params, noise_choice, noise_values, encoding_choice, ideal_encoding_params, qc, num_shots, data_test, predicted_labels)
        print('The proportion classified differently after noise with learned encoding is:', 1 - number_classified_same)

    ## Overlay decision boundary
    '''
    # Generate Grid of datapoints to determine and visualise ideal decision boundary WITH/WITHOUT noise added
    '''
    if noise:
        predicted_labels = ClassificationCircuit(qubits, data_test, noise_choice, noise_values).make_predictions(ideal_params, encoding_choice, ideal_encoding_params, num_shots, qc)

        plot_params = {'colors': ['blue', 'orange'], 'alpha': 1}
        scatter(data_test,  true_labels_test, predicted_labels, **plot_params)

        predicted_labels_grid = ClassificationCircuit(qubits, data_grid, noise_choice, noise_values).make_predictions(ideal_params, encoding_choice, ideal_encoding_params, num_shots, qc)
        
        plot_params = {'colors': ['red', 'green'], 'alpha': 0.2}
        scatter(data_grid, predicted_labels_grid, **plot_params)
        plt.show()
    else:
        predicted_labels = ClassificationCircuit(qubits, data_test).make_predictions(ideal_params, encoding_choice, ideal_encoding_params, num_shots, qc)
        
        plot_params = {'colors': ['blue', 'orange'], 'alpha': 1}
        scatter(data_test, true_labels_test,  predicted_labels, **plot_params)
        
        predicted_labels_grid = ClassificationCircuit(qubits, data_grid).make_predictions(ideal_params, encoding_choice, ideal_encoding_params, num_shots, qc)
        
        plot_params = {'colors': ['red', 'green'], 'alpha': 0.2}
        scatter(data_grid, predicted_labels_grid, **plot_params)
        plt.show()
def main(data_choice='iris',
         amp_damp_noise=False,
         bit_flip_noise=False,
         dephasing_noise=False,
         global_depolarizing_noise=False,
         legend=False):

    # Generate data
    data_train, data_test, true_labels_train, true_labels_test = generate_data(
        data_choice, num_points=100, split=True)

    def unitary_evolved_noiseless(rho, params):
        '''
        rho should be an encoded state. This function returns the evolved state with the unitary params.
        '''

        unitary_layer_1_1 = rz(2 * params[2]) @ ry(2 * params[1]) @ rz(
            2 * params[0])
        unitary_layer_1_2 = rz(2 * params[5]) @ ry(2 * params[4]) @ rz(
            2 * params[3])
        U_1 = np.kron(unitary_layer_1_1, unitary_layer_1_2)

        # First layer
        rho = U_1 @ rho @ U_1.conj().T

        unitary_layer_2_1 = rx(2 * params[6] + np.pi) @ hmat
        unitary_layer_2_2 = rz(2 * params[7])
        U_2 = np.kron(unitary_layer_2_1, unitary_layer_2_2) @ cnotmat

        # Second Layer
        rho = U_2 @ rho @ U_2.conj().T

        unitary_layer_3_1 = hmat
        unitary_layer_3_2 = rz(-2 * params[8])
        U_3 = np.kron(unitary_layer_3_1, unitary_layer_3_2) @ cnotmat

        # Third Layer
        rho = U_3 @ rho @ U_3.conj().T

        unitary_layer_4_1 = rz(2 * params[11]) @ ry(2 * params[10]) @ rz(
            2 * params[9])
        unitary_layer_4_2 = imat
        U_4 = np.kron(unitary_layer_4_1, unitary_layer_4_2) @ cnotmat

        # Fourth Layer
        rho = U_4 @ rho @ U_4.conj().T

        return rho

    def unitary_evolved_noisy(rho,
                              params,
                              noise_choice='Pauli',
                              noise_values=None,
                              noise=True):
        '''
        rho should be an encoded state. This function returns the evolved state with the unitary params.
        The noise channel is applied after each step in the unitary
        '''
        if noise:
            rho = channels(rho,
                           noise_choice=noise_choice,
                           noise_values=noise_values)  # Apply noise

        unitary_layer_1_n = np.kron(rz(2 * params[0]), rz(2 * params[3]))
        rho = unitary_layer_1_n @ rho @ unitary_layer_1_n.conj().T
        if noise:
            rho = channels(rho,
                           noise_choice=noise_choice,
                           noise_values=noise_values)  # Apply noise

        unitary_layer_2_n = np.kron(ry(2 * params[1]), ry(2 * params[4]))
        rho = unitary_layer_2_n @ rho @ unitary_layer_2_n.conj().T
        if noise:
            rho = channels(rho,
                           noise_choice=noise_choice,
                           noise_values=noise_values)  # Apply noise

        unitary_layer_3_n = np.kron(rz(2 * params[2]), rz(2 * params[5]))
        rho = unitary_layer_3_n @ rho @ unitary_layer_3_n.conj().T
        if noise:
            rho = channels(rho,
                           noise_choice=noise_choice,
                           noise_values=noise_values)  # Apply noise

        unitary_layer_4_n = cnotmat
        rho = unitary_layer_4_n @ rho @ unitary_layer_4_n.conj().T

        if noise:
            rho = channels(rho,
                           noise_choice=noise_choice,
                           noise_values=noise_values)  # Apply noise

        unitary_layer_2_1 = rx(2 * params[6] + np.pi) @ hmat
        unitary_layer_2_2 = rz(2 * params[7])
        unitary_layer_5_n = np.kron(unitary_layer_2_1, unitary_layer_2_2)
        rho = unitary_layer_5_n @ rho @ unitary_layer_5_n.conj().T
        if noise:
            rho = channels(rho,
                           noise_choice=noise_choice,
                           noise_values=noise_values)  # Apply noise

        unitary_layer_6_n = cnotmat
        rho = unitary_layer_6_n @ rho @ unitary_layer_6_n.conj().T

        if noise:
            rho = channels(rho,
                           noise_choice=noise_choice,
                           noise_values=noise_values)  # Apply noise

        unitary_layer_3_1 = hmat
        unitary_layer_3_2 = rz(-2 * params[8])
        unitary_layer_7_n = np.kron(unitary_layer_3_1, unitary_layer_3_2)

        # Third Layer

        rho = unitary_layer_7_n @ rho @ unitary_layer_7_n.conj().T
        if noise:
            rho = channels(rho,
                           noise_choice=noise_choice,
                           noise_values=noise_values)  # Apply noise

        unitary_layer_8_n = cnotmat
        rho = unitary_layer_8_n @ rho @ unitary_layer_8_n.conj().T

        if noise:
            rho = channels(rho,
                           noise_choice=noise_choice,
                           noise_values=noise_values)  # Apply noise

        unitary_layer_9_n = np.kron(rz(2 * params[9]), imat)
        rho = unitary_layer_9_n @ rho @ unitary_layer_9_n.conj().T
        if noise:
            rho = channels(rho,
                           noise_choice=noise_choice,
                           noise_values=noise_values)  # Apply noise

        unitary_layer_10_n = np.kron(ry(2 * params[10]), imat)
        rho = unitary_layer_10_n @ rho @ unitary_layer_10_n.conj().T
        if noise:
            rho = channels(rho,
                           noise_choice=noise_choice,
                           noise_values=noise_values)  # Apply noise

        unitary_layer_11_n = np.kron(rz(2 * params[11]), imat)
        rho = unitary_layer_11_n @ rho @ unitary_layer_11_n.conj().T
        if noise:
            rho = channels(rho,
                           noise_choice=noise_choice,
                           noise_values=noise_values)  # Apply noise

        return rho

    # Encodings
    def state(f, g, x_1, y_1, x_2, y_2, encoding_params):
        # Encode first two features, x_1, y_1 in first qubit
        rho_1 = np.array([[
            abs(f(x_1, y_1, encoding_params))**2,
            f(x_1, y_1, encoding_params) *
            np.conj(g(x_1, y_1, encoding_params))
        ],
                          [
                              np.conj(f(x_1, y_1, encoding_params)) *
                              g(x_1, y_1, encoding_params),
                              abs(g(x_1, y_1, encoding_params))**2
                          ]])

        # Encode second two features, x_2, y_2 in second qubit
        rho_2 = np.array([[
            abs(f(x_2, y_2, encoding_params))**2,
            f(x_2, y_2, encoding_params) *
            np.conj(g(x_2, y_2, encoding_params))
        ],
                          [
                              np.conj(f(x_2, y_2, encoding_params)) *
                              g(x_2, y_2, encoding_params),
                              abs(g(x_2, y_2, encoding_params))**2
                          ]])

        rho = np.kron(rho_1, rho_2)  # Tensor product state

        return rho / np.trace(rho)

    def f_dae(x, y, encoding_params):
        return np.cos(encoding_params[0] * x)

    def g_dae(x, y, encoding_params):
        return np.exp(encoding_params[1] * 1j * y) * np.sin(
            encoding_params[0] * x)

    def f_wf(x, y, encoding_params=None):
        return (np.sqrt(1 + encoding_params[0] * y**2) * x) / np.sqrt(x**2 +
                                                                      y**2)

    def g_wf(x, y, encoding_params=None):
        return (np.sqrt(1 - encoding_params[0] * x**2) * y) / np.sqrt(x**2 +
                                                                      y**2)

    def f_sdae(x, y, encoding_params):
        return np.cos(encoding_params[0] * x + encoding_params[1] * y)

    def g_sdae(x, y, encoding_params):
        return np.sin(encoding_params[0] * x + encoding_params[1] * y)

    def pauli(rho, pi=0.4, px=0.1, py=0.1, pz=0.4):
        """Applies a single qubit pauli channel to the state rho."""

        assert np.isclose(sum((pi, px, py, pz)), 1.0)

        return (pi * rho + px * xmat @ rho @ xmat + py * ymat @ rho @ ymat +
                pz * zmat @ rho @ zmat)

    def one_q_pauli_kraus(pi, px, py, pz):
        kraus = [np.sqrt(pi) * imat, np.sqrt(px) * xmat, \
                    np.sqrt(py) * ymat, np.sqrt(pz) * zmat]
        return kraus

    def two_q_pauli_kraus(pi, px, py, pz):
        [pi_1, pi_2] = pi
        [px_1, px_2] = px
        [py_1, py_2] = py
        [pz_1, pz_2] = pz

        assert np.isclose(sum((pi_1, px_1, py_1, pz_1)), 1.0)
        assert np.isclose(sum((pi_2, px_2, py_2, pz_2)), 1.0)

        kraus_1 = one_q_pauli_kraus(pi_1, px_1, py_1, pz_1)
        kraus_2 = one_q_pauli_kraus(pi_2, px_2, py_2, pz_2)
        pauli_kraus = [np.kron(k1, k2) for k1 in kraus_1 for k2 in kraus_2]

        return pauli_kraus

    def two_q_pauli(rho, pi, px, py, pz):
        '''
            Applies random single qubit Pauli gates to 
            each qubit in the state with potentially different strengths
        '''
        pauli_kraus = two_q_pauli_kraus(pi, px, py, pz)
        # constuct list of all Kraus operators being applied to the state and sum to
        # generate noisy state
        rho = np.array([k @ rho @ k.conj().T for k in pauli_kraus]).sum(axis=0)
        return rho

    def two_q_global_depolarizing(rho, p=0.1):
        return (1 - p) * rho + (p) * np.kron(imat, imat) / 2**2

    def amplitude_damping(rho, p):
        E_00 = np.array([[1, 0], [0, np.sqrt(1 - p[0])]])
        E_01 = np.array([[0, np.sqrt(p[0])], [0, 0]])

        E_10 = np.array([[1, 0], [0, np.sqrt(1 - p[1])]])
        E_11 = np.array([[0, np.sqrt(p[1])], [0, 0]])

        rho =  np.kron(E_00, E_10) @ rho @ np.kron(E_00, E_10).conj().T + np.kron(E_00, E_11) @ rho @ np.kron(E_00, E_11).conj().T \
            + np.kron(E_01, E_10) @ rho @ np.kron(E_01, E_10).conj().T + np.kron(E_01, E_11) @ rho @ np.kron(E_01, E_11).conj().T

        return rho

    # Noise channels
    def channels(rho, noise_choice='Pauli', noise_values=None):
        if noise_choice.lower() == 'pauli':
            pi, px, py, pz = noise_values
            rho = two_q_pauli(rho, pi, px, py, pz)

        elif noise_choice.lower() == 'global_depolarizing':
            p = noise_values
            rho = two_q_global_depolarizing(rho, p)

        elif noise_choice.lower() == 'dephasing':
            pz = noise_values
            rho = two_q_pauli(rho, [1 - pz[0], 1 - pz[1]], pz, [0, 0], [0, 0])

        elif noise_choice.lower() == 'bit_flip':
            px = noise_values
            rho = two_q_pauli(rho, [1 - px[0], 1 - px[1]], px, [0, 0], [0, 0])

        elif noise_choice.lower() == 'amp_damp':
            p = noise_values
            rho = amplitude_damping(rho, p)
        else:
            raise NotImplementedError

        return rho

    # Predictions
    def make_prediction(rho):
        # Compute prediction for data sample in state rho
        proj_0 = np.kron(np.kron(pi0, imat), imat)

        elt = np.trace(proj_0 @ rho)
        if elt.real >= 0.499999999999999999:
            return 0
        return 1

    def compute_cost(ideal_labels, noisy_labels):
        """
        Computes the cost between the ideal case labels, and the labels
        produced in the presence of noise. Simple indicator cost.
        """
        assert len(ideal_labels) == len(noisy_labels)
        sum_count = 0
        for ii in range(len(ideal_labels)):
            if not ideal_labels[ii] == noisy_labels[ii]:
                sum_count += 1
        average_sum = sum_count / len(ideal_labels)
        return average_sum

    def set_params(data_choice='iris', encoding_choice='denseangle_param'):

        if data_choice.lower() == 'iris':
            if encoding_choice.lower() == 'denseangle_param':                ideal_params = [ 2.02589489, 1.24358318, -0.6929718,\
                            0.85764484, 2.7572075, 1.12317156, \
                            4.01974889, 0.30921738, 0.88106973,\
                            1.461694, 0.367226, 5.01508911 ]

            elif encoding_choice.lower() == 'superdenseangle_param':                ideal_params = [1.1617383, -0.05837820, -0.7216498,\
                            1.3195103, 0.52933357, 1.2854939,\
                            1.2097700, 0.26920745, 0.4239539, \
                            1.2999367, 0.37921617, 0.790320211]

            elif encoding_choice.lower() == 'wavefunction':                ideal_params = [ 2.37732073, 1.01449711, 1.12025344,\
                            -0.087440021, 0.46937127, 2.14387135, \
                            0.4696964, 1.444409282, 0.14412614,\
                            1.4825742, 1.0817654, 6.30943537 ]

            elif encoding_choice.lower() == 'wavefunction_param':                ideal_params = [ 2.37732073, 1.01449711, 1.12025344,\
                            -0.087440021, 0.46937127, 2.14387135, \
                            0.4696964, 1.444409282, 0.14412614,\
                            1.4825742, 1.0817654, 6.30943537]

            else:
                raise NotImplementedError
        else:
            raise ValueError('This dataset has not been trained for.')

        return ideal_params

    def average_fidelity(encoding_choice='denseangle_param',
                         encoding_params=None,
                         noise_choice='Pauli',
                         noise_values=None):
        rho_noiseless, rho_noisy = [], []

        params = set_params(data_choice='iris',
                            encoding_choice=encoding_choice)

        fidelity = np.zeros(
            len(data_test))  # one element of fidelity per data point
        pred_labels_noisy = np.zeros(len(data_test), dtype=int)
        pred_labels_noiseless = np.zeros(len(data_test), dtype=int)
        print('------------------------------------')
        print('Encoding is:', encoding_choice)
        print('------------------------------------')

        for ii, point in enumerate(data_test):
            if true_labels_test[ii] == 0:
                label_qubit = np.array([[1, 0], [0, 0]])
            else:
                label_qubit = np.array([[0, 0], [0, 1]])

            if encoding_choice.lower() == 'denseangle_param':
                rho_encoded = state(f_dae, g_dae, point[0], point[1], point[2],
                                    point[3],
                                    encoding_params)  # encode data point

            elif encoding_choice.lower() == 'wavefunction_param':
                rho_encoded = state(f_wf, g_wf, point[0], point[1], point[2],
                                    point[3],
                                    encoding_params)  # encode data point

            elif encoding_choice.lower() == 'superdenseangle_param':
                rho_encoded = state(f_sdae, g_sdae, point[0], point[1],
                                    point[2], point[3],
                                    encoding_params)  # encode data point

            else:
                raise NotImplementedError
            rhos_noiseless_indiv = unitary_evolved_noiseless(
                rho_encoded, params)
            rho_noisy_indiv = unitary_evolved_noisy(rho_encoded,
                                                    params,
                                                    noise_choice=noise_choice,
                                                    noise_values=noise_values)

            rho_noiseless.append(np.kron(rhos_noiseless_indiv, label_qubit))
            rho_noisy.append(np.kron(rho_noisy_indiv, label_qubit))

            pred_labels_noiseless[ii] = make_prediction(rho_noiseless[ii])
            pred_labels_noisy[ii] = make_prediction(rho_noisy[ii])

            fidelity[ii] = (np.trace(sqrtm(rho_noisy[ii] @ rho_noiseless[ii]))
                            )**2  # compute fidelity per sample
        cost_ideal = compute_cost(true_labels_test, pred_labels_noiseless)
        cost_noisy = compute_cost(true_labels_test, pred_labels_noisy)

        cost_difference = cost_noisy - cost_ideal

        rho_noiseless_mixed = np.array(
            (1 / len(rho_noiseless)) * np.array(rho_noiseless).sum(axis=0))
        rho_noisy_mixed = (1 /
                           len(rho_noisy)) * np.array(rho_noisy).sum(axis=0)

        avg_fidelity_mixed = ((np.trace(
            sqrtm(
                sqrtm(rho_noisy_mixed) @ rho_noiseless_mixed
                @ sqrtm(rho_noisy_mixed))))**2
                              ).real  # compute fidelity for mixed state

        avg_fidelity = (1 / len(data_test)) * np.sum(
            fidelity, axis=0)  # compute average fidelity over dataset
        avg_bound = (2 / len(data_test)) * np.sum(
            np.square(np.ones_like(fidelity) - fidelity),
            axis=0)  # compute average bound over dataset
        print(avg_fidelity)
        if avg_fidelity_mixed > 1:
            avg_fidelity_mixed = 1  # reset fidelity less than one if numerical precision makes it > 1

        mixed_bound = 2 * np.sqrt(
            1 - avg_fidelity_mixed)  # Bound on cost function error

        return cost_difference, avg_bound, mixed_bound, avg_fidelity, avg_fidelity_mixed

    encoding_params_dae = [np.pi / 2, 2 * np.pi]
    encoding_params_wf = [0]
    encoding_params_sdae = [np.pi, 2 * np.pi]

    encoding_params = [
        encoding_params_dae, encoding_params_wf, encoding_params_sdae
    ]

    num_points = 50

    if amp_damp_noise:
        fidelity_bound_plot(average_fidelity,
                            encoding_params=encoding_params,
                            noise_choice='amp_damp',
                            show=True,
                            num_points=num_points,
                            num_qbs=2,
                            legend=legend)
        fidelity_compare_plot(average_fidelity,
                              encoding_params=encoding_params,
                              noise_choice='amp_damp',
                              show=True,
                              num_points=num_points,
                              num_qbs=2,
                              legend=legend)

    if bit_flip_noise:
        fidelity_bound_plot(average_fidelity,
                            encoding_params=encoding_params,
                            noise_choice='bit_flip',
                            show=True,
                            num_points=num_points,
                            num_qbs=2,
                            legend=legend)
        fidelity_compare_plot(average_fidelity,
                              encoding_params=encoding_params,
                              noise_choice='bit_flip',
                              show=True,
                              num_points=num_points,
                              num_qbs=2,
                              legend=legend)

    if dephasing_noise:
        fidelity_bound_plot(average_fidelity,
                            encoding_params=encoding_params,
                            noise_choice='dephasing',
                            show=True,
                            num_points=num_points,
                            num_qbs=2,
                            legend=legend)
        fidelity_compare_plot(average_fidelity,
                              encoding_params=encoding_params,
                              noise_choice='dephasing',
                              show=True,
                              num_points=num_points,
                              num_qbs=2,
                              legend=legend)

    if global_depolarizing_noise:
        '''
            Global depolarizing noise on both qubits together.
        '''
        fidelity_bound_plot(average_fidelity,
                            encoding_params=encoding_params,
                            noise_choice='global_depolarizing',
                            show=True,
                            num_points=num_points,
                            num_qbs=2,
                            legend=legend)
        fidelity_compare_plot(average_fidelity,
                              encoding_params=encoding_params,
                              noise_choice='global_depolarizing',
                              show=True,
                              num_points=num_points,
                              num_qbs=2,
                              legend=legend)
def main(train=False,
         encoding='denseangle_param',
         ideal=False,
         noise=False,
         analytic=False,
         compare=False):
    """
    # Find optimal parameters for linear decision boundary and add noise
    """

    ### Firstly, generate for dataset:
    '''
    # We use the transpose of the (scaled to unit square) Moons dataset in order to see a non-linear decision boundary
    '''
    data_vertical_train, data_vertical_test, true_labels_train, true_labels_test = generate_data(
        'random_vertical_boundary', num_points=500, split=True)

    ### Next, generate correct classification parameters for dataset (perfect classification):
    '''
    # Define parameters of model. Start with DenseAngle encoding with fixed parameters.
    '''

    qc_name = '1q-qvm'
    qc = get_qc(qc_name)
    num_shots = 1024
    device_qubits = qc.qubits()
    classifier_qubits = device_qubits
    n_layers = 1
    init_params = np.random.rand(3)
    if encoding.lower() == 'denseangle_param':
        encoding_choice = 'denseangle_param'
        # init_encoding_params = [np.pi, 2*np.pi]
        init_encoding_params = [np.pi, 2 * np.pi]

    elif encoding.lower() == 'wavefunction' or encoding.lower(
    ) == 'wavefunction_param':
        encoding_choice = 'wavefunction_param'
        init_encoding_params = [0]

    optimiser = 'Powell'

    if train:
        ### Train model, and check classification result of ideal parameters found
        '''
        # Train model using scipy.optimize
        '''
        params, result_unitary_param = train_classifier(
            qc, num_shots, init_params, encoding_choice, init_encoding_params,
            optimiser, data_vertical_train, true_labels_train)
        print('The optimised parameters are:', result_unitary_param.x)
        print('These give a cost of:', ClassificationCircuit(classifier_qubits, data_vertical_train).build_classifier(result_unitary_param.x, n_layers, \
                                                                            encoding_choice, init_encoding_params, num_shots, qc, true_labels_train))
        ideal_params_vertical = result_unitary_param.x
    else:
        ### Define Ideal parameters for trained model learned from previous. Simple model can acheieve classification of about 90 %

        if encoding_choice.lower() == 'denseangle_param':
            '''
            # 100% Classification parameters (modulo points on the boundary)
            '''
            # ideal_params_vertical = [3.8208,1.525,0.0808]
            ideal_params_vertical = [1.67814786, 1.56516469, 1.77820848]
        elif encoding_choice.lower() == 'wavefunction_param':
            '''
            # 78% Classification parameters (modulo points on the boundary)
            '''
            ideal_params_vertical = [2.2921198, 0.61375299, -5.15252796]

    plt.rcParams.update({
        "font.size": 20,
        "font.serif": "Computer Modern Roman"
    })

    ### Overlay decision bounday
    '''
    # Generate Grid of datapoints to determine and visualise ideal decision boundary
    '''
    data_choice = 'full_vertical_boundary'
    num__grid_points = 1000
    data_grid, grid_true_labels = generate_data(data_choice, num__grid_points)
    data_grid, grid_true_labels = remove_zeros(data_grid, grid_true_labels)

    if ideal:

        predicted_labels_test = ClassificationCircuit(classifier_qubits, data_vertical_test, qc).make_predictions(ideal_params_vertical,  n_layers, \
                                                                                        encoding_choice, init_encoding_params, num_shots)
        plot_params = {'colors': ['blue', 'orange'], 'alpha': 1}
        scatter(data_vertical_test, true_labels_test, predicted_labels_test,
                **plot_params)

        predicted_labels_grid = ClassificationCircuit(classifier_qubits, data_grid, qc).make_predictions(ideal_params_vertical, n_layers,\
                                                                             encoding_choice, init_encoding_params, num_shots)
        plot_params = {'colors': ['red', 'green'], 'alpha': 0.2}
        scatter(data_grid, predicted_labels_grid, **plot_params)
        plt.show()

    ### Define noise parameters
    '''
        # Define noise parameters to add to model to determine how classification is affected.
    '''

    noise_choice = 'amp_damp_before_measurement'
    noise_values = 0.4

    ### Add noise to circuit and classify
    '''
        # Add noise to circuit, and determine number of points classified differently (not mis-classified since we can't achieve perfect classification)
    '''

    if noise:
        ## Overlay decision boundary
        '''
        # Generate Grid of datapoints to determine and visualise ideal decision boundary WITH noise added
        '''
        predicted_labels_test_noise = ClassificationCircuit(classifier_qubits, data_vertical_test, qc,\
                     noise_choice, noise_values).make_predictions(ideal_params_vertical, n_layers, encoding_choice, init_encoding_params, num_shots)
        plot_params = {'colors': ['blue', 'orange'], 'alpha': 1}
        scatter(data_vertical_test, true_labels_test,
                predicted_labels_test_noise, **plot_params)

        predicted_labels_grid_noise = ClassificationCircuit(classifier_qubits, data_grid, qc,\
                                                            noise_choice, noise_values).make_predictions(ideal_params_vertical, n_layers, \
                                                            encoding_choice, init_encoding_params, num_shots)

        plot_params = {'colors': ['red', 'green'], 'alpha': 0.2}
        scatter(data_grid, predicted_labels_grid_noise, **plot_params)
        plt.show()
    '''
    # Define function to compute points which will remian correctly classified after noise is added
    '''
    def correct_function(data_point, params, encoding_choice, encoding_params):
        [alpha_1, alpha_2, alpha_3] = params
        [x_1, x_2] = data_point

        if encoding_choice.lower() == 'denseangle_param':
            [theta, phi] = encoding_params
            function = (np.sin(alpha_2) )**2 * ( np.cos(theta * x_1) )**2  + (np.cos(alpha_2))**2 * (np.sin(theta * x_1))**2 \
                        + ((1/2)*(np.sin(2 * alpha_2) * np.sin(2 * theta * x_1) * np.exp(-1j*(2 * alpha_3 + phi * x_2)))).real
        elif encoding_choice.lower() == 'wavefunction_param':
            [theta] = encoding_params
            l2_norm = np.linalg.norm(np.array([x_1, x_2]))**2
            function = (np.sin(alpha_2)**2 ) * ( x_1**2/(l2_norm) )  + (np.cos(alpha_2)**2) * (x_2**2/(l2_norm)) \
                        + ((1/(2*l2_norm))*(np.sin(2 * alpha_2) * (x_1) * (x_2) * np.exp(-1j*(2 * alpha_3)))).real

        return function

    def compute_analytic_misclassifed_condition(data, params, encoding_choice,
                                                encoding_params,
                                                noise_strength, true_labels):
        correct_classification_labels = []
        for ii, data_point in enumerate(data):

            function = correct_function(data_point, params, encoding_choice,
                                        encoding_params)
            if true_labels[ii] == 0:
                correct_classification_labels.append(
                    0
                )  # If datapoint was zero originally, it will be correctly classified regardless of noise

            else:
                if function > 1 / (
                        2 * (1 - noise_strength)
                ):  # If data point was classified as 1, it will be correctly classified if condition is met.
                    correct_classification_labels.append(0)

                else:
                    correct_classification_labels.append(1)
        number_robust = 1 - sum(correct_classification_labels) / len(
            correct_classification_labels)  # percentage of misclassified points
        return np.array(correct_classification_labels), number_robust

    def plot_number_misclassified_amp_damp(ideal_params, num_shots, num_points,
                                           qc, noise_values):

        points_noise_inc = []

        data_vertical_train, data_vertical_test, true_labels_train, true_labels_test = generate_data('random_vertical_boundary',\
                                                                                                     num_points=num_points, split=True)
        interval = 0.2
        encoding_choice = 'denseangle_param'
        theta = np.arange(0, 2 * np.pi, interval)
        phi = np.arange(0, 2 * np.pi, interval)
        X, Y = np.meshgrid(theta, phi)
        noise_choice = 'amp_damp_before_measurement'
        test_acc_ideal = np.zeros((theta.shape[0], phi.shape[0]), dtype=float)

        test_acc_noise = np.zeros((theta.shape[0], phi.shape[0]), dtype=float)
        number_robust = np.zeros((theta.shape[0], phi.shape[0]), dtype=float)

        for ii, t in enumerate(theta):
            for jj, p in enumerate(phi):
                temp_encoding_params = [t, p]

                # Classification of encoding parameters *without* noise
                ideal_predictions, test_acc_ideal[ii,jj]  = generate_noisy_classification(ideal_params, 1, None, None,\
                                                                                                    encoding_choice, temp_encoding_params, qc,\
                                                                                                    classifier_qubits, num_shots, data_vertical_test, true_labels_test)

                # Learned encoding parameters *with* noise
                noisy_predictions, test_acc_noise[ii,jj]  = generate_noisy_classification(ideal_params, 1, noise_choice, noise_values,\
                                                                                                    encoding_choice, temp_encoding_params, qc,\
                                                                                                    classifier_qubits, num_shots, data_vertical_test, true_labels_test)
                # Number expected to be robust under analytic condition
                correct_classification_labels, number_robust[ii, jj] = compute_analytic_misclassifed_condition(data_vertical_test, ideal_params_vertical,\
                                                                                                                encoding_choice, temp_encoding_params,\
                                                                                                                noise_values, true_labels_test)

                print('Theta, Phi is:', t, p)
                print('Test accuracy ideal:', test_acc_ideal[ii, jj])
                print('Test accuracy with noise:', test_acc_noise[ii, jj])
                print('Proportion robust:', number_robust[ii, jj])

        max_acc_indices_ideal = np.unravel_index(
            np.argmax(test_acc_ideal, axis=None), test_acc_ideal.shape)
        max_acc_indices = np.unravel_index(
            np.argmax(test_acc_noise, axis=None), test_acc_noise.shape)
        max_robust_indices = np.unravel_index(
            np.argmax(number_robust, axis=None), number_robust.shape)

        plt.rcParams.update({"font.size": 14, "font.family": "serif"})

        # ----------------------
        # Uncomment below for 3d plots
        # ----------------------

        # fig = plt.figure(figsize=plt.figaspect(0.33))
        # ax1 = fig.add_subplot(1, 3, 1, projection='3d')
        # surf1 = ax1.plot_surface(X, Y, test_acc_ideal, cmap=cm.coolwarm_r,linewidth=0, antialiased=False)
        # # ax1.set_zlim(0.45, 1.01)
        # cbar1 =fig.colorbar(surf1)
        # cbar1.ax.set_ylabel('Test Accuracy')

        # ax2 = fig.add_subplot(1, 3, 2, projection='3d')
        # surf2 = ax2.plot_surface(X, Y, test_acc_noise, cmap=cm.coolwarm_r, linewidth=0, antialiased=False)
        # # ax2.set_zlim(0.45, 1.01)
        # cbar2 = fig.colorbar(surf2)
        # cbar2.ax.set_ylabel('Test Accuracy')

        # ax3 = fig.add_subplot(1, 3, 3, projection='3d')

        # surf3 = ax3.plot_surface(X, Y, number_robust, cmap=cm.PuOr, linewidth=0, antialiased=False)
        # cbar3 = fig.colorbar(surf3)
        # cbar3.ax.set_ylabel('Proportion robust')

        # ax1.set_ylabel(r'$\theta (rads)$')
        # ax1.set_xlabel(r'$\phi (rads)$' )
        # ax1.set_title(  'Best accuracy ideal: '             + str( round( test_acc_ideal[max_acc_indices_ideal] , 2) ) \
        #                 + '\nBest accuracy with noise: '    + str( round( test_acc_noise[max_acc_indices_ideal] , 2) ) \
        #                 + '\nRobustness: '                  + str( round( number_robust[max_acc_indices_ideal]  , 2) ) + '\n' \
        #                 + r'$[\theta, \phi]$ = '            + '['+str(round(theta [ max_acc_indices_ideal[0] ], 2) )+ ', ' + str( round( phi [ max_acc_indices_ideal[1] ] , 2) ) + ']' )

        # ax2.set_ylabel(r'$\theta (rads)$')
        # ax2.set_xlabel(r'$\phi (rads)$' )
        # ax2.set_title(  'Best accuracy with noise: '    + str( round( test_acc_noise[max_acc_indices]   , 2) ) \
        #                 + '\nBest accuracy ideal: '     + str( round( test_acc_ideal[max_acc_indices]   , 2) ) \
        #                 + '\nRobustness: '               + str( round( number_robust[max_acc_indices]    , 2) ) + '\n' \
        #                 + r'$[\theta, \phi]$ = '        + '['+str(theta [ max_acc_indices[0] ])+ ', ' + str(round( phi [ max_acc_indices[1] ], 2) ) + ']' )

        # ax3.set_ylabel(r'$\theta (rads)$')
        # ax3.set_xlabel(r'$\phi (rads)$' )
        # ax3.set_title('Max. robustness: '               + str( round( number_robust[max_robust_indices]  , 2) ) \
        #                 +'\nBest accuracy with noise: ' + str( round( test_acc_noise[max_robust_indices] , 2) ) \
        #                 +'\nBest accuracy ideal: '      + str( round( test_acc_ideal[max_robust_indices] , 2) ) + '\n'\
        #                 +r'$[\theta, \phi]$ = '         + '[' + str(theta [ max_robust_indices[0] ]) \
        #                                                 + ', ' + str(phi [ max_robust_indices[1] ] ) + ']' )

        ## 2D PLOTS
        fig, ax = plt.subplots(1, 3)
        im0 = ax[0].imshow(test_acc_ideal,
                           cmap=cm.coolwarm_r,
                           extent=[0, 2 * np.pi, 2 * np.pi, 0])
        divider = make_axes_locatable(ax[0])
        cax = divider.append_axes('right', size='5%', pad=0.1)
        cbar0 = fig.colorbar(im0, cax=cax, orientation='vertical')

        cbar0.ax.set_ylabel('Test Accuracy')

        im1 = ax[1].imshow(test_acc_noise,
                           cmap=cm.coolwarm_r,
                           extent=[0, 2 * np.pi, 2 * np.pi, 0])
        divider = make_axes_locatable(ax[1])
        cax = divider.append_axes('right', size='5%', pad=0.1)
        cbar1 = fig.colorbar(im1, cax=cax, orientation='vertical')

        cbar1.ax.set_ylabel('Test Accuracy')

        im2 = ax[2].imshow(number_robust,
                           cmap=cm.PuOr,
                           extent=[0, 2 * np.pi, 2 * np.pi, 0])
        divider = make_axes_locatable(ax[2])
        cax = divider.append_axes('right', size='5%', pad=0.1)
        cbar2 = fig.colorbar(im2, cax=cax, orientation='vertical')

        cbar2.ax.set_ylabel('Proportion robust')

        ax[0].set_title(  'Best accuracy ideal: '             + str( round( test_acc_ideal[max_acc_indices_ideal] , 2) ) \
                        + '\nBest accuracy with noise: '    + str( round( test_acc_noise[max_acc_indices_ideal] , 2) ) \
                        + '\nRobustness: '                  + str( round( number_robust[max_acc_indices_ideal]  , 2) ) + '\n' \
                        + r'$[\theta, \phi]$ = '            + '['+str(round(theta [ max_acc_indices_ideal[0] ], 2) )+ ', ' + str( round( phi [ max_acc_indices_ideal[1] ] , 2) ) + ']' )

        ax[1].set_title(  'Best accuracy with noise: '    + str( round( test_acc_noise[max_acc_indices]   , 2) ) \
                        + '\nBest accuracy ideal: '     + str( round( test_acc_ideal[max_acc_indices]   , 2) ) \
                        + '\nRobustness: '               + str( round( number_robust[max_acc_indices]    , 2) ) + '\n' \
                        + r'$[\theta, \phi]$ = '        + '['+str(theta [ max_acc_indices[0] ])+ ', ' + str(round( phi [ max_acc_indices[1] ], 2) ) + ']' )

        ax[2].set_title('Max. robustness: '               + str( round( number_robust[max_robust_indices]  , 2) ) \
                        +'\nBest accuracy with noise: ' + str( round( test_acc_noise[max_robust_indices] , 2) ) \
                        +'\nBest accuracy ideal: '      + str( round( test_acc_ideal[max_robust_indices] , 2) ) + '\n'\
                        +r'$[\theta, \phi]$ = '         + '[' + str(theta [ max_robust_indices[0] ]) \
                                                        + ', ' + str(phi [ max_robust_indices[1] ] ) + ']' )

        return

    if analytic:
        correct_classification_labels, number_robust = compute_analytic_misclassifed_condition(data_grid, ideal_params_vertical,\
                                                                                                encoding_choice, init_encoding_params,\
                                                                                                noise_values, grid_true_labels)
        plot_params = {'colors': ['blue', 'black'], 'alpha': 0.3}

        scatter(data_grid, correct_classification_labels, **plot_params)
        plt.show()

    if compare:
        plot_number_misclassified_amp_damp(ideal_params_vertical, num_shots,
                                           500, qc, noise_values)
        plt.show()
    def plot_number_misclassified_amp_damp(ideal_params, num_shots, num_points,
                                           qc, noise_values):

        points_noise_inc = []

        data_vertical_train, data_vertical_test, true_labels_train, true_labels_test = generate_data('random_vertical_boundary',\
                                                                                                     num_points=num_points, split=True)
        interval = 0.2
        encoding_choice = 'denseangle_param'
        theta = np.arange(0, 2 * np.pi, interval)
        phi = np.arange(0, 2 * np.pi, interval)
        X, Y = np.meshgrid(theta, phi)
        noise_choice = 'amp_damp_before_measurement'
        test_acc_ideal = np.zeros((theta.shape[0], phi.shape[0]), dtype=float)

        test_acc_noise = np.zeros((theta.shape[0], phi.shape[0]), dtype=float)
        number_robust = np.zeros((theta.shape[0], phi.shape[0]), dtype=float)

        for ii, t in enumerate(theta):
            for jj, p in enumerate(phi):
                temp_encoding_params = [t, p]

                # Classification of encoding parameters *without* noise
                ideal_predictions, test_acc_ideal[ii,jj]  = generate_noisy_classification(ideal_params, 1, None, None,\
                                                                                                    encoding_choice, temp_encoding_params, qc,\
                                                                                                    classifier_qubits, num_shots, data_vertical_test, true_labels_test)

                # Learned encoding parameters *with* noise
                noisy_predictions, test_acc_noise[ii,jj]  = generate_noisy_classification(ideal_params, 1, noise_choice, noise_values,\
                                                                                                    encoding_choice, temp_encoding_params, qc,\
                                                                                                    classifier_qubits, num_shots, data_vertical_test, true_labels_test)
                # Number expected to be robust under analytic condition
                correct_classification_labels, number_robust[ii, jj] = compute_analytic_misclassifed_condition(data_vertical_test, ideal_params_vertical,\
                                                                                                                encoding_choice, temp_encoding_params,\
                                                                                                                noise_values, true_labels_test)

                print('Theta, Phi is:', t, p)
                print('Test accuracy ideal:', test_acc_ideal[ii, jj])
                print('Test accuracy with noise:', test_acc_noise[ii, jj])
                print('Proportion robust:', number_robust[ii, jj])

        max_acc_indices_ideal = np.unravel_index(
            np.argmax(test_acc_ideal, axis=None), test_acc_ideal.shape)
        max_acc_indices = np.unravel_index(
            np.argmax(test_acc_noise, axis=None), test_acc_noise.shape)
        max_robust_indices = np.unravel_index(
            np.argmax(number_robust, axis=None), number_robust.shape)

        plt.rcParams.update({"font.size": 14, "font.family": "serif"})

        # ----------------------
        # Uncomment below for 3d plots
        # ----------------------

        # fig = plt.figure(figsize=plt.figaspect(0.33))
        # ax1 = fig.add_subplot(1, 3, 1, projection='3d')
        # surf1 = ax1.plot_surface(X, Y, test_acc_ideal, cmap=cm.coolwarm_r,linewidth=0, antialiased=False)
        # # ax1.set_zlim(0.45, 1.01)
        # cbar1 =fig.colorbar(surf1)
        # cbar1.ax.set_ylabel('Test Accuracy')

        # ax2 = fig.add_subplot(1, 3, 2, projection='3d')
        # surf2 = ax2.plot_surface(X, Y, test_acc_noise, cmap=cm.coolwarm_r, linewidth=0, antialiased=False)
        # # ax2.set_zlim(0.45, 1.01)
        # cbar2 = fig.colorbar(surf2)
        # cbar2.ax.set_ylabel('Test Accuracy')

        # ax3 = fig.add_subplot(1, 3, 3, projection='3d')

        # surf3 = ax3.plot_surface(X, Y, number_robust, cmap=cm.PuOr, linewidth=0, antialiased=False)
        # cbar3 = fig.colorbar(surf3)
        # cbar3.ax.set_ylabel('Proportion robust')

        # ax1.set_ylabel(r'$\theta (rads)$')
        # ax1.set_xlabel(r'$\phi (rads)$' )
        # ax1.set_title(  'Best accuracy ideal: '             + str( round( test_acc_ideal[max_acc_indices_ideal] , 2) ) \
        #                 + '\nBest accuracy with noise: '    + str( round( test_acc_noise[max_acc_indices_ideal] , 2) ) \
        #                 + '\nRobustness: '                  + str( round( number_robust[max_acc_indices_ideal]  , 2) ) + '\n' \
        #                 + r'$[\theta, \phi]$ = '            + '['+str(round(theta [ max_acc_indices_ideal[0] ], 2) )+ ', ' + str( round( phi [ max_acc_indices_ideal[1] ] , 2) ) + ']' )

        # ax2.set_ylabel(r'$\theta (rads)$')
        # ax2.set_xlabel(r'$\phi (rads)$' )
        # ax2.set_title(  'Best accuracy with noise: '    + str( round( test_acc_noise[max_acc_indices]   , 2) ) \
        #                 + '\nBest accuracy ideal: '     + str( round( test_acc_ideal[max_acc_indices]   , 2) ) \
        #                 + '\nRobustness: '               + str( round( number_robust[max_acc_indices]    , 2) ) + '\n' \
        #                 + r'$[\theta, \phi]$ = '        + '['+str(theta [ max_acc_indices[0] ])+ ', ' + str(round( phi [ max_acc_indices[1] ], 2) ) + ']' )

        # ax3.set_ylabel(r'$\theta (rads)$')
        # ax3.set_xlabel(r'$\phi (rads)$' )
        # ax3.set_title('Max. robustness: '               + str( round( number_robust[max_robust_indices]  , 2) ) \
        #                 +'\nBest accuracy with noise: ' + str( round( test_acc_noise[max_robust_indices] , 2) ) \
        #                 +'\nBest accuracy ideal: '      + str( round( test_acc_ideal[max_robust_indices] , 2) ) + '\n'\
        #                 +r'$[\theta, \phi]$ = '         + '[' + str(theta [ max_robust_indices[0] ]) \
        #                                                 + ', ' + str(phi [ max_robust_indices[1] ] ) + ']' )

        ## 2D PLOTS
        fig, ax = plt.subplots(1, 3)
        im0 = ax[0].imshow(test_acc_ideal,
                           cmap=cm.coolwarm_r,
                           extent=[0, 2 * np.pi, 2 * np.pi, 0])
        divider = make_axes_locatable(ax[0])
        cax = divider.append_axes('right', size='5%', pad=0.1)
        cbar0 = fig.colorbar(im0, cax=cax, orientation='vertical')

        cbar0.ax.set_ylabel('Test Accuracy')

        im1 = ax[1].imshow(test_acc_noise,
                           cmap=cm.coolwarm_r,
                           extent=[0, 2 * np.pi, 2 * np.pi, 0])
        divider = make_axes_locatable(ax[1])
        cax = divider.append_axes('right', size='5%', pad=0.1)
        cbar1 = fig.colorbar(im1, cax=cax, orientation='vertical')

        cbar1.ax.set_ylabel('Test Accuracy')

        im2 = ax[2].imshow(number_robust,
                           cmap=cm.PuOr,
                           extent=[0, 2 * np.pi, 2 * np.pi, 0])
        divider = make_axes_locatable(ax[2])
        cax = divider.append_axes('right', size='5%', pad=0.1)
        cbar2 = fig.colorbar(im2, cax=cax, orientation='vertical')

        cbar2.ax.set_ylabel('Proportion robust')

        ax[0].set_title(  'Best accuracy ideal: '             + str( round( test_acc_ideal[max_acc_indices_ideal] , 2) ) \
                        + '\nBest accuracy with noise: '    + str( round( test_acc_noise[max_acc_indices_ideal] , 2) ) \
                        + '\nRobustness: '                  + str( round( number_robust[max_acc_indices_ideal]  , 2) ) + '\n' \
                        + r'$[\theta, \phi]$ = '            + '['+str(round(theta [ max_acc_indices_ideal[0] ], 2) )+ ', ' + str( round( phi [ max_acc_indices_ideal[1] ] , 2) ) + ']' )

        ax[1].set_title(  'Best accuracy with noise: '    + str( round( test_acc_noise[max_acc_indices]   , 2) ) \
                        + '\nBest accuracy ideal: '     + str( round( test_acc_ideal[max_acc_indices]   , 2) ) \
                        + '\nRobustness: '               + str( round( number_robust[max_acc_indices]    , 2) ) + '\n' \
                        + r'$[\theta, \phi]$ = '        + '['+str(theta [ max_acc_indices[0] ])+ ', ' + str(round( phi [ max_acc_indices[1] ], 2) ) + ']' )

        ax[2].set_title('Max. robustness: '               + str( round( number_robust[max_robust_indices]  , 2) ) \
                        +'\nBest accuracy with noise: ' + str( round( test_acc_noise[max_robust_indices] , 2) ) \
                        +'\nBest accuracy ideal: '      + str( round( test_acc_ideal[max_robust_indices] , 2) ) + '\n'\
                        +r'$[\theta, \phi]$ = '         + '[' + str(theta [ max_robust_indices[0] ]) \
                                                        + ', ' + str(phi [ max_robust_indices[1] ] ) + ']' )

        return
def main(train=False,
         retrain=False,
         qc_name='2q-qvm',
         data_choice='iris',
         noise_choice='decoherence_symmetric_ro',
         noise_values=None):

    ### Firstly, generate for dataset:

    data_train, data_test, true_labels_train, true_labels_test = generate_data(
        data_choice, num_points=500, split=True)

    data_train, true_labels_train = remove_zeros(data_train, true_labels_train)
    data_test, true_labels_test = remove_zeros(data_test, true_labels_test)

    # encodings = [ 'denseangle_param','superdenseangle_param', 'wavefunction_param' ]
    encodings = ['superdenseangle_param']
    minimal_costs, ideal_costs, noisy_costs, noisy_costs_uncorrected = [
        np.ones(len(encodings)) for _ in range(4)
    ]

    # qc_name = '2q-qvm'
    qc = get_qc(qc_name)
    num_shots = 1024
    device_qubits = qc.qubits()
    classifier_qubits = device_qubits
    n_layers = 1
    # init_params = np.random.rand(len(qubits),n_layers,  3)
    # init_params = np.random.rand(len(qubits),n_layers,  3)
    # init_params = np.random.rand((7)) # TTN
    init_params = np.random.rand((12))  # General 2 qubit unitary

    ideal_params = []
    ideal_encoding_params = []
    init_encoding_params = []

    for ii, encoding_choice in enumerate(encodings):

        print('\n**********************************')
        print('\nThe encoding is:', encoding_choice)
        print('\n**********************************')

        if encoding_choice.lower() == 'wavefunction_param':
            init_encoding_params.append(
                [0]
            )  # Generalized Wavefunction Encoding initialised to Wavefunction encoding
        else:
            init_encoding_params.append([np.pi, 2 * np.pi])

        if train:

            optimiser = 'Powell'
            params, result_unitary_param = train_classifier(qc, classifier_qubits, num_shots, init_params, n_layers,\
                                                            encoding_choice, init_encoding_params[ii],\
                                                            optimiser, data_train, true_labels_train)

            print('The optimised parameters are:', result_unitary_param.x)
            print('These give a cost of:', ClassificationCircuit(classifier_qubits, data_train, qc).build_classifier(result_unitary_param.x, n_layers,\
                                                                                                        encoding_choice, init_encoding_params[ii],\
                                                                                                        num_shots, true_labels_train))
            ideal_params.append(result_unitary_param.x)
        else:

            if data_choice.lower() == 'iris':
                if encoding_choice.lower() == 'denseangle_param':                    ideal_params.append([ 2.02589489, 1.24358318, -0.6929718,\
                                             0.85764484, 2.7572075, 1.12317156, \
                                             4.01974889, 0.30921738, 0.88106973,\
                                             1.461694, 0.367226, 5.01508911 ])

                    # elif    encoding_choice.lower() == 'superdenseangle_param':         ideal_params.append([1.1617383, -0.05837820, -0.7216498,\
                    #                                                                                         1.3195103, 0.52933357, 1.2854939,
                    #                                                                                         1.2097700, 0.26920745, 0.4239539,
                    #                                                                                         1.2999367, 0.37921617, 0.790320211])

                elif encoding_choice.lower() == 'superdenseangle_param':                    ideal_params.append([ 2.91988765,  1.85012634,  1.75234515,  0.81420946,\
                                        1.286217,0.62223565, 0.8356422, 1.36681893,  0.58494563,\
                                         -0.02031075,  0.80183355,  6.92525521])

                elif encoding_choice.lower() == 'wavefunction':                    ideal_params.append([ 2.37732073, 1.01449711, 1.12025344,\
                                -0.087440021, 0.46937127, 2.14387135, \
                                0.4696964, 1.444409282, 0.14412614,\
                                1.4825742, 1.0817654, 6.30943537 ])

                elif encoding_choice.lower() == 'wavefunction_param':                    ideal_params.append([ 2.37732073, 1.01449711, 1.12025344,\
                                -0.087440021, 0.46937127, 2.14387135, \
                                0.4696964, 1.444409282, 0.14412614,\
                                1.4825742, 1.0817654, 6.30943537 ])

        ideal_costs[ii] = ClassificationCircuit(classifier_qubits, data_test, qc).build_classifier(ideal_params[ii], n_layers, \
                                                                                                encoding_choice, init_encoding_params[ii], \
                                                                                                num_shots, true_labels_test)

        print('In the ideal case, the cost is:', ideal_costs[ii])

        predicted_labels_ideal = ClassificationCircuit(classifier_qubits, data_test, qc).make_predictions(ideal_params[ii], n_layers,\
                                                                                                    encoding_choice, init_encoding_params[ii],\
                                                                                                    num_shots)

        if noise_choice is not None:

            noisy_costs_uncorrected[ii] = ClassificationCircuit(classifier_qubits, data_test, qc, \
                                                            noise_choice, noise_values).build_classifier(ideal_params[ii], n_layers,\
                                                                                                        encoding_choice, init_encoding_params[ii],\
                                                                                                        num_shots, true_labels_test)
            print('\nWithout encoding training, the noisy cost is:',
                  noisy_costs_uncorrected[ii])

            noisy_predictions, number_classified_same = generate_noisy_classification(ideal_params[ii], n_layers,\
                                                                                    noise_choice, noise_values,\
                                                                                    encoding_choice, init_encoding_params[ii],\
                                                                                    qc, classifier_qubits, num_shots, data_test, predicted_labels_ideal)

            print('The proportion classified differently after noise is:',
                  1 - number_classified_same)

            if retrain:
                if encoding_choice.lower() == 'wavefunction_param':
                    optimiser = 'L-BFGS-B'
                else:
                    optimiser = 'Powell'

                encoding_params, result_encoding_param = train_classifier_encoding(
                    qc, noise_choice, noise_values, num_shots,
                    ideal_params[ii], encoding_choice,
                    init_encoding_params[ii], optimiser, data_train,
                    true_labels_train)
                print('The optimised encoding parameters with noise are:',
                      result_encoding_param.x)
                ideal_encoding_params.append(result_encoding_param.x)

            else:
                if data_choice.lower() == 'iris' and noise_choice.lower(
                ) == 'decoherence_symmetric_ro':

                    if encoding_choice.lower() == 'denseangle_param':
                        ideal_encoding_params.append(init_encoding_params[ii])
                    elif encoding_choice.lower() == 'superdenseangle_param':
                        ideal_encoding_params.append(init_encoding_params[ii])
                    elif encoding_choice.lower() == 'wavefunction_param':
                        ideal_encoding_params.append(init_encoding_params[ii])

                else:
                    print('THIS DATASET HAS NOT BEEN TRAINED FOR')
                    if encoding_choice.lower() == 'denseangle_param':
                        ideal_encoding_params.append(init_encoding_params[ii])
                    elif encoding_choice.lower() == 'superdenseangle_param':
                        ideal_encoding_params.append(init_encoding_params[ii])
                    elif encoding_choice.lower() == 'wavefunction_param':
                        ideal_encoding_params.append(init_encoding_params[ii])

            noisy_costs[ii] = ClassificationCircuit(classifier_qubits, data_test, qc,  \
                                                    noise_choice, noise_values).build_classifier(ideal_params[ii], n_layers, \
                                                                                                encoding_choice, ideal_encoding_params[ii],\
                                                                                                num_shots, true_labels_test)

            print('\nWith encoding training, the noisy cost is:',
                  noisy_costs[ii])

            noisy_predictions, number_classified_same = generate_noisy_classification(ideal_params[ii], n_layers, \
                                                                                        noise_choice, noise_values,\
                                                                                        encoding_choice, ideal_encoding_params[ii],\
                                                                                        qc, classifier_qubits, num_shots, data_test, predicted_labels_ideal)

            print(
                'The proportion classified differently after noise with learned encoding is:',
                1 - number_classified_same)

    # for ii, encoding_choice in enumerate(encodings):
    # print('\nThe encoding is:'                      , encodings[ii]                 )
    # print('The ideal params are:'                   , ideal_params[ii]              )
    # print('The ideal encoding params are'           , ideal_encoding_params[ii]     )

    # print('The ideal cost for encoding'             , ideal_costs[ii]               )
    # print('The noisy cost with untrained encoding'  , noisy_costs_uncorrected[ii]   )
    # print('The noisy cost with trained encoding'    , noisy_costs[ii]               )

    return encodings, ideal_params, init_encoding_params, ideal_encoding_params, ideal_costs, noisy_costs_uncorrected, noisy_costs