#!/usr/bin/env python
# coding: utf-8

# # Self-Driving Car Engineer Nanodegree
#
# ## Deep Learning
#
# ## Project: Build a Traffic Sign Recognition Classifier
#
# ##### Author Ian Whittal
#
#
# The [rubric](https://review.udacity.com/#!/rubrics/481/view) contains "Stand Out Suggestions" for enhancing the project beyond the minimum requirements. The stand out suggestions are optional. If you decide to pursue the "stand out suggestions", you can include the code in this Ipython notebook and also discuss the results in the writeup file.
#

#
# ## Import Libraries

# In[1]:

import pickle
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import random
import scipy.ndimage
import cv2
get_ipython().run_line_magic('matplotlib', 'inline')

# ---
# ## Load The Data

# In[2]:

# Load pickled data
# import pickle

# Reference location of saved the training, validation and testing data

training_file = "../data/train.p"
validation_file= "../data/valid.p"
testing_file = "../data/test.p"

with open(training_file, mode='rb') as f: # rb -> (r) read only and (b) opens the file in binary mode.
    train = pickle.load(f) # train is a dictionary
with open(validation_file, mode='rb') as f:
    valid = pickle.load(f) # valid is a dictionary
with open(testing_file, mode='rb') as f:
    test = pickle.load(f) # test is a dictionary

# Numpy Arrays
X_train, y_train = train['features'], train['labels']
X_valid, y_valid = valid['features'], valid['labels']
X_test, y_test = test['features'], test['labels']

# Check to ensure features equals labels for each data set
assert(len(X_train) == len(y_train))
assert(len(X_valid) == len(y_valid))
assert(len(X_test) == len(y_test))

# Print shapes of training, validation and test data

print("X_train shape:", X_train[0].shape)
print("y_train shape:", y_train.shape)
print("X_valid shape:", X_valid[0].shape)
print("y_valid shape:", y_valid.shape)
print("X_test shape:", X_test[0].shape)
print("y_test shape:", y_test.shape)

# ---
#
# ## Dataset Summary & Exploration
#
# The pickled data is a dictionary with 4 key/value pairs:
#
# - `'features'` is a 4D array containing raw pixel data of the traffic sign images, (num examples, width, height, channels).
# - `'labels'` is a 1D array containing the label/class id of the traffic sign. The file `signnames.csv` contains id -> name mappings for each id.
# - `'sizes'` is a list containing tuples, (width, height) representing the original width and height the image.
# - `'coords'` is a list containing tuples, (x1, y1, x2, y2) representing coordinates of a bounding box around the sign in the image. **THESE COORDINATES ASSUME THE ORIGINAL IMAGE. THE PICKLED DATA CONTAINS RESIZED VERSIONS (32 by 32) OF THESE IMAGES**
#
# Complete the basic data summary below. Use python, numpy and/or pandas methods to calculate the data summary rather than hard coding the results. For example, the [pandas shape method](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.shape.html) might be useful for calculating some of the summary results.

# ### Basic Summary of the Data Set Using Python, Numpy

# In[3]:

### Replace each question mark with the appropriate value.
### Use python, pandas or numpy methods rather than hard coding the results

# Number of training examples
n_train = len(X_train)

# Number of validation examples
n_validation = len(X_valid)

# Number of testing examples.
n_test = len(X_test)

# hat's the shape of an traffic sign image?
image_shape = X_train[0].shape

# How many unique classes/labels there are in the dataset. python np.unique() returns unique in array, use training dataset as it will have all labels
n_classes = len(np.unique(y_train))

print("Number of training examples =", n_train)
print("Number of validation examples =", n_validation)
print("Number of testing examples =", n_test)
print("Shape of first training image =", image_shape[0], "w x", image_shape[1], "h x ", image_shape[2], "d")
print("Number of classes =", n_classes)

# ### Exploratory Visualization of the Dataset

# Visualize the German Traffic Signs Dataset using the pickled file(s).  From the visualization we can clearly see this is a large amount of training data, however there is very little validation data.  The test data seems appropriate enough to test our classifier on.
#
# The histogram below shows an overlay of each data set across each of the 43 different sign classes.  Of additional note is that there are limited images available for individual classes, which may make those images harder to classify accurately.  I will want to use data augmentation to create more images overall for training.
#

# In[4]:

# Show visualations of dataset

fig, axs = plt.subplots(2,10, figsize=(15, 6))
fig.subplots_adjust(hspace = .2, wspace=.1)
axs = axs.ravel()
for i in range(20):
    index = random.randint(0, len(X_train))
    image = X_train[index]
    axs[i].axis('off')
    axs[i].imshow(image)
    axs[i].set_title(y_train[index])

# In[5]:

# Histogram of Image Class Distribution

a = y_train
b = y_valid
c = y_test
bins = n_classes

plt.hist(a, bins, alpha = 1.0, label='Training data')
plt.hist(b, bins, alpha = 1.0, label='Validation data')
plt.hist(c, bins, alpha = 0.6, label='Test data')
plt.legend(loc='upper center')

plt.show()

# ----
#
# ## Design and Test a Model Architecture
#
# Design and implement a deep learning model that learns to recognize traffic signs. Train and test your model on the [German Traffic Sign Dataset](http://benchmark.ini.rub.de/?section=gtsrb&subsection=dataset).
#
# The LeNet-5 implementation shown in the [classroom](https://classroom.udacity.com/nanodegrees/nd013/parts/fbf77062-5703-404e-b60c-95b78b2f3f9e/modules/6df7ae49-c61c-4bb2-a23e-6527e69209ec/lessons/601ae704-1035-4287-8b11-e2c2716217ad/concepts/d4aca031-508f-4e0b-b493-e7b706120f81) at the end of the CNN lesson is a solid starting point. You'll have to change the number of classes and possibly the preprocessing, but aside from that it's plug and play!
#
# With the LeNet-5 solution from the lecture, you should expect a validation set accuracy of about 0.89. To meet specifications, the validation set accuracy will need to be at least 0.93. It is possible to get an even higher accuracy, but 0.93 is the minimum for a successful project submission.
#
# There are various aspects to consider when thinking about this problem:
#
# - Neural network architecture (is the network over or underfitting?)
# - Play around preprocessing techniques (normalization, rgb to grayscale, etc)
# - Number of examples per label (some have more than others).
# - Generate fake data.
#
# Here is an example of a [published baseline model on this problem](http://yann.lecun.com/exdb/publis/pdf/sermanet-ijcnn-11.pdf). It's not required to be familiar with the approach used in the paper but, it's good practice to try to read papers like these.

# ## Data Augmentation (Rotation)

# In[6]:

## Data augmentation technique: Rotate training images
degrees = 10

# Rotating images
degrees_positive = 10
X_train_rotated_positive = []

for i in range(len(X_train)):
    rotated_image = scipy.ndimage.rotate(X_train[i], degrees_positive)
    X_train_rotated_positive.append(rotated_image)

degrees_negative = 350
X_train_rotated_negative = []
for i in range(len(X_train)):
    rotated_image = scipy.ndimage.rotate(X_train[i], degrees_negative)
    X_train_rotated_negative.append(rotated_image)

# Crop image, due to other image size after rotation. Attention: it's not automated! It fit's currently to the 10° rotation.
for i in range(len(X_train_rotated_positive)):
    X_train_rotated_positive[i] = X_train_rotated_positive[i][2:34,2:34]   # box=(y:y+crop, x:x+crop)

for i in range(len(X_train_rotated_negative)):
    X_train_rotated_negative[i] = X_train_rotated_negative[i][2:34,2:34]   # box=(y:y+crop, x:x+crop)

# appending rotated images to training set

# Convert the data into list type to use the method "append"
X_train = list(X_train)

# combine the Lists
for i in range(len(X_train_rotated_positive)):
    X_train.append(X_train_rotated_positive[i])
    X_train.append(X_train_rotated_negative[i])

#Convert the data back to a np.array
X_train = np.array(X_train)

# New number of training examples (after data augmentation)
new_n_train = len(X_train)
print("New number of training samples after data augmentation =", new_n_train)

# do the same for the labels y_train
# Convert the data into list type to use the method "append"
y_train = list(y_train)

# lengthen the list
for i in range(len(y_train)):
    y_train.append(y_train[i])
    y_train.append(y_train[i])

#Convert the data back to a np.array
y_train = np.array(y_train)

# New length of ground truth labels
new_n_train_y = len(y_train)
print("New length of ground truth labels =", new_n_train_y)

# In[ ]:

# Display an example for rotation

fig, (ax1, ax2, ax3) = plt.subplots(1,3)
ax1.imshow(X_train[2018])
ax1.set_title('Original')
ax1.axis('ON')  # clear x- and y-axes
ax2.imshow(X_train_rotated_positive[2018])
ax2.set_title('Rotated +10°')
ax2.axis('ON')  # clear x- and y-axes
ax3.imshow(X_train_rotated_negative[2018])
ax3.set_title('Rotated -10°')
ax3.axis('ON')  # clear x- and y-axes
plt.savefig('./examples/Figure_original_rotated.jpg', dpi=300)
plt.show()

# ## Pre-process the Data Set (normalization, grayscale, etc.)

# ###  Shuffle
#
# Shuffle the training data.

# In[7]:

from sklearn.utils import shuffle

X_train, y_train = shuffle(X_train, y_train)

print('shuffle complete')

# ### Normalizing
#
# Normalizing helps the network to converge faster. It makes it a lot easier for the optimizer to proceed numerically. It is required to normalize the image data so that the data has mean zero and equal variance. Minimally, the image data should be normalized so that the data has mean zero and equal variance. For image data, `(pixel - 127.5)/ 255` will take the pixel  is a quick way to approximately normalize the data and can be used in this project. A well conditioned image allows the optimizer to be more efficient to find a solution.
#
# ### Single-channel images (e.g. grayscale)
#
# As Pierre Sermanet and Yann LeCun mentioned in their [paper](http://yann.lecun.com/exdb/publis/pdf/sermanet-ijcnn-11.pdf), using color channels didn't seem to improve things a lot.  Therefore, I will only use a single channel in my model, e.g. grayscale images instead of color RGB.
#
# ### Histogram Equalization
# This method usually increases the global contrast of many images, especially when the usable data of the image is represented by close contrast values. Through this adjustment, the intensities can be better distributed on the histogram. This allows for areas of lower local contrast to gain a higher contrast. Histogram equalization accomplishes this by effectively spreading out the most frequent intensity values.
#

# In[8]:

# # Normalizing Images for zero mean and equal variance to improve convergence rate
def normalize(image):
    normal = (image - 127.5)/255
    return np.reshape(normal, (32,32,1))

# Convert RGB to grayscale
def grayscale(image):
    gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
    return np.reshape(gray, (32,32,1))

# Histogram equalization to improve contrast
def histogram_equalize(image):
    equal = cv2.equalizeHist(image)
    return np.reshape(equal, (32,32,1))

# In[9]:

# training set data preprocessing
X_train_normalized = []

for image in X_train:
    gray_image = grayscale(image)
    equal_image = histogram_equalize(gray_image)
    normalized_image = normalize(equal_image)
    X_train_normalized.append(normalized_image)

# validation set data preprocessing
X_valid_normalized = []

for image in X_valid:
    gray_image = grayscale(image)
    equal_image = histogram_equalize(gray_image)
    normalized_image = normalize(equal_image)
    X_valid_normalized.append(normalized_image)

# test set data preprocessing
X_test_normalized = []

for image in X_test:
    gray_image = grayscale(image)
    equal_image = histogram_equalize(gray_image)
    normalized_image = normalize(equal_image)
    X_test_normalized.append(normalized_image)

print('completed')

# ### Model Architecture (Deep Learning Model) Based on LeNet Architecture

# ![LeNet Architecture](lenet.png)
# Source: Yan LeCun
#
# #### Parameters
#
# To calculate the number of neurons in each layer on our CNN:
#
# Given:
# *  Input Layer has a Width `W` and a Height `H`
# *  Convolution Layer has a Filter size `F`
# *  Stride of `S`
# *  Padding `P`  Note no padding was neccessary for image library (images are 32x32x3)
# *  Number of Filters `K`
#
# Formula:
# *  Width of Next Layer `W_out = [(W - F + 2P) / S] + 1`
# *  Height of Next Layer `H_out = [(H - F + 2P) / S] + 1`
# *  Output Depth `D_out = K` Number of filters
# *  Output Volume `V_out = W_out * H_out * D_out`
#
# With parameter sharing, each neuron in an output channel shares it's weights with every other neuron in that channel.  So the number of parameters is equal to the number of neurons in the filter, plus a bias neuron, all multiplied by the number of channels in the output layer.
#
# **Remember with weight sharing we use the same filter for an entire depth slice!**
#
# If you have N inputs, & K outputs you have (N+1)K parameters to use.
#
#

# In[10]:

from tensorflow.contrib.layers import flatten

def LeNet(x, keep_prob):
    # Hyperparameters for tuning

    #Agruments used for tf.truncated_normal, randomly defines variables for the weights and biases for each layer
    mu = 0 # zero mean
    sigma = 0.1 # variance

    # Layer 1: Convolutional. Input = 32x32x1. Output = 28x28x6.
    conv1_W = tf.Variable(tf.truncated_normal(shape=(5, 5, 1, 6), mean = mu, stddev = sigma)) #[5,5,1,6] is a 5x5 filter with a input depth of 1 and output depth of 6
    conv1_b = tf.Variable(tf.zeros(6))
    conv1   = tf.nn.conv2d(x, conv1_W, strides=[1, 1, 1, 1], padding='VALID') + conv1_b # strides = [batch, y_direction, x_direction, input_channels]
    print("Shape after 1st convolutional layer: ", conv1.shape)

    # Activation Function: Rectified Linear Units (ReLU)
    '''The rectified linear activation function or ReLU for short is a piecewise linear function that will output the
    input directly if it is positive, otherwise, it will output zero. The sigmoid and hyperbolic tangent activation
    functions cannot be used in networks with many layers due to the vanishing gradient problem.
    The rectified linear activation function overcomes the vanishing gradient problem, allowing models to learn faster
    and perform better.'''
    conv1 = tf.nn.relu(conv1)

    # Subsampling - Pooling - Max Pooling. Input = 28x28x6. Output = 14x14x6.
    '''A pooling layer is generally used to:
        * Decrease the size of the output
        * Prevent Overfitting'''

    conv1 = tf.nn.max_pool(conv1, ksize=[1,2,2,1], strides=[1,2,2,1], padding='VALID') # 2x2 Kernal. 2x2 Stride
    print("Shape after 1st pooling: ", conv1.shape)

    # Layer 2: Convolutional. Output = 10x10x16.
    conv2_W = tf.Variable(tf.truncated_normal(shape=(5, 5, 6, 16), mean = mu, stddev = sigma))
    conv2_b = tf.Variable(tf.zeros(16))
    conv2   = tf.nn.conv2d(conv1, conv2_W, strides=[1, 1, 1, 1], padding='VALID') + conv2_b
    print("Shape after 2nd convolutional layer: ", conv2.shape)

    # Activation Function: Rectified Linear Units (ReLU)
    conv2 = tf.nn.relu(conv2)

    # Subsampling - Pooling - Max Pooling. Input = 10x10x16. Output = 5x5x16
    conv2 = tf.nn.max_pool(conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='VALID')
    print("Shape after 2nd pooling: ", conv2.shape)

    # Flatten. Flattens to a vector Input = 5x5x16 = 400, therefore Output = 400.
    fc0   = flatten(conv2)

    # Layer 3: Fully Connected. Input = 400. Output = 120.
    fc1_W = tf.Variable(tf.truncated_normal(shape=(400, 120), mean = mu, stddev = sigma))
    fc1_b = tf.Variable(tf.zeros(120))
    fc1   = tf.matmul(fc0, fc1_W) + fc1_b
    print("Shape after 1st fully connected layer: ", fc1.shape)

    # Activation Function: Rectified Linear Units (ReLU)
    fc1    = tf.nn.relu(fc1)

    # Layer 4: Fully Connected. Input = 120. Output = 84.
    fc2_W  = tf.Variable(tf.truncated_normal(shape=(120, 84), mean = mu, stddev = sigma))
    fc2_b  = tf.Variable(tf.zeros(84))
    fc2    = tf.matmul(fc1, fc2_W) + fc2_b
    print("Shape after 2nd fully connected layer: ", fc2.shape)

    # Activation Function: Rectified Linear Units (ReLU)
    fc2    = tf.nn.relu(fc2)

    # Regularization: Dropout
    '''Dropout is a regularization technique for reducing overfitting.  The technique temporarily drops units (artificial neurons) from
    from the network, along with all of those units incoming and outgoing connections'''
    fc2 = tf.nn.dropout(fc2, keep_prob)
    print("Shape after dropout: ", fc2.shape)

    # Layer 5: Fully Connected. Input = 84. Output = 43.
    fc3_W  = tf.Variable(tf.truncated_normal(shape=(84, 43), mean = mu, stddev = sigma))
    fc3_b  = tf.Variable(tf.zeros(43))
    logits = tf.matmul(fc2, fc3_W) + fc3_b
    print("Shape of Logits after 3rd fully connected layer: ", logits.shape)

    return logits

print('completed')

# ## Train, Validate and Test the Model

# A validation set can be used to assess how well the model is performing. A low accuracy on the training and validation
# sets imply underfitting. A high accuracy on the training set but low accuracy on the validation set implies overfitting.
#
# ### Setup Hyperparameters
#
# `keep_prob` Dropout is a regularization technique for reducing overfitting.  The technique temporarily drops units (artificial neurons) from the network, along with all of those units incoming and outgoing connections.  From Nitish Srivastava [paper](https://www.cs.toronto.edu/~hinton/absps/JMLRdropout.pdf) *"Dropout probability p independent of other units, where p can be chosen using a validation set or can simply be set at 0.5, which seems to be close to optimal for a wide range of networks and tasks."
#
# `EPOCHS` tells TensorFlow how many times to run our training data through the network in general the more epochs, the better our model will train but also the longer training will take.
#
# `BATCH_SIZE` tells TensorFlow how many training images to run through the network at a time the larger the batch size, the faster our model will train, but our processor may have a memory limit on how large a batch it can run.
#
# `rate` learning rate tells TensorFlow how quickly to update the network's weights; 0.001 is a good default value but can be experimented with.

# In[11]:

keep_prob = 1.0  #0.5 for Training/Valdation, 1.0 for Test

EPOCHS = 40

BATCH_SIZE = 100

rate = 0.001

# ### Features and Labels
# Train our modified LeNet CNN to classify training data.
#
# `x` is a placeholder for a batch of input images. Initalize the batch size to `None` which allows the placeholder to later accept a batch of any size.
#
# `y` is a placeholder for a batch of output labels. Initially our `y` labels come through with sparce variables, which means they are integers and not `one-hot` encoded yet.
#
# `one_hot_y` is a one-hot-label, a 1D list that is 'n' length of the classes

# In[12]:

x = tf.placeholder(tf.float32, (None, 32, 32, 1)) # images
y = tf.placeholder(tf.int32, (None)) # labels
one_hot_y = tf.one_hot(y, 43) # One-hot label encode each of the 43 classes

# ### Training Pipeline
# Training pipeline that uses the model to classify the "training" traffic sign data.
#
# `LeNet` pass input date to the LeNet function to calculate our `logits`
#
# The learning `rate` tells TensorFlow how quickly to update the network's weights.
#
# A `logits` function, also known as the log-odds function, is a function that represents probability values from 0 to 1, and negative infinity to infinity. The function is an inverse to the sigmoid function that limits values between 0 and 1 across the Y-axis, rather than the X-axis.
#
# Cross Entropy - After determining our `logits` we need to assign a probability to each label, which can be used to classify the data.  The `softmax function` turns logits into probabilities.  Cross-entropy is just a measure of how different the logits are from the ground truth training labels.
#
# Loss Operation - The `tf.reduce_mean` function averages the cross entropy from all of the training images.
#
# Optimizer - `AdamOptimizer` is a replacement optimization algorithm for stochastic gradient descent (SGD) for training deep learning models. Adam combines the best properties of the AdaGrad and RMSProp algorithms to provide an optimization algorithm that can handle sparse gradients on noisy problems.  We use our hyperparamter `rate` to tune the learning rate here.
#
# Training Operation - The `minimize` function is used on the optimizer which uses backpropagation to update the network and minimize our training loss

# In[13]:

# Pipeline

logits = LeNet(x, keep_prob) # pass the input data to the LeNet function to calculate our logits

cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels = one_hot_y, logits = logits) # softmax function, compare those
# logits to the ground truth labels and calculate the cross entropy.

loss_operation = tf.reduce_mean(cross_entropy) # averages the CE from all of the training images

optimizer = tf.train.AdamOptimizer(learning_rate = rate) # uses Adam algorithm to minimize the loss function similarly to SGD.

training_operation = optimizer.minimize(loss_operation) # run the minimize function on the optimzier

# ### Model Evaluation
#
# Evaluate how well the loss and accuracy of the model for a given dataset.
#
# `correct_prediction` is to measure whether a given prediction is correct by comparing the `logits` prediction to the `one_hot_y` ground truth label.
#
# `accuracy_operation` is to calculate the model's overall accuracy by averaging the individual prediction accuracies.
#
# `evaluate` function takes a dataset as input, sets intial variables `num_examples` and `total_accuracy` and batches to data set to run it through the evaluation pipeline.  The function averages each batch to calculate the total accuracy of the model.
#
#

# In[14]:

correct_prediction = tf.equal(tf.argmax(logits, 1), tf.argmax(one_hot_y, 1))
accuracy_operation = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

def evaluate(X_data, y_data):
    num_examples = len(X_data)
    total_accuracy = 0
    sess = tf.get_default_session()
    for offset in range(0, num_examples, BATCH_SIZE):
        batch_x, batch_y = X_data[offset:offset+BATCH_SIZE], y_data[offset:offset+BATCH_SIZE] # batch the dataset
        accuracy = sess.run(accuracy_operation, feed_dict={x: batch_x, y: batch_y}) # accuracy = 1 or 0
        total_accuracy += (accuracy * len(batch_x))
    return total_accuracy / num_examples

# ### Train the Model
#
# Run the training data through the training pipeline to train the model.
#
# Before each `EPOCHS`, `shuffle` the training set.
#
# After each `EPOCHS`, measure the loss and accuracy of the validation set.
#
# Save the model after training.
#

# In[15]:

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    num_examples = len(X_train)

    print("Training...")
    print()
    for i in range(EPOCHS): # EPOCHS is a set hyperparameter
        X_train_normalized, y_train = shuffle(X_train_normalized, y_train) # shuffle to prevent training data bias
        for offset in range(0, num_examples, BATCH_SIZE): # range(start, stop, step)
            end = offset + BATCH_SIZE
            batch_x = X_train_normalized[offset:end] # break training data into batches
            batch_y = y_train[offset:end]
            sess.run(training_operation, feed_dict={x: batch_x, y: batch_y}) # train the model on each batch

        # at the end of each epoch, we evaluate the model on our validation data
        validation_accuracy = evaluate(X_valid_normalized, y_valid)

        # also evaluate the model on training data to see if the model is over- or underfitted
        training_accuracy = evaluate(X_train_normalized, y_train)

        print("EPOCH {} ...".format(i+1))
        print("Accuracy on the validation set = {:.3f}".format(validation_accuracy))
        print("Accuracy on the training set = {:.3f}".format(training_accuracy))

        print()

    try:
        saver
    except NameError:
        saver = tf.train.Saver()

    saver.save(sess, 'lenet') # save the model to be able to load it up later and modify it or evaluate it on test dataset
    print("Model saved")

# ### Test Log
#
#  2021-02-25
#
# * Test 1 - 94.4 %
# preprocessing: shuffle, normalization
# model: original LeNet, batch size: 128, epochs: 10, rate: 0.001, mu: 0, sigma: 0.1
#
#
# * Test 2 - 90.3 %
# preprocessing: shuffle, normalization
# model: original LeNet plus added a dropout before final 3rd fully connected layer,
# batch size: 128, epochs: 10, rate: 0.001, mu: 0, sigma: 0.1
#
#
# * Test 3 - 89.0 %
# preprocessing: shuffle, normalization, added grayscale
# model: original LeNet plus added a dropout before final 3rd fully connected layer,
# batch size: 128, epochs: 10, rate: 0.001, mu: 0, sigma: 0.1
#
#
# * Test 4 - 98.2 %
# preprocessing: shuffle, normalization, grayscale
# model: original LeNet plus added a dropout before final 3rd fully connected layer,
# batch size: 128, epochs: 50, rate: 0.001, mu: 0, sigma: 0.1
# Notes: Changing the number of Epochs improved accuracy greatly, validation accuracy is only 78.3% (overfitting)
#
#
# * Test 5 - 98.3 %
# preprocessing: shuffle, normalization, grayscale
# model: original LeNet plus added a dropout before final 3rd fully connected layer,
# batch size: 128, epochs: 50, rate: 0.0005, mu: 0, sigma: 0.1
# Notes: Changing the learning rate did not improve accuracy, validation accuracy is only 74.5% (overfitting)
#
#
# * Test 6 - 98.3 %- Added Data Augmentation to add additional training images (rotated)
# preprocessing: shuffle, normalization, grayscale
# model: original LeNet plus added a dropout before final 3rd fully connected layer,
# batch size: 128, epochs: 50, rate: 0.001, mu: 0, sigma: 0.1
# Notes: Changing the learning rate did not improve accuracy, validation accuracy is only 82.5% (overfitting)
#
#
# * Test 7 - 98.0 % - Added Data Augmentation training images (rotated)
# preprocessing: shuffle, normalization, grayscale
# model: original LeNet plus added a dropout before final 3rd fully connected layer,
# batch size: 128, epochs: 50, rate: 0.001, mu: 0, sigma: 0.1 keep_prob: 0.5 for Dropout
# Notes: From Nitish Srivastava [paper](https://www.cs.toronto.edu/~hinton/absps/JMLRdropout.pdf) "*Dropout probability p independent of other units, where p can be chosen using a validation set or can simply be set at 0.5, which seems to be close to optimal for a wide range of networks and tasks."*
#
#
# * Test 8 - 99.5% !! Training Accuracy / 94.2 % Validation Accuracy !!!
# Added Data Augmentation training images (rotated)
# preprocessing: shuffle, normalization (changed normalization to -1 to 1), grayscale
# model: original LeNet plus added a dropout before final 3rd fully connected layer,
# batch size: 128, epochs: 50, rate: 0.001, mu: 0, sigma: 0.1 keep_prob: 0.5 for Dropout
# Notes: From Nitish Srivastava [paper](https://www.cs.toronto.edu/~hinton/absps/JMLRdropout.pdf) "<i>Dropout probability p independent of other units, where p can be chosen using a validation set or can simply be set at 0.5, which seems to be close to optimal for a wide range of networks and tasks."
#
#
# * Test 9 - 99.7% !! Training Accuracy / 94.8 % Validation Accuracy !!!
# Same as Test 8, reduced Batch size to 100.  Slight improvement.
#
#
# * Test 10 - 99.6% Training Accuracy / Validation Accuracy = 95.9
# Added histogram equalization to pre-processing.  Added Data Augmentation training images (rotated)
# preprocessing: shuffle, normalization (changed normalization to -1 to 1), grayscale
# model: original LeNet plus added a dropout before final 3rd fully connected layer,
# batch size: 128, epochs: 50, rate: 0.001, mu: 0, sigma: 0.1 keep_prob: 0.5 for Dropout
#
# * Test 11 - **99.5% Training Accuracy / Validation Accuracy = 95.8 %
# Added histogram equalization to pre-processing.  Added Data Augmentation training images (rotated)
# preprocessing: shuffle, normalization (changed normalization to -1 to 1), grayscale
# model: original LeNet plus added a dropout before final 3rd fully connected layer,
# batch size: 128, epochs: 40, rate: 0.001, mu: 0, sigma: 0.1 keep_prob: 0.5 for Dropout
#

# ### Test the Model
#
# Load our `LeNet` model saved from training and run on test data.

# In[16]:

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    saver = tf.train.import_meta_graph('./lenet.meta')
    saver.restore(sess, "./lenet")
    test_accuracy = evaluate(X_test_normalized, y_test)
    print("Accuracy on the Test set = {:.3f}".format(test_accuracy))

# ---
#
# ## Test a Model on New Images
#
# To test my model I downloaded (5) German road signs at random.  I wanted to try really unique photos, so I tried to find road signs that are toxic as I read about in the following paper [DARTS: Deceiving Autonomous Cars with Toxic Signs](https://arxiv.org/pdf/1802.06430.pdf).  I wanted to ensure the pictures had some graffiti, poor lighting, I also selected a few images that had a limited training data for that class.
#

# ### Load and Output the Images

# In[17]:

import numpy as np
import cv2
import glob
import os
from PIL import Image
import pandas as pd
import matplotlib.gridspec as gridspec

# Load images from .png files to` NumPy array named X_web
X_web = np.empty([0, 32, 32, 3], dtype = np.float64) # numpy.empty(shape, dtype); shape > int or tuple of int;

# loading the images into a file list (no numpy array! only a list)
web_file_list = [f for f in glob.glob("signs/" + '*.png')]

for i in range(1,6):
    image = cv2.imread('../CarND-Traffic-Sign-Classifier-Project/signs/example_' + str(i) + '.png')
    print("original image has shape: ", image.shape)
    img_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    image_resized = cv2.resize(img_rgb, (32, 32))
    print("resized image has shape:  ", image_resized.shape)
    cv2.imwrite('../CarND-Traffic-Sign-Classifier-Project/signs/cv2_'+ str(i) +'.png', image_resized)
    X_web = np.append(X_web, [image_resized[:, :, :3]], axis = 0)

# In[18]:

# Enter correct labels (groundtruth) for the images found on the web

y_web = np.array([
    14, # "example_1" Stop
    17, # "example_2" No entry
    4, # "example_3" Speed limit (70km/h)
    19, # "example_4" Dangerous curve to the left
    0, # "example_5" Speed limit (20km/h)
])

# In[19]:

# Plot original images from the web
fig = plt.figure()
fig.subplots_adjust(left = 0, right = 2, bottom = 0, top = 2, hspace = 0.05, wspace = 0.05)

for i in range(1,6):
    axis = fig.add_subplot(1, 6, i + 1, xticks=[], yticks=[])
    web_image = Image.open('../CarND-Traffic-Sign-Classifier-Project/signs/example_' + str(i) + '.png')
    axis.imshow(web_image)

# ## Pre-Process Data (Normalize & Grayscale Image)

# In[20]:

# Normalizing Images for zero mean and equal variance.
X_web = (X_web - 127.5)/255 #pixel range from 0 to 1

# Convert RGB image to grayscale
X_web = np.sum(X_web/3, axis=3, keepdims=True)

print("X_web[0].shape: ", X_web[0].shape)
print("X_web.shape: ", X_web.shape)

# ### Accuracy

# In[21]:

### Run the predictions here and use the model to output the prediction for each image.
### Make sure to pre-process the images with the same pre-processing pipeline used earlier.
### Feel free to use as many code cells as needed.
with tf.Session() as sess:
    saver = tf.train.import_meta_graph('lenet.meta')
    saver.restore(sess, tf.train.latest_checkpoint('./'))
    num_examples = len(X_web)
    BATCH_SIZE = 1
    web_accuracy = evaluate(X_web, y_web)
    print("Accuracy on images found on the web = {:.3f}".format(web_accuracy))

# ### Output Top 5 Softmax Probabilities For Each Image Found on the Web

# For each of the new images, print out the model's softmax probabilities to show the **certainty** of the model's predictions (limit the output to the top 5 probabilities for each image). [`tf.nn.top_k`](https://www.tensorflow.org/versions/r0.12/api_docs/python/nn.html#top_k) could prove helpful here.
#
# The example below demonstrates how tf.nn.top_k can be used to find the top k predictions for each image.
#
# `tf.nn.top_k` will return the values and indices (class ids) of the top k predictions. So if k=3, for each sign, it'll return the 3 largest probabilities (out of a possible 43) and the correspoding class ids.
#
# Take this numpy array as an example. The values in the array represent predictions. The array contains softmax probabilities for five candidate images with six possible classes. `tf.nn.top_k` is used to choose the three classes with the highest probability:
#
# ```
# # (5, 6) array
# a = np.array([[ 0.24879643,  0.07032244,  0.12641572,  0.34763842,  0.07893497,
#          0.12789202],
#        [ 0.28086119,  0.27569815,  0.08594638,  0.0178669 ,  0.18063401,
#          0.15899337],
#        [ 0.26076848,  0.23664738,  0.08020603,  0.07001922,  0.1134371 ,
#          0.23892179],
#        [ 0.11943333,  0.29198961,  0.02605103,  0.26234032,  0.1351348 ,
#          0.16505091],
#        [ 0.09561176,  0.34396535,  0.0643941 ,  0.16240774,  0.24206137,
#          0.09155967]])
# ```
#
# Running it through `sess.run(tf.nn.top_k(tf.constant(a), k=3))` produces:
#
# ```
# TopKV2(values=array([[ 0.34763842,  0.24879643,  0.12789202],
#        [ 0.28086119,  0.27569815,  0.18063401],
#        [ 0.26076848,  0.23892179,  0.23664738],
#        [ 0.29198961,  0.26234032,  0.16505091],
#        [ 0.34396535,  0.24206137,  0.16240774]]), indices=array([[3, 0, 5],
#        [0, 1, 4],
#        [0, 5, 1],
#        [1, 3, 5],
#        [1, 4, 3]], dtype=int32))
# ```
#
# Looking just at the first row we get `[ 0.34763842,  0.24879643,  0.12789202]`, you can confirm these are the 3 largest probabilities in `a`. You'll also notice `[3, 0, 5]` are the corresponding indices.

# In[22]:

### Print out the top five softmax probabilities for the predictions on the German traffic sign images found on the web.

top_5_probabilities = tf.nn.top_k(tf.nn.softmax(logits), k=5) # tf.nn.top_k(input, k=?)

keep_prob = 1.0

with tf.Session() as sess:
    saver = tf.train.import_meta_graph('lenet.meta')
    saver.restore(sess, tf.train.latest_checkpoint('./'))
    top_5_probabilities_output = sess.run(top_5_probabilities, feed_dict = {x:X_web, y:y_web})
    print(top_5_probabilities_output)

# ---
#
# ## Visualize the Neural Network's State with Test Images
#
#  This Section is not required to complete but acts as an additional excersise for understaning the output of a neural network's weights. While neural networks can be a great learning device they are often referred to as a black box. We can understand what the weights of a neural network look like better by plotting their feature maps. After successfully training your neural network you can see what it's feature maps look like by plotting the output of the network's weight layers in response to a test stimuli image. From these plotted feature maps, it's possible to see what characteristics of an image the network finds interesting. For a sign, maybe the inner network feature maps react with high activation to the sign's boundary outline or to the contrast in the sign's painted symbol.
#
#  Provided for you below is the function code that allows you to get the visualization output of any tensorflow weight layer you want. The inputs to the function should be a stimuli image, one used during training or a new one you provided, and then the tensorflow variable name that represents the layer's state during the training process, for instance if you wanted to see what the [LeNet lab's](https://classroom.udacity.com/nanodegrees/nd013/parts/fbf77062-5703-404e-b60c-95b78b2f3f9e/modules/6df7ae49-c61c-4bb2-a23e-6527e69209ec/lessons/601ae704-1035-4287-8b11-e2c2716217ad/concepts/d4aca031-508f-4e0b-b493-e7b706120f81) feature maps looked like for it's second convolutional layer you could enter conv2 as the tf_activation variable.
#
# For an example of what feature map outputs look like, check out NVIDIA's results in their paper [End-to-End Deep Learning for Self-Driving Cars](https://devblogs.nvidia.com/parallelforall/deep-learning-self-driving-cars/) in the section Visualization of internal CNN State. NVIDIA was able to show that their network's inner weights had high activations to road boundary lines by comparing feature maps from an image with a clear path to one without. Try experimenting with a similar test to show that your trained network's weights are looking for interesting features, whether it's looking at differences in feature maps from images with or without a sign, or even what feature maps look like in a trained network vs a completely untrained one on the same sign image.
#
# <figure>
#  <img src="visualize_cnn.png" width="380" alt="Combined Image" />
#  <figcaption>
#  <p></p>
#  <p style="text-align: center;"> Your output should look something like this (above)</p>
#  </figcaption>
# </figure>
#  <p></p>
#

# In[23]:

### Visualize your network's feature maps here.

# image_input: the test image being fed into the network to produce the feature maps
# tf_activation: should be a tf variable name used during your training procedure that represents the calculated state of a specific weight layer
# activation_min/max: can be used to view the activation contrast in more detail, by default matplot sets min and max to the actual min and max values of the output
# plt_num: used to plot out multiple different weight feature map sets on the same block, just extend the plt number for each new feature map entry

def outputFeatureMap(image_input, tf_activation, activation_min=-1, activation_max=-1 ,plt_num=1):
    # Here make sure to preprocess your image_input in a way your network expects
    # with size, normalization, ect if needed
    # image_input =
    # Note: x should be the same name as your network's tensorflow data placeholder variable
    # If you get an error tf_activation is not defined it may be having trouble accessing the variable from inside a function
    activation = tf_activation.eval(session=sess,feed_dict={x : image_input})
    featuremaps = activation.shape[3]
    plt.figure(plt_num, figsize=(15,15))
    for featuremap in range(featuremaps):
        plt.subplot(6,8, featuremap+1) # sets the number of feature maps to show on each row and column
        plt.title('FeatureMap ' + str(featuremap)) # displays the feature map number
        if activation_min != -1 & activation_max != -1:
            plt.imshow(activation[0,:,:, featuremap], interpolation="nearest", vmin =activation_min, vmax=activation_max, cmap="gray")
        elif activation_max != -1:
            plt.imshow(activation[0,:,:, featuremap], interpolation="nearest", vmax=activation_max, cmap="gray")
        elif activation_min !=-1:
            plt.imshow(activation[0,:,:, featuremap], interpolation="nearest", vmin=activation_min, cmap="gray")
        else:
            plt.imshow(activation[0,:,:, featuremap], interpolation="nearest", cmap="gray")

# In[24]:

#LeNet Model will only accept 32x32 images

img = np.zeros((1,32,32,3))

img[0,:,:,:] = np.array(Image.open('../CarND-Traffic-Sign-Classifier-Project/signs/example_2.png').resize((32,32)))

#normalize
image_input = (img - 127.5)/255

#grayscale

image_input = np.sum(img/3, axis=3, keepdims=True)

with tf.Session() as sess:
    new_saver = tf.train.import_meta_graph('lenet.meta')
    new_saver.restore(sess, tf.train.latest_checkpoint('.'))

    conv_1 = tf.get_default_graph().get_tensor_by_name("Conv2D_1:0")

    plt.imshow(np.squeeze(image_input),cmap='gray')
    plt.show()
    outputFeatureMap(image_input, conv_1)


"""
.. _tut-annotations:

The :term:`Events <events>` and :class:`~mne.Annotations` data structures
=========================================================================

:term:`Events <events>` and :term:`annotations` are quite similar.
This tutorial highlights their differences and similarities, and tries to shed
some light on which one is preferred to use in different situations when using
MNE.

Both events and :class:`~mne.Annotations` can be seen as triplets
where the first element answers to **when** something happens and the last
element refers to **what** it is.
The main difference is that events represent the onset in samples taking into
account the first sample value
(:attr:`raw.first_samp <mne.io.Raw.first_samp>`), and the description is
an integer value.
In contrast, :class:`~mne.Annotations` represents the
``onset`` in seconds (relative to the reference ``orig_time``),
and the ``description`` is an arbitrary string.
There is no correspondence between the second element of events and
:class:`~mne.Annotations`.
For events, the second element corresponds to the previous value on the
stimulus channel from which events are extracted. In practice, the second
element is therefore in most cases zero.
The second element of :class:`~mne.Annotations` is a float
indicating its duration in seconds.

See :ref:`ex-read-events`
for a complete example of how to read, select, and visualize **events**;
and :ref:`tut-artifact-rejection` to
learn how :class:`~mne.Annotations` are used to mark bad segments
of data.

An example of events and annotations
------------------------------------

The following example shows the recorded events in `sample_audvis_raw.fif` and
marks bad segments due to eye blinks.
"""

import os.path as op
import numpy as np

import mne

# Load the data
data_path = mne.datasets.sample.data_path()
fname = op.join(data_path, 'MEG', 'sample', 'sample_audvis_raw.fif')
raw = mne.io.read_raw_fif(fname)

###############################################################################
# First we'll create and plot events associated with the experimental paradigm:

# extract the events array from the stim channel
events = mne.find_events(raw)

# Specify event_id dictionary based on the meaning of experimental triggers
event_id = {'Auditory/Left': 1, 'Auditory/Right': 2,
            'Visual/Left': 3, 'Visual/Right': 4,
            'smiley': 5, 'button': 32}
color = {1: 'green', 2: 'yellow', 3: 'red', 4: 'c', 5: 'black', 32: 'blue'}

mne.viz.plot_events(events, raw.info['sfreq'], raw.first_samp, color=color,
                    event_id=event_id)

###############################################################################
# Next, we're going to detect eye blinks and turn them into
# :class:`~mne.Annotations`:

# find blinks
annotated_blink_raw = raw.copy()
eog_events = mne.preprocessing.find_eog_events(raw)
n_blinks = len(eog_events)

# Turn blink events into Annotations of 0.5 seconds duration,
# each centered on the blink event:
onset = eog_events[:, 0] / raw.info['sfreq'] - 0.25
duration = np.repeat(0.5, n_blinks)
description = ['bad blink'] * n_blinks
annot = mne.Annotations(onset, duration, description,
                        orig_time=raw.info['meas_date'])
annotated_blink_raw.set_annotations(annot)

# plot the annotated raw
annotated_blink_raw.plot()

###############################################################################
# Add :term:`annotations` to :term:`raw` objects
# ----------------------------------------------
#
# An important element of :class:`~mne.Annotations` is
# ``orig_time`` which is the time reference for the ``onset``.
# It is key to understand that when calling
# :func:`raw.set_annotations <mne.io.Raw.set_annotations>`, given
# annotations are copied and transformed so that
# :class:`raw.annotations.orig_time <mne.Annotations>`
# matches the recording time of the raw object.
# Refer to the documentation of :class:`~mne.Annotations` to see
# the expected behavior depending on ``meas_date`` and ``orig_time``.
# Where ``meas_date`` is the recording time stored in
# :class:`Info <mne.Info>`.
# You can find more information about :class:`Info <mne.Info>` in
# :ref:`tut-info-class`.
#
# We'll now manipulate some simulated annotations.
# The first annotations has ``orig_time`` set to ``None`` while the
# second is set to a chosen POSIX timestamp for illustration purposes.
# Note that both annotations have different ``onset`` values.

###############################################################################

# Create an annotation object with orig_time undefined (default)
annot_none = mne.Annotations(onset=[0, 2, 9], duration=[0.5, 4, 0],
                             description=['foo', 'bar', 'foo'],
                             orig_time=None)
print(annot_none)

# Create an annotation object with orig_time
orig_time = '2002-12-03 19:01:31.676071'
annot_orig = mne.Annotations(onset=[22, 24, 31], duration=[0.5, 4, 0],
                             description=['foo', 'bar', 'foo'],
                             orig_time=orig_time)
print(annot_orig)

###############################################################################
# Now we create two raw objects and set each with different annotations.
# Then we plot both raw objects to compare the annotations.

# Create two cropped copies of raw with the two previous annotations
raw_a = raw.copy().crop(tmax=12).set_annotations(annot_none)
raw_b = raw.copy().crop(tmax=12).set_annotations(annot_orig)

# Plot the raw objects
raw_a.plot()
raw_b.plot()

###############################################################################
# Note that although the ``onset`` values of both annotations were different,
# due to complementary ``orig_time`` they are now identical. This is because
# the first one (``annot_none``), once set in raw, adopted its ``orig_time``.
# The second one (``annot_orig``) already had an ``orig_time``, so its
# ``orig_time`` was changed to match the onset time of the raw. Changing an
# already defined ``orig_time`` of annotations caused its ``onset`` to be
# recalibrated with respect to the new ``orig_time``. As a result both
# annotations have now identical ``onset`` and identical ``orig_time``:

# Show the annotations in the raw objects
print(raw_a.annotations)
print(raw_b.annotations)

# Show that the onsets are the same
np.set_printoptions(precision=6)
print(raw_a.annotations.onset)
print(raw_b.annotations.onset)

###############################################################################
# Notice again that for the case where ``orig_time`` is ``None``,
# it is assumed that the ``orig_time`` is the time of the first sample of data.

raw_delta = (1 / raw.info['sfreq'])
print('raw.first_sample is {}'.format(raw.first_samp * raw_delta))
print('annot_none.onset[0] is {}'.format(annot_none.onset[0]))
print('raw_a.annotations.onset[0] is {}'.format(raw_a.annotations.onset[0]))

###############################################################################
# Valid operations in :class:`mne.Annotations`
# --------------------------------------------
#
# Concatenate
# ~~~~~~~~~~~
#
# It is possible to concatenate two annotations with the + operator (just like
# lists) if both share the same ``orig_time``

annot = mne.Annotations(onset=[10], duration=[0.5],
                        description=['foobar'],
                        orig_time=orig_time)
annot = annot_orig + annot  # concatenation
print(annot)

###############################################################################
# Iterating, Indexing and Slicing :class:`mne.Annotations`
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
# :class:`~mne.Annotations` supports iterating, indexing and slicing.
# Iterating over :class:`~mne.Annotations` and indexing with an integer returns
# a dictionary. While slicing returns a new :class:`~mne.Annotations` instance.
#
# See the following examples and usages:

# difference between indexing and slicing a single element
print(annot[0])  # indexing
print(annot[:1])  # slicing

###############################################################################
# How about iterations?

for key, val in annot[0].items():  # iterate on one element which is dictionary
    print(key, val)

###############################################################################

for idx, my_annot in enumerate(annot):  # iterate on the Annotations object
    print('annot #{0}: onset={1}'.format(idx, my_annot['onset']))
    print('annot #{0}: duration={1}'.format(idx, my_annot['duration']))
    print('annot #{0}: description={1}'.format(idx, my_annot['description']))

###############################################################################

for idx, my_annot in enumerate(annot[:1]):
    for key, val in my_annot.items():
        print('annot #{0}: {1} = {2}'.format(idx, key, val))

###############################################################################
# Iterating, indexing and slicing return a copy. This has implications like the
# fact that changes are not kept.

# this change is not kept
annot[0]['onset'] = 42
print(annot[0])

# this change is kept
annot.onset[0] = 42
print(annot[0])

###############################################################################
# Save
# ~~~~
#
# Note that you can also save annotations to disk in FIF format::
#
#     >>> annot.save('my-annot.fif')
#
# Or as CSV with onsets in (absolute) ISO timestamps::
#
#     >>> annot.save('my-annot.csv')
#
# Or in plain text with onsets relative to ``orig_time``::
#
#     >>> annot.save('my-annot.txt')
#


"""
Tests the methods and functions in the ``galaxy`` module.

Implemented Tests
-----------------

Ensure that the class can be called!

Generate some data using a pre-determined seed and check the results match the expected output.

Your Tasks
----------

Test the ``write_galaxies`` option for ``generate_random_data``. Read this data back in
and ensure that it is correct.  Should this data be kept on disk after the test? Should
the test delete it?

Generate some random galaxies and properties and write them both to file.
In the test proper, read in the galaxies, execute the same functions, then check if the
results match the answer written to file.

Generate a random set of galaxies and check their output. How do you handle random numbers
in testing scenarios?

What happens when there are zero galaxies in the region passed to
``mass_within_region()``? How should we handle this case? Is there a "correct" answer?

Extend the module to handle 3 spatial dimensions. How would you update the tests to
account for this? BE CAREFUL! At every step, we want to ensure that our tests are still
passing.

Author: Jacob Seiler
"""

from example_scripts import galaxy

import pytest

def test_gal_class():
    """
    In this test, we just want to ensure that the Galaxy class can be instantiated. Easiest
    test ever but still could be useful!
    """

    gal = galaxy.Galaxy(0.5, -21.5, -20)

    assert(gal.x == 0.5)
    assert(gal.y == -21.5)
    #assert(gal.z == 142.4)
    assert(gal.mass == -20)

@pytest.mark.parametrize(
        "x_bound, y_bound, expected_mass, expected_N, seed",
        [([0,50.0], [23.0, 28.0], 17.55854294814121, 29, 777)]
        )
def test_mass_in_region(x_bound, y_bound, expected_mass, expected_N, seed):
    """
    Check that if we generate galaxies using a specific random seed, then the statistics
    match the expected values.

    Can you extend the ``parameterize`` decorator to test a few other combinations?

    Can you implement a similar test that reads previously generated galaxies, calculates
    some statistics, then compares the answer to the "correct" answer?

    In some instances, we may WANT a test to fail to check certain conditions. In these
    instances, we want the individual test to fail, but for the overall pytest to pass.
    Search the ``pytest.mark.parametrize`` docs for the correct way to do this. Can you
    choose some parameters to get this scenario to work? When would this be useful?

    Parameters
    ----------

    x_bound, y_bound: [float, float]
        The minimum and maximum bounds that define the region we're summing/averaging
        inside.

    expected_mass: float
        The mass inside the region specified by
        (x_bound[0], y_bound[0]), (x_bound[1], y_bound[1]).

    expected_N : int
        The number of galaxies inside the region specified by
        (x_bound[0], y_bound[0]), (x_bound[1], y_bound[1]).

    seed: int
        Seed used to initialize the state of the random generator. If ``None``, will use
        the system clock as defined by ``numpy.random.seed()``.
    """

    import numpy as np

    # First generate some random galaxies.
    gals = galaxy.generate_random_data(seed=seed)

    # If the seed is 777, then we know what the output should be.
    if seed == 777:

        mass_in_region, N_in_region = galaxy.mass_within_region(gals, x_bound, y_bound)

        # Because 'mass_in_region' is a floating point, don't want to test pure equality.
        test_result = np.allclose(mass_in_region, expected_mass)
        assert(test_result)

        assert(N_in_region == expected_N)

    # Can you implement a solution to handle truly random galaxies?
    else:

        # Is this enough for a debug message...?
        print("Tests for testing seeds other than 777 not implemented.")
        assert False


import torch
import cupy as cp
import math

from .CustomKernel import CustomKernel
from ..util import get_absolute_path

class IVFPQTopkCuda(CustomKernel):
  def __init__(
      self,
      m=8,
      k=256,
      tpb=256,
      n_cs=4,
      stack_capacity=4,
      sm_size=48*256*4,
    ):
    super(IVFPQTopkCuda, self).__init__()
    assert tpb >= 32
    assert tpb == self.next_power_of_2(tpb)
    assert k == 256
    assert m * 1024 <= sm_size
    assert m % n_cs == 0
    assert stack_capacity >= 2
    self.m = m
    self.k = k
    self.tpb=tpb
    self.n_cs = n_cs
    self.sm_size = sm_size
    self.stack_capacity = stack_capacity

    with open(get_absolute_path("kernels","cuda","ivfpq_topk.cu"), "r") as f:
      self.code = f.read()
    varnames = ", ".join([f"d{i}" for i in range(n_cs)])
    code = (self.code
      .replace("_VARNAMES_", varnames)
      .replace("_M_", str(m))
      .replace("_K_", str(k))
      .replace("_TPB_", str(self.tpb))
      .replace("_NCS_", str(n_cs))
      .replace("_STACKCAP_", str(stack_capacity))
    )

    self._topk_fn = cp.RawKernel(
      code = code,
      name = 'ivfpq_topk',
      options = (
        '--maxrregcount=255',
        '--use_fast_math'
      ),
      backend='nvrtc',
    )
    self._topk_fn.max_dynamic_shared_size_bytes = sm_size

    self._topk_residual_fn = cp.RawKernel(
      code = code,
      name = 'ivfpq_topk_residual',
      options = (
        '--maxrregcount=255',
        '--use_fast_math'
      ),
      backend='nvrtc',
    )
    self._topk_residual_fn.max_dynamic_shared_size_bytes = sm_size

    self._topk_residual_precomputed_fn = cp.RawKernel(
      code = code,
      name = 'ivfpq_topk_residual_precomputed',
      options = (
        '--maxrregcount=255',
        '--use_fast_math'
      ),
      backend='nvrtc',
    )
    self._topk_residual_precomputed_fn.max_dynamic_shared_size_bytes = sm_size

  @staticmethod
  def next_power_of_2(x):
    return 1 if x == 0 else 2**math.ceil(math.log2(x))

  def topk(
      self, data, precomputed,
      is_empty, cell_start, cell_size,
      n_probe_list, n_candidates=None
    ):
    """
      data: shape=[n_subvectors // n_cs, n_data, n_cs], dtype=uint8
      precomputed: shape=[n_query, n_clusters], dtype=float32
      is_empty: shape=[n_data], dtype=uint8
      cell_start: shape=[n_query, max_n_probe], dtype=int64
      cell_size: shape=[n_query, max_n_probe], dtype=int64
      n_probe_list: shape=[n_query], dtype=int64
      n_candidates: int, `k` in topk
    """
    n_data = data.shape[1]
    n_query = cell_start.shape[0]
    n_probe = cell_start.shape[1]
    assert precomputed.shape == (self.m, n_query, self.k)
    assert data.shape[0] == self.m // self.n_cs
    assert data.shape[2] == self.n_cs
    assert is_empty.shape[0] == n_data
    assert cell_size.shape[1] == n_probe
    assert data.dtype == torch.uint8
    assert precomputed.dtype == torch.float32
    assert cell_start.dtype == cell_size.dtype == torch.int64
    assert is_empty.dtype == torch.uint8
    assert n_probe_list.shape == (n_query, )
    assert n_probe_list.dtype == torch.int64
    if n_candidates is None:
      n_candidates = self.tpb
    else:
      assert n_candidates <= self.tpb
    n_candidates_pow_of_2 = 2 * self.next_power_of_2(math.ceil(n_candidates / 2))
    assert n_candidates_pow_of_2 in [2 * 2**i for i in range(10)]

    tot_size = cell_size.sum(dim=1)
    values = torch.empty(n_query, n_candidates_pow_of_2, device="cuda:0", dtype=torch.float32)
    values.fill_(float("-inf"))
    indices = torch.zeros(n_query, n_candidates_pow_of_2, device="cuda:0", dtype=torch.int64)
    threads_per_block = (self.tpb,)
    blocks_per_grid = (n_query,)

    self._topk_fn(
      grid=blocks_per_grid,
      block=threads_per_block,
      shared_mem = self.sm_size,
      args=[
        data.data_ptr(),
        precomputed.data_ptr(),
        is_empty.data_ptr(),
        cell_start.data_ptr(),
        cell_size.data_ptr(),
        tot_size.data_ptr(),
        n_probe_list.data_ptr(),
        values.data_ptr(),
        indices.data_ptr(),
        n_data, n_query, n_probe, n_candidates_pow_of_2
        ],
      stream=self.stream
    )
    return (values[:, :n_candidates], indices[:, :n_candidates])

  def topk_residual(
      self, data, precomputed,
      is_empty, cell_start, cell_size,
      base_sims, n_probe_list, n_candidates=None
    ):
    """
      data: shape=[n_subvectors // n_cs, n_data, n_cs], dtype=uint8
      precomputed: shape=[n_query, max_n_probe, n_subvectors, n_clusters], dtype=float32
      is_empty: shape=[n_data], dtype=uint8
      cell_start: shape=[n_query, max_n_probe], dtype=int64
      cell_size: shape=[n_query, max_n_probe], dtype=int64
      base_sims: shape=[n_query, max_n_probe], dtype=float32
      n_probe_list: shape=[n_query], dtype=int64
      n_candidates: int, `k` in topk
    """
    n_data = data.shape[1]
    n_query = cell_start.shape[0]
    n_probe = cell_start.shape[1]
    assert precomputed.shape == (n_query, n_probe, self.m, self.k)
    assert data.shape == (self.m // self.n_cs, n_data, self.n_cs)
    assert is_empty.shape == (n_data,)
    assert cell_size.shape == (n_query, n_probe)
    assert base_sims.shape == (n_query, n_probe)
    assert data.dtype == torch.uint8
    assert precomputed.dtype == torch.float32
    assert cell_start.dtype == cell_size.dtype == torch.int64
    assert is_empty.dtype == torch.uint8
    assert base_sims.dtype == torch.float32
    assert n_probe_list.shape == (n_query, )
    assert n_probe_list.dtype == torch.int64
    precomputed = precomputed.contiguous()
    base_sims = base_sims.contiguous()
    if n_candidates is None:
      n_candidates = self.tpb
    else:
      assert n_candidates <= self.tpb
    n_candidates_pow_of_2 = 2 * self.next_power_of_2(math.ceil(n_candidates / 2))
    assert n_candidates_pow_of_2 in [2 * 2**i for i in range(10)]

    tot_size = cell_size.sum(dim=1)
    values = torch.empty(n_query, n_candidates_pow_of_2, device="cuda:0", dtype=torch.float32)
    values.fill_(float("-inf"))
    indices = torch.zeros(n_query, n_candidates_pow_of_2, device="cuda:0", dtype=torch.int64)
    threads_per_block = (self.tpb,)
    blocks_per_grid = (n_query,)

    self._topk_residual_fn(
      grid=blocks_per_grid,
      block=threads_per_block,
      shared_mem = self.sm_size,
      args=[
        data.data_ptr(),
        precomputed.data_ptr(),
        base_sims.data_ptr(),
        is_empty.data_ptr(),
        cell_start.data_ptr(),
        cell_size.data_ptr(),
        tot_size.data_ptr(),
        n_probe_list.data_ptr(),
        values.data_ptr(),
        indices.data_ptr(),
        n_data, n_query, n_probe, n_candidates_pow_of_2
        ],
      stream=self.stream
    )
    return (values[:, :n_candidates], indices[:, :n_candidates])

  def topk_residual_precomputed(
      self, data, part1, part2, cells, base_sims,
      is_empty, cell_start, cell_size,
      n_probe_list, n_candidates=None
    ):
    """
      data: shape=[n_subvectors // n_cs, n_data, n_cs], dtype=uint8
      part1: shape=[n_query, n_subvectors, n_pq_clusters], dtype=float32
      part2: shape=[n_cells, n_subvectors, n_pq_clusters], dtype=float32
      cells: shape=[n_query, max_n_probe], dtype=int64
      base_sims: shape=[n_query, max_n_probe], dtype=float32
      is_empty: shape=[n_data], dtype=uint8
      cell_start: shape=[n_query, max_n_probe], dtype=int64
      cell_size: shape=[n_query, max_n_probe], dtype=int64
      n_probe_list: shape=[n_query], dtype=int64
      n_candidates: int, `k` in topk
    """
    n_data = data.shape[1]
    n_query = cell_start.shape[0]
    n_probe = cell_start.shape[1]
    assert data.shape == (self.m // self.n_cs, n_data, self.n_cs)
    assert is_empty.shape == (n_data,)
    assert cell_size.shape == (n_query, n_probe)
    assert base_sims.shape == (n_query, n_probe)
    assert data.dtype == torch.uint8
    assert part1.dtype == part2.dtype == torch.float32
    assert cell_start.dtype == cell_size.dtype == torch.int64
    assert is_empty.dtype == torch.uint8
    assert base_sims.dtype == torch.float32
    assert n_probe_list.shape == (n_query, )
    assert n_probe_list.dtype == torch.int64
    part1 = part1.contiguous()
    part2 = part2.contiguous()
    cells = cells.contiguous()
    base_sims = base_sims.contiguous()
    if n_candidates is None:
      n_candidates = self.tpb
    else:
      assert n_candidates <= self.tpb
    n_candidates_pow_of_2 = 2 * self.next_power_of_2(math.ceil(n_candidates / 2))
    assert n_candidates_pow_of_2 in [2 * 2**i for i in range(10)]

    tot_size = cell_size.sum(dim=1)
    values = torch.empty(n_query, n_candidates_pow_of_2, device="cuda:0", dtype=torch.float32)
    values.fill_(float("-inf"))
    indices = torch.zeros(n_query, n_candidates_pow_of_2, device="cuda:0", dtype=torch.int64)
    threads_per_block = (self.tpb,)
    blocks_per_grid = (n_query,)

    self._topk_residual_precomputed_fn(
      grid=blocks_per_grid,
      block=threads_per_block,
      shared_mem = self.sm_size,
      args=[
        data.data_ptr(),
        part1.data_ptr(),
        part2.data_ptr(),
        cells.data_ptr(),
        base_sims.data_ptr(),
        is_empty.data_ptr(),
        cell_start.data_ptr(),
        cell_size.data_ptr(),
        tot_size.data_ptr(),
        n_probe_list.data_ptr(),
        values.data_ptr(),
        indices.data_ptr(),
        n_data, n_query, n_probe, n_candidates_pow_of_2
        ],
      stream=self.stream
    )
    return (values[:, :n_candidates], indices[:, :n_candidates])


import os
import tqdm
import math
import time
import argparse
import numpy as np
import multiprocessing
import tensorflow as tf
import _pickle as cPickle
from itertools import repeat
from multiprocessing import Pool
from src.official.transformer.v2 import optgen_v8, optgen_v9, optgen_v11, optgen_v12, optgen_v13, optgen_v21
from src.score_result import penalized_logp, qed, drd2, similarity
from src.official.transformer.utils.molecule_tokenizer import Moltokenizer
from src.official.transformer.v2 import optgen_v1, optgen_v2, optgen_v3, optgen_v4, optgen_v5, optgen_v6, optgen_v7

__author__ = 'Bonggun Shin'

class Timer(object):
    def __init__(self, name=None):
        self.name = name

    def __enter__(self):
        self.tstart = time.time()

    def __exit__(self, type, value, traceback):
        if self.name:
            print('[%s]' % self.name)
        print('Elapsed: %s' % (time.time() - self.tstart))

def static_var(varname, value):
    def decorate(func):
        setattr(func, varname, value)
        return func
    return decorate

@static_var("model", None)
def get_dev_model(params):
    if get_dev_model.model is None:
        print("dev_model is creating!!!")
        model_name = "optgen_%s" % params["model_version"]
        get_dev_model.model = eval(model_name).create_model(params, is_train=False)
        print("=========================== %s dev created!! ==========================" % model_name)

    else:
        print("dev_model is reused!!!")

    return get_dev_model.model

@static_var("model", None)
def get_trn_model(params):
    if get_trn_model.model is None:
        print("trn_model is creating!!!")
        model_name = "optgen_%s" % params["model_version"]
        get_trn_model.model = eval(model_name).create_model(params, is_train=True)
        print("=========================== %s trn created!! ==========================" % model_name)

    else:
        print("trn_model is reused!!!")

    return get_trn_model.model

def get_model(params, epoch_num):

    trn_model = get_trn_model(params)
    model_filename = "%s/%s" % (params["model_dir"], "cp-%04d.ckpt" % (epoch_num))
    print("Load weights: {}".format(model_filename))
    trn_model.load_weights(model_filename).expect_partial()

    dev_model = get_dev_model(params)
    dev_model.summary()

    # model.layers[12].set_weights(trn_model.layers[4].get_weights())
    model_name = "optgen_%s" % params["model_version"]
    trn_optgen_layer_index = get_layer_index(trn_model, name=model_name)
    dev_optgen_layer_index = get_layer_index(dev_model, name=model_name)
    dev_model.layers[dev_optgen_layer_index].set_weights(trn_model.layers[trn_optgen_layer_index].get_weights())
    print("trn_optgen_layer_index(%d, %s), dev_optgen_layer_index(%d, %s)" %
          (trn_optgen_layer_index, trn_model.layers[trn_optgen_layer_index].name,
           dev_optgen_layer_index, dev_model.layers[dev_optgen_layer_index].name))
    print("set weights for (%s) layer" % dev_model.layers[dev_optgen_layer_index].name)

    if params["use_propnet"] == 1:
        print("========================== Transferring PropNET weights....==========================")
        transfer_weights_from_propnet(dev_model, params)
    if params["use_simnet"] == 1:
        print("========================== Transferring SimNET weights....==========================")
        transfer_weights_from_simnet(dev_model, params)

    return dev_model

def get_layer_index(model, name=None, custom_name=None):
    for idx, l in enumerate(model.layers):
        if name=="embedding" and "embedding_freezable" in l.name:
            if custom_name==l.custom_name:
                return idx
        else:
            if name==l.name:
                return idx

    print("wrong name", name)
    exit()
    return -1

def transfer_weights_from_propnet(model, params):
    propnet_weight_path = params["propnet_weight_path"]
    print(propnet_weight_path)
    propnet_weights = cPickle.load(open(propnet_weight_path, 'rb'))

    model_layers = [l for l in model.layers]
    # model_layers[63]  # Emb 42
    # model_layers[64]  # Bidirectional 43
    # model_layers[65]  # idense
    # model_layers[66]  # Dense2 44
    layer_index = get_layer_index(model, name='embedding', custom_name='propnet_emb')
    print("setting propnet weights: %s..." % model_layers[layer_index].name)
    model_layers[layer_index].set_weights([propnet_weights["embedding_shared_weights"]])

    layer_index = get_layer_index(model, name='propnet_bidirectional')
    print("setting propnet weights: %s..." % model_layers[layer_index].name)
    lstm_weights = []
    lstm_weights.append(propnet_weights["forward_prop_lstm_0"])
    lstm_weights.append(propnet_weights["forward_prop_lstm_1"])
    lstm_weights.append(propnet_weights["forward_prop_lstm_2"])
    lstm_weights.append(propnet_weights["backward_prop_lstm_0"])
    lstm_weights.append(propnet_weights["backward_prop_lstm_1"])
    lstm_weights.append(propnet_weights["backward_prop_lstm_2"])
    model_layers[layer_index].set_weights(lstm_weights)

    layer_index = get_layer_index(model, name='propnet_idense')
    output_weights = []
    output_weights.append(propnet_weights["idense0"])
    output_weights.append(propnet_weights["idense1"])
    print("setting propnet weights: %s..." % model_layers[layer_index].name)
    model_layers[layer_index].set_weights(output_weights)

    layer_index = get_layer_index(model, name='propnet_output')
    output_weights = []
    output_weights.append(propnet_weights["output0"])
    output_weights.append(propnet_weights["output1"])
    print("setting propnet weights: %s..." % model_layers[layer_index].name)
    model_layers[layer_index].set_weights(output_weights)

    print("setting propnet weights done!!!")

def transfer_weights_from_simnet(model, params):
    simnet_weight_path = params["simnet_weight_path"]
    print(simnet_weight_path)
    simnet_weights = cPickle.load(open(simnet_weight_path, 'rb'))
    model_layers = [l for l in model.layers]
    # model_layers[63]  # Emb 42
    # model_layers[64]  # Bidirectional 43
    # model_layers[65]  # idense
    # model_layers[66]  # Dense2 44
    layer_index = get_layer_index(model, name='embedding', custom_name='simnet_emb')
    print("setting simnet weights: %s..." % model_layers[layer_index].name)
    model_layers[layer_index].set_weights([simnet_weights["embedding_shared_weights"]])

    layer_index = get_layer_index(model, name='simnet_bidirectional')
    print("setting simnet weights: %s..." % model_layers[layer_index].name)
    lstm_weights = []
    lstm_weights.append(simnet_weights["forward_sim_lstm_0"])
    lstm_weights.append(simnet_weights["forward_sim_lstm_1"])
    lstm_weights.append(simnet_weights["forward_sim_lstm_2"])
    lstm_weights.append(simnet_weights["backward_sim_lstm_0"])
    lstm_weights.append(simnet_weights["backward_sim_lstm_1"])
    lstm_weights.append(simnet_weights["backward_sim_lstm_2"])
    model_layers[layer_index].set_weights(lstm_weights)

    layer_index = get_layer_index(model, name='simnet_idense')
    output_weights = []
    output_weights.append(simnet_weights["simdense0"])
    output_weights.append(simnet_weights["simdense1"])
    print("setting simnet weights: %s..." % model_layers[layer_index].name)
    model_layers[layer_index].set_weights(output_weights)

    layer_index = get_layer_index(model, name='simnet_output')
    output_weights = []
    output_weights.append(simnet_weights["simoutput0"])
    output_weights.append(simnet_weights["simoutput1"])
    print("setting simnet weights: %s..." % model_layers[layer_index].name)
    model_layers[layer_index].set_weights(output_weights)

    print("setting simnet weights done!!!")

def get_score(src, trg, task='logp04'):
    sim = similarity(src, trg)
    if sim >= 1.0:
        return 0
    if task=='logp04':
        logp_improvement = penalized_logp(trg) - penalized_logp(src)
        # return logp_improvement * (0.6+min(sim, 0.4))
        if sim<0.4 and sim>=0:
            return 0
        else:
            return logp_improvement

    elif task=='qed':
        if sim < 0.4:
            return 0
        else:
            return qed(trg)

    elif task=='drd2':
        if sim < 0.4:
            return 0
        else:
            try:
                val = drd2(trg)
            except:
                print("***************\n***************\n***************\n***************\n")
                print("***************\n***************\n***************\n***************\n")
                print(trg)
                print("***************\n***************\n***************\n***************\n")
                print("***************\n***************\n***************\n***************\n")
                val = 0
            return val

    else:
        assert 'wrong task: %s' % task

def get_test_smiles(base_path, test_name, is_dev):
    file_name = '%s/v10.all_kinds_of_smiles.cpkl' % base_path
    (names, smiles_dataset) = cPickle.load(open(file_name, 'rb'))
    trn_smiles, dev_logp_smiles, dev_qed_smiles, dev_drd2_smiles, dev_all_smiles, tst_logp_smiles, tst_qed_smiles, \
    tst_drd2_smiles, tst_all_smiles, all_smiles = smiles_dataset

    if is_dev==1:
        if test_name == 'logp04':
            test_smiles = dev_logp_smiles

        elif test_name == 'qed':
            test_smiles = dev_qed_smiles

        elif test_name == 'drd2':
            test_smiles = dev_drd2_smiles

    else:
        if test_name == 'logp04':
            test_smiles = tst_logp_smiles

        elif test_name == 'qed':
            test_smiles = tst_qed_smiles

        elif test_name == 'drd2':
            test_smiles = tst_drd2_smiles

    file_name = '%s/v10.property_for_all_smiles.cpkl' % base_path
    property_dic = cPickle.load(open(file_name, 'rb'))

    return test_smiles, property_dic

def worker_wrapper(args):
    return worker(*args)

# def worker(return_dict, outputs, smiles_list, n, wid, start_index, end_index):
def worker(outputs, smiles_list, n, wid, start_index, end_index):
    """
    Args:
      return_dic: store result here
      n: int, number of data
      wid: int, worker id
      strat_index: int, the start index of original data that this worker will work on
      end_index: int, the end index of original data that this worker will work on
    """
    print('[worker-%d] start_index(%d), end_index(%d) n(%d)' %
          (wid, start_index, end_index, n))

    if end_index > n:
        end_index = n

    score_all = []
    for idx in range(start_index, end_index, 1):
        output = outputs[idx]
        smiles_x = smiles_list[idx]
        smiles_y = moltokenizer.decode(output)
        score = get_score(smiles_x, smiles_y, task=args.test)
        score_all.append(score)

    # return_dict[wid] = score_all

    # print('calculation done for worker %d' % wid)

    return score_all

def evaluate(test_smiles_list, property_dic, model, moltokenizer):
    # test_smiles_list = list(test_smiles.keys())
    # test_smiles_list = test_smiles_list[:100]
    smiles_list = []
    ids_list = []
    property_x_list = []
    property_desired_list = []

    if test_name == 'logp04':
        logp_list = np.array(range(5)) * .5 - 1.0  # -1.0 -0.5 0.0 0.5 1.0
        qed_list = [0.1, 0.6]
        drd2_list = [0.52, 0.8]

    elif test_name == 'qed':
        logp_list = np.array(range(5)) * 1.0 - 2  # -8, -4 0 4 8
        qed_list = [0.91, 0.98]
        drd2_list = [0.2, 0.5]

    else: # drd2
        logp_list = np.array(range(5)) * 0.5 - 1
        qed_list = [0.1, 0.6]
        drd2_list = [0.52, 0.8]

    for idx, smiles in enumerate(test_smiles_list):
        # smiles = test_smiles_list[i]
        item = property_dic[smiles]
        ids = moltokenizer.encode(smiles)
        # ids = item['ids']
        logp_val = item['logP']
        qed_val = item['qed']
        drd2_val = item['drd2']

        property_x = np.expand_dims(np.array([logp_val, qed_val, drd2_val]), axis=0)

        for logp_improvement in logp_list:
            for qed_desired in qed_list:
                for drd2_desired in drd2_list:
                    property_desired = np.expand_dims(
                        np.array([logp_val + logp_improvement, qed_desired, drd2_desired]), axis=0)

                    smiles_list.append(smiles)
                    ids_list.append(ids)
                    property_x_list.append(property_x)
                    property_desired_list.append(property_desired)

    x = np.array(tf.keras.preprocessing.sequence.pad_sequences(ids_list, dtype="int64", padding="post"))
    px = np.concatenate(property_x_list, axis=0)
    py = np.concatenate(property_desired_list, axis=0)

    outputs, scores = model.predict([x, px, py], batch_size=args.eb, verbose=1)
    # scores = np.array(scores)
    print("prediction score report")
    print("min(%f), max(%f), mean(%f), std(%f), median(%f)" % (min(scores), max(scores), np.mean(scores), np.std(scores), np.median(scores) ))

    if args.p==1:
        score_all = []
        for idx, output in tqdm.tqdm(enumerate(outputs)):
            smiles_x = smiles_list[idx]
            smiles_y = moltokenizer.decode(output)
            score = get_score(smiles_x, smiles_y, task=args.test)
            score_all.append(score)

    else:
        with Timer("score multi calculation..."):
            n = len(outputs)
            n_proc = args.p
            batch = math.ceil(n / (n_proc))

            with Pool(processes=n_proc) as pool:
                r = pool.map_async(worker_wrapper,
                                   zip(repeat(outputs), repeat(smiles_list),
                                       repeat(n), range(1, n_proc + 1), range(0, n, batch),
                                       range(batch, batch * n_proc + 1, batch))
                                   )
                r.wait()

            score_all = []
            for partial_score in r.get():
                score_all+=partial_score

    score_final = []
    for i in range(len(test_smiles_list)):
        score = max(score_all[20*(i):20*(i+1)])
        score_final.append(score)

    return score_final

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument('-base', type=str, default="../data", help='base path.')
    parser.add_argument('-test', type=str, default='drd2', help='the name of testset') # qed, drd2, lop04

    parser.add_argument('-g', default="0", choices=["0", "1", "2", "3", "4", "5", "6", "7"], type=str)
    parser.add_argument('-lr', default="2", type=str) # 1e-3
    parser.add_argument('-he', default=8, type=int, help='num_heads')
    parser.add_argument('-hi', default=4, type=int, help='num_hidden_layers')
    parser.add_argument('-f', default=256, type=int, help='filter_size')
    parser.add_argument('-b', default=4096, type=int, help='batch_size') # 4096, 2048
    parser.add_argument('-v', default=21, type=int, help='model version')
    parser.add_argument('-t', default=0, type=int, help='attempt')
    parser.add_argument('-es', default=110, type=int, help='eval epoch start')
    parser.add_argument('-ee', default=120, type=int, help='eval epoch end')
    parser.add_argument('-et', default=10, type=int, choices=[1, 5, 10, 20], help='eval epoch step')
    parser.add_argument('-pnet', default=1, type=int, help='if propnet used')
    parser.add_argument('-snet', default=1, type=int, help='if simnet used')
    parser.add_argument('-bf', default=1, type=int, help='if use_beam_filter')
    parser.add_argument('-p', default=8, type=int, help='number of processes')
    parser.add_argument('-eb', default=1000, type=int, help='evaluate batch')

    args, unparsed = parser.parse_known_args()

    os.environ["CUDA_VISIBLE_DEVICES"] = args.g
    gpus = tf.config.experimental.list_physical_devices('GPU')
    tf.config.experimental.set_memory_growth(gpus[0], True)

    model_version = "v%d" % args.v

    base_path = args.base

    n_samples = 10827615

    data_dir = "%s/v10_trn" % base_path

    iterations = 100
    steps_between_evals = n_samples // args.b
    train_steps = steps_between_evals * iterations
    validation_steps = 32
    batch_size = args.b

    model_dir = "%s/model.vv%d.vnet%d.pnet%d.snet%d.head%d.hid%d.fil%d.batch%d.lr%s.t%d" % (base_path, args.v,
                                                                                args.vnet, args.pnet,
                                                                                args.snet, args.he, args.hi,
                                                                                args.f, args.b, args.lr,
                                                                                args.t)

    propnet_weight_path = "%s/v30.propnet.weights.cpkl" % base_path
    simnet_weight_path = "%s/v30.simnet.weights.cpkl" % base_path

    test_name = args.test

    mt_weight_path = "%s/mt_weights.cpkl" % base_path

    optgen_config_file = "%s/../config/optgen_config.json" % base_path

    params = optgen_v2.load_config(optgen_config_file)
    params["propnet_weight_path"] = propnet_weight_path
    params["simnet_weight_path"] = simnet_weight_path

    params["steps_between_evals"] = steps_between_evals
    params["batch_size"] = batch_size
    params["train_steps"] = train_steps
    params["validation_steps"] = validation_steps
    params["model_dir"] = model_dir
    params["mt_weight_path"] = mt_weight_path
    params["num_heads"] = args.he
    params["num_hidden_layers"] = args.hi
    params["filter_size"] = args.f
    params["learning_rate"] = float(args.lr)
    params["model_version"] = model_version

    params["n_samples"] = n_samples
    params["iterations"] = iterations
    params["data_dir"] = data_dir

    params["vocab_file"] = "%s/optgen_vocab.txt" % base_path
    params["vocab_size"] = 71

    params["use_propnet"] = args.pnet
    params["use_simnet"] = args.snet
    params["use_beam_filter"] = args.bf

    print("========================================[params]========================================")
    for k,v in params.items():
        print(k, ":" ,v)
    print("========================================[params]========================================")

    moltokenizer = Moltokenizer(params["vocab_file"])
    dev_smiles, _ = get_test_smiles(base_path, test_name, is_dev=1)
    tst_smiles, property_dic = get_test_smiles(base_path, test_name, is_dev=0)

    epoch_list = range(args.es, args.ee, args.et)
    print("epoch_list", [i for i in epoch_list])

    dev_score_list = []
    tst_score_list = []
    dev_std_list = []
    tst_std_list = []
    for epoch_num in epoch_list:
        tst_model = get_model(params, epoch_num)

        score_final_dev = evaluate(dev_smiles, property_dic, tst_model, moltokenizer)
        score_final_tst = evaluate(tst_smiles, property_dic, tst_model, moltokenizer)

        if test_name == 'logp04':
            dev_score = np.mean(score_final_dev)
            tst_score = np.mean(score_final_tst)
            dev_std = np.std(score_final_dev)
            tst_std = np.std(score_final_tst)
            dev_std_list.append(dev_std)
            tst_std_list.append(tst_std)

        elif test_name == 'qed':
            dev_score = sum(np.array(score_final_dev) >= 0.9) / float(len(score_final_dev))
            tst_score = sum(np.array(score_final_tst) >= 0.9) / float(len(score_final_tst))

        else: # test_name == 'drd2':
            dev_score = sum(np.array(score_final_dev) > 0.5) / float(len(score_final_dev))
            tst_score = sum(np.array(score_final_tst) > 0.5) / float(len(score_final_tst))

        dev_score_list.append(dev_score)
        tst_score_list.append(tst_score)
        print("======================[v%d.p%d.s%d.bf%d.prop(%s) epoch %d]======================" %
              (args.vnet, args.pnet, args.snet, args.bf, test_name, epoch_num))
        print("[dev_score_list]")
        print(dev_score_list)
        print("[tst_score_list]")
        print(tst_score_list)

    best_index = np.argmax(dev_score_list)
    print("best_index(%d), best_dev(%f), best_tst(%f)" % (
        best_index, dev_score_list[best_index], tst_score_list[best_index]))

    if test_name == 'logp04':
        print(dev_std_list[best_index])
        print(tst_std_list[best_index])