#!/usr/bin/python3

import numpy as np
from MinerEnv import MinerEnv
# from keras.models import model_from_json
import sys
from warnings import simplefilter
simplefilter(action='ignore', category=FutureWarning)

ACTION_GO_LEFT = 0
ACTION_GO_RIGHT = 1
ACTION_GO_UP = 2
ACTION_GO_DOWN = 3
ACTION_FREE = 4
ACTION_CRAFT = 5

HOST = "localhost"
PORT = 37313
# if len(sys.argv) == 3:
#     HOST = str(sys.argv[1])
#     PORT = int(sys.argv[2])

# load json and create model
# json_file = open('RLModelSample.json', 'r')
# loaded_model_json = json_file.read()
# json_file.close()
# DQNAgent = model_from_json(loaded_model_json)
# load weights into new model
# DQNAgent.load_weights("RLModelSample.h5")
# print("Loaded model from disk")
status_map = {0: "STATUS_PLAYING", 1: "STATUS_ELIMINATED_WENT_OUT_MAP", 2: "STATUS_ELIMINATED_OUT_OF_ENERGY",
              3: "STATUS_ELIMINATED_INVALID_ACTION", 4: "STATUS_STOP_EMPTY_GOLD", 5: "STATUS_STOP_END_STEP"}
action_map = {0: "GO LEFT", 1: "GO RIGHT",
              2: "GO DOWN", 3: "GO UP", 4: "SLEEP", 5: "DIG GOLD"}
count_step = 0

try:
    # Initialize environment
    minerEnv = MinerEnv(HOST, PORT)
    minerEnv.start()  # Connect to the game
    minerEnv.reset()
    s = minerEnv.get_state()  # Getting an initial state
    while not minerEnv.check_terminate():
        try:
            # Getting an action from the trained model
            # action = np.argmax(DQNAgent.predict(s.reshape(1, len(s))))
            action = minerEnv.get_action()
            print("next action = ", action_map[action])
            # Performing the action in order to obtain the new state
            minerEnv.step(str(action))
            s_next = minerEnv.get_state()  # Getting a new state
            s = s_next
            count_step += 1
        except Exception as e:
            import traceback
            traceback.print_exc()
            print("Finished.")
            break
    minerEnv.end()
    print("After finish, total step: ", count_step)
    print(status_map[minerEnv.state.status])

except Exception as e:
    import traceback
    traceback.print_exc()
print("End game.")


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

# # Language Models: Auto-Complete
#
# In this assignment, you will build an auto-complete system.  Auto-complete system is something you may see every day
# - When you google something, you often have suggestions to help you complete your search.
# - When you are writing an email, you get suggestions telling you possible endings to your sentence.
#
# By the end of this assignment, you will develop a prototype of such a system.
#
#

# ## Outline
# - [1 Load and Preprocess Data](#1)
# - [1.1: Load the data](#1.1)
# - [1.2 Pre-process the data](#1.2)
#     - [Exercise 01](#ex-01)
#     - [Exercise 02](#ex-02)
#     - [Exercise 03](#ex-03)
#     - [Exercise 04](#ex-04)
#     - [Exercise 05](#ex-05)
#     - [Exercise 06](#ex-06)
#     - [Exercise 07](#ex-07)
# - [2 Develop n-gram based language models](#2)
#     - [Exercise 08](#ex-08)
#     - [Exercise 09](#ex-09)
# - [3 Perplexity](#3)
#     - [Exercise 10](#ex-10)
# - [4 Build an auto-complete system](#4)
#     - [Exercise 11](#ex-11)

# A key building block for an auto-complete system is a language model.
# A language model assigns the probability to a sequence of words, in a way that more "likely" sequences receive higher scores.  For example,
# >"I have a pen"
# is expected to have a higher probability than
# >"I am a pen"
# since the first one seems to be a more natural sentence in the real world.
#
# You can take advantage of this probability calculation to develop an auto-complete system.
# Suppose the user typed
# >"I eat scrambled"
# Then you can find a word `x`  such that "I eat scrambled x" receives the highest probability.  If x = "eggs", the sentence would be
# >"I eat scrambled eggs"
#
# While a variety of language models have been developed, this assignment uses **N-grams**, a simple but powerful method for language modeling.
# - N-grams are also used in machine translation and speech recognition.
#
#
# Here are the steps of this assignment:
#
# 1. Load and preprocess data
#     - Load and tokenize data.
#     - Split the sentences into train and test sets.
#     - Replace words with a low frequency by an unknown marker `<unk>`.
# 1. Develop N-gram based language models
#     - Compute the count of n-grams from a given data set.
#     - Estimate the conditional probability of a next word with k-smoothing.
# 1. Evaluate the N-gram models by computing the perplexity score.
# 1. Use your own model to suggest an upcoming word given your sentence.

# In[92]:

import math
import random
import numpy as np
import pandas as pd
import nltk
nltk.data.path.append('.')

#
# ## Part 1: Load and Preprocess Data
#
#
# ### Part 1.1: Load the data
# You will use twitter data.
# Load the data and view the first few sentences by running the next cell.
#
# Notice that data is a long string that contains many many tweets.
# Observe that there is a line break "\n" between tweets.

# In[93]:

with open("en_US.twitter.txt", "r") as f:
    data = f.read()
print("Data type:", type(data))
print("Number of letters:", len(data))
print("First 300 letters of the data")
print("-------")
display(data[0:300])
print("-------")

print("Last 300 letters of the data")
print("-------")
display(data[-300:])
print("-------")

#
# ### Part 1.2 Pre-process the data
#
# Preprocess this data with the following steps:
#
# 1. Split data into sentences using "\n" as the delimiter.
# 1. Split each sentence into tokens. Note that in this assignment we use "token" and "words" interchangeably.
# 1. Assign sentences into train or test sets.
# 1. Find tokens that appear at least N times in the training data.
# 1. Replace tokens that appear less than N times by `<unk>`
#
#
# Note: we omit validation data in this exercise.
# - In real applications, we should hold a part of data as a validation set and use it to tune our training.
# - We skip this process for simplicity.

#
# ### Exercise 01
#
# Split data into sentences.

#
#
#     Hints
#
#

#

#
 Use str.split

#

#

# In[94]:

# UNQ_C1 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
### GRADED_FUNCTION: split_to_sentences ###
def split_to_sentences(data):
    """
    Split data by linebreak "\n"

    Args:
        data: str

    Returns:
        A list of sentences
    """
    ### START CODE HERE (Replace instances of 'None' with your code) ###
    sentences = data.split('\n')
    ### END CODE HERE ###

    # Additional clearning (This part is already implemented)
    # - Remove leading and trailing spaces from each sentence
    # - Drop sentences if they are empty strings.
    sentences = [s.strip() for s in sentences]
    sentences = [s for s in sentences if len(s) > 0]

    return sentences

# In[95]:

# test your code
x = """
I have a pen.\nI have an apple. \nAh\nApple pen.\n
"""
print(x)

split_to_sentences(x)

# Expected answer:
# ```CPP
# ['I have a pen.', 'I have an apple.', 'Ah', 'Apple pen.']
# ```

#
# ### Exercise 02
# The next step is to tokenize sentences (split a sentence into a list of words).
# - Convert all tokens into lower case so that words which are capitalized (for example, at the start of a sentence) in the original text are treated the same as the lowercase versions of the words.
# - Append each tokenized list of words into a list of tokenized sentences.

#
#
#     Hints
#
#

#

#
Use str.lower to convert strings to lowercase.

#
Please use nltk.word_tokenize to split sentences into tokens.

#
If you used str.split insteaad of nltk.word_tokenize, there are additional edge cases to handle, such as the punctuation (comma, period) that follows a word.

#

#

#

# In[96]:

# UNQ_C2 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
### GRADED_FUNCTION: tokenize_sentences ###
def tokenize_sentences(sentences):
    """
    Tokenize sentences into tokens (words)

    Args:
        sentences: List of strings

    Returns:
        List of lists of tokens
    """

    # Initialize the list of lists of tokenized sentences
    tokenized_sentences = []
    ### START CODE HERE (Replace instances of 'None' with your code) ###

    # Go through each sentence
    for sentence in sentences:

        # Convert to lowercase letters
        sentence = sentence.lower()

        # Convert into a list of words
        tokenized = nltk.word_tokenize(sentence)

        # append the list of words to the list of lists
        tokenized_sentences.append(tokenized)

    ### END CODE HERE ###

    return tokenized_sentences

# In[97]:

# test your code
sentences = ["Sky is blue.", "Leaves are green.", "Roses are red."]
tokenize_sentences(sentences)

# ### Expected output
#
# ```CPP
# [['sky', 'is', 'blue', '.'],
#  ['leaves', 'are', 'green', '.'],
#  ['roses', 'are', 'red', '.']]
# ```

#
# ### Exercise 03
#
#
# Use the two functions that you have just implemented to get the tokenized data.
# - split the data into sentences
# - tokenize those sentences

# In[98]:

# UNQ_C3 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
### GRADED_FUNCTION: get_tokenized_data ###
def get_tokenized_data(data):
    """
    Make a list of tokenized sentences

    Args:
        data: String

    Returns:
        List of lists of tokens
    """
    ### START CODE HERE (Replace instances of 'None' with your code) ###

    # Get the sentences by splitting up the data
    sentences = split_to_sentences(data)

    # Get the list of lists of tokens by tokenizing the sentences
    tokenized_sentences = tokenize_sentences(sentences)

    ### END CODE HERE ###

    return tokenized_sentences

# In[99]:

# test your function
x = "Sky is blue.\nLeaves are green\nRoses are red."
get_tokenized_data(x)

# ##### Expected outcome
#
# ```CPP
# [['sky', 'is', 'blue', '.'],
#  ['leaves', 'are', 'green'],
#  ['roses', 'are', 'red', '.']]
# ```

# ### Split into train and test sets
#
# Now run the cell below to split data into training and test sets.

# In[100]:

tokenized_data = get_tokenized_data(data)
random.seed(87)
random.shuffle(tokenized_data)

train_size = int(len(tokenized_data) * 0.8)
train_data = tokenized_data[0:train_size]
test_data = tokenized_data[train_size:]

# In[101]:

print("{} data are split into {} train and {} test set".format(
    len(tokenized_data), len(train_data), len(test_data)))

print("First training sample:")
print(train_data[0])

print("First test sample")
print(test_data[0])

# ##### Expected output
#
# ```CPP
# 47961 data are split into 38368 train and 9593 test set
# First training sample:
# ['i', 'personally', 'would', 'like', 'as', 'our', 'official', 'glove', 'of', 'the', 'team', 'local', 'company', 'and', 'quality', 'production']
# First test sample
# ['that', 'picture', 'i', 'just', 'seen', 'whoa', 'dere', '!', '!', '>', '>', '>', '>', '>', '>', '>']
# ```

#
# ### Exercise 04
#
# You won't use all the tokens (words) appearing in the data for training.  Instead, you will use the more frequently used words.
# - You will focus on the words that appear at least N times in the data.
# - First count how many times each word appears in the data.
#
# You will need a double for-loop, one for sentences and the other for tokens within a sentence.
#

#
#
#     Hints
#
#

#

#
If you decide to import and use defaultdict, remember to cast the dictionary back to a regular 'dict' before returning it.

#

#

#

# In[102]:

# UNQ_C4 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
### GRADED_FUNCTION: count_words ###
def count_words(tokenized_sentences):
    """
    Count the number of word appearence in the tokenized sentences

    Args:
        tokenized_sentences: List of lists of strings

    Returns:
        dict that maps word (str) to the frequency (int)
    """

    word_counts = {}
    ### START CODE HERE (Replace instances of 'None' with your code) ###

    # Loop through each sentence
    for sentence in tokenized_sentences: # complete this line

        # Go through each token in the sentence
        for token in sentence: # complete this line

            # If the token is not in the dictionary yet, set the count to 1
            if token not in word_counts.keys(): # complete this line
                word_counts[token] = 1

            # If the token is already in the dictionary, increment the count by 1
            else:
                word_counts[token] += 1

    ### END CODE HERE ###

    return word_counts

# In[103]:

# test your code
tokenized_sentences = [['sky', 'is', 'blue', '.'],
                       ['leaves', 'are', 'green', '.'],
                       ['roses', 'are', 'red', '.']]
count_words(tokenized_sentences)

# ##### Expected output
#
# Note that the order may differ.
#
# ```CPP
# {'sky': 1,
#  'is': 1,
#  'blue': 1,
#  '.': 3,
#  'leaves': 1,
#  'are': 2,
#  'green': 1,
#  'roses': 1,
#  'red': 1}
# ```

# ### Handling 'Out of Vocabulary' words
#
# If your model is performing autocomplete, but encounters a word that it never saw during training, it won't have an input word to help it determine the next word to suggest. The model will not be able to predict the next word because there are no counts for the current word.
# - This 'new' word is called an 'unknown word', or out of vocabulary (OOV) words.
# - The percentage of unknown words in the test set is called the  OOV  rate.
#
# To handle unknown words during prediction, use a special token to represent all unknown words 'unk'.
# - Modify the training data so that it has some 'unknown' words to train on.
# - Words to convert into "unknown" words are those that do not occur very frequently in the training set.
# - Create a list of the most frequent words in the training set, called the  closed vocabulary .
# - Convert all the other words that are not part of the closed vocabulary to the token 'unk'.

#
# ### Exercise 05
#
# You will now create a function that takes in a text document and a threshold `count_threshold`.
# - Any word whose count is greater than or equal to the threshold `count_threshold` is kept in the closed vocabulary.
# - Returns the word closed vocabulary list.

# In[104]:

# UNQ_C5 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
### GRADED_FUNCTION: get_words_with_nplus_frequency ###
def get_words_with_nplus_frequency(tokenized_sentences, count_threshold):
    """
    Find the words that appear N times or more

    Args:
        tokenized_sentences: List of lists of sentences
        count_threshold: minimum number of occurrences for a word to be in the closed vocabulary.

    Returns:
        List of words that appear N times or more
    """
    # Initialize an empty list to contain the words that
    # appear at least 'minimum_freq' times.
    closed_vocab = []

    # Get the word couts of the tokenized sentences
    # Use the function that you defined earlier to count the words
    word_counts = count_words(tokenized_sentences)

    ### START CODE HERE (Replace instances of 'None' with your code) ###

    # for each word and its count
    for word, cnt in word_counts.items(): # complete this line

        # check that the word's count
        # is at least as great as the minimum count
        if cnt >= count_threshold :

            # append the word to the list
            closed_vocab.append(word)
    ### END CODE HERE ###

    return closed_vocab

# In[105]:

# test your code
tokenized_sentences = [['sky', 'is', 'blue', '.'],
                       ['leaves', 'are', 'green', '.'],
                       ['roses', 'are', 'red', '.']]
tmp_closed_vocab = get_words_with_nplus_frequency(tokenized_sentences, count_threshold=2)
print(f"Closed vocabulary:")
print(tmp_closed_vocab)

# ##### Expected output
#
# ```CPP
# Closed vocabulary:
# ['.', 'are']
# ```

#
# ### Exercise 06
#
# The words that appear `count_threshold` times or more are in the closed vocabulary.
# - All other words are regarded as `unknown`.
# - Replace words not in the closed vocabulary with the token `<unk>`.

# In[106]:

# UNQ_C6 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
### GRADED_FUNCTION: replace_oov_words_by_unk ###
def replace_oov_words_by_unk(tokenized_sentences, vocabulary, unknown_token="<unk>"):
    """
    Replace words not in the given vocabulary with '<unk>' token.

    Args:
        tokenized_sentences: List of lists of strings
        vocabulary: List of strings that we will use
        unknown_token: A string representing unknown (out-of-vocabulary) words

    Returns:
        List of lists of strings, with words not in the vocabulary replaced
    """

    # Place vocabulary into a set for faster search
    vocabulary = set(vocabulary)

    # Initialize a list that will hold the sentences
    # after less frequent words are replaced by the unknown token
    replaced_tokenized_sentences = []

    # Go through each sentence
    for sentence in tokenized_sentences:

        # Initialize the list that will contain
        # a single sentence with "unknown_token" replacements
        replaced_sentence = []
        ### START CODE HERE (Replace instances of 'None' with your code) ###

        # for each token in the sentence
        for token in sentence: # complete this line

            # Check if the token is in the closed vocabulary
            if token in vocabulary: # complete this line
                # If so, append the word to the replaced_sentence
                replaced_sentence.append(token)
            else:
                # otherwise, append the unknown token instead
                replaced_sentence.append('<unk>')
        ### END CODE HERE ###

        # Append the list of tokens to the list of lists
        replaced_tokenized_sentences.append(replaced_sentence)

    return replaced_tokenized_sentences

# In[107]:

tokenized_sentences = [["dogs", "run"], ["cats", "sleep"]]
vocabulary = ["dogs", "sleep"]
tmp_replaced_tokenized_sentences = replace_oov_words_by_unk(tokenized_sentences, vocabulary)
print(f"Original sentence:")
print(tokenized_sentences)
print(f"tokenized_sentences with less frequent words converted to '<unk>':")
print(tmp_replaced_tokenized_sentences)

# ### Expected answer
#
# ```CPP
# Original sentence:
# [['dogs', 'run'], ['cats', 'sleep']]
# tokenized_sentences with less frequent words converted to '<unk>':
# [['dogs', '<unk>'], ['<unk>', 'sleep']]
# ```

#
# ### Exercise 07
#
# Now we are ready to process our data by combining the functions that you just implemented.
#
# 1. Find tokens that appear at least count_threshold times in the training data.
# 1. Replace tokens that appear less than count_threshold times by "<unk\>" both for training and test data.

# In[108]:

# UNQ_C7 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
### GRADED_FUNCTION: preprocess_data ###
def preprocess_data(train_data, test_data, count_threshold):
    """
    Preprocess data, i.e.,
        - Find tokens that appear at least N times in the training data.
        - Replace tokens that appear less than N times by "<unk>" both for training and test data.
    Args:
        train_data, test_data: List of lists of strings.
        count_threshold: Words whose count is less than this are
                      treated as unknown.

    Returns:
        Tuple of
        - training data with low frequent words replaced by "<unk>"
        - test data with low frequent words replaced by "<unk>"
        - vocabulary of words that appear n times or more in the training data
    """
    ### START CODE HERE (Replace instances of 'None' with your code) ###

    # Get the closed vocabulary using the train data
    vocabulary = get_words_with_nplus_frequency(train_data, count_threshold)

    # For the train data, replace less common words with "<unk>"
    train_data_replaced = replace_oov_words_by_unk(train_data, vocabulary)

    # For the test data, replace less common words with "<unk>"
    test_data_replaced = replace_oov_words_by_unk(test_data, vocabulary)

    ### END CODE HERE ###
    return train_data_replaced, test_data_replaced, vocabulary

# In[109]:

# test your code
tmp_train = [['sky', 'is', 'blue', '.'],
     ['leaves', 'are', 'green']]
tmp_test = [['roses', 'are', 'red', '.']]

tmp_train_repl, tmp_test_repl, tmp_vocab = preprocess_data(tmp_train,
                                                           tmp_test,
                                                           count_threshold = 1)

print("tmp_train_repl")
print(tmp_train_repl)
print()
print("tmp_test_repl")
print(tmp_test_repl)
print()
print("tmp_vocab")
print(tmp_vocab)

# ##### Expected outcome
#
# ```CPP
# tmp_train_repl
# [['sky', 'is', 'blue', '.'], ['leaves', 'are', 'green']]
#
# tmp_test_repl
# [['<unk>', 'are', '<unk>', '.']]
#
# tmp_vocab
# ['sky', 'is', 'blue', '.', 'leaves', 'are', 'green']
# ```

# ### Preprocess the train and test data
# Run the cell below to complete the preprocessing both for training and test sets.

# In[110]:

minimum_freq = 2
train_data_processed, test_data_processed, vocabulary = preprocess_data(train_data,
                                                                        test_data,
                                                                        minimum_freq)

# In[111]:

print("First preprocessed training sample:")
print(train_data_processed[0])
print()
print("First preprocessed test sample:")
print(test_data_processed[0])
print()
print("First 10 vocabulary:")
print(vocabulary[0:10])
print()
print("Size of vocabulary:", len(vocabulary))

# ##### Expected output
#
# ```CPP
# First preprocessed training sample:
# ['i', 'personally', 'would', 'like', 'as', 'our', 'official', 'glove', 'of', 'the', 'team', 'local', 'company', 'and', 'quality', 'production']
#
# First preprocessed test sample:
# ['that', 'picture', 'i', 'just', 'seen', 'whoa', 'dere', '!', '!', '>', '>', '>', '>', '>', '>', '>']
#
# First 10 vocabulary:
# ['i', 'personally', 'would', 'like', 'as', 'our', 'official', 'glove', 'of', 'the']
#
# Size of vocabulary: 14821
# ```

# You are done with the preprocessing section of the assignment.
# Objects `train_data_processed`, `test_data_processed`, and `vocabulary` will be used in the rest of the exercises.

#
# ## Part 2: Develop n-gram based language models
#
# In this section, you will develop the n-grams language model.
# - Assume the probability of the next word depends only on the previous n-gram.
# - The previous n-gram is the series of the previous 'n' words.
#
# The conditional probability for the word at position 't' in the sentence, given that the words preceding it are $w_{t-1}, w_{t-2} \cdots w_{t-n}$ is:
#
# $$ P(w_t | w_{t-1}\dots w_{t-n}) \tag{1}$$
#
# You can estimate this probability  by counting the occurrences of these series of words in the training data.
# - The probability can be estimated as a ratio, where
# - The numerator is the number of times word 't' appears after words t-1 through t-n appear in the training data.
# - The denominator is the number of times word t-1 through t-n appears in the training data.
#
# $$ \hat{P}(w_t | w_{t-1}\dots w_{t-n}) = \frac{C(w_{t-1}\dots w_{t-n}, w_n)}{C(w_{t-1}\dots w_{t-n})} \tag{2} $$
#
# - The function $C(\cdots)$ denotes the number of occurence of the given sequence.
# - $\hat{P}$ means the estimation of $P$.
# - Notice that denominator of the equation (2) is the number of occurence of the previous $n$ words, and the numerator is the same sequence followed by the word $w_t$.
#
# Later, you will modify the equation (2) by adding k-smoothing, which avoids errors when any counts are zero.
#
# The equation (2) tells us that to estimate probabilities based on n-grams, you need the counts of n-grams (for denominator) and (n+1)-grams (for numerator).

#
# ### Exercise 08
# Next, you will implement a function that computes the counts of n-grams for an arbitrary number $n$.
#
# When computing the counts for n-grams, prepare the sentence beforehand by prepending $n-1$ starting markers "<s\>" to indicate the beginning of the sentence.
# - For example, in the bi-gram model (N=2), a sequence with two start tokens "<s\><s\>" should predict the first word of a sentence.
# - So, if the sentence is "I like food", modify it to be "<s\><s\> I like food".
# - Also prepare the sentence for counting by appending an end token "<e\>" so that the model can predict when to finish a sentence.
#
# Technical note: In this implementation, you will store the counts as a dictionary.
# - The key of each key-value pair in the dictionary is a **tuple** of n words (and not a list)
# - The value in the key-value pair is the number of occurrences.
# - The reason for using a tuple as a key instead of a list is because a list in Python is a mutable object (it can be changed after it is first created).  A tuple is "immutable", so it cannot be altered after it is first created.  This makes a tuple suitable as a data type for the key in a dictionary.

#
#
#     Hints
#
#

#

#
 To prepend or append, you can create lists and concatenate them using the + operator

#
 To create a list of a repeated value, you can follow this syntax: ['a'] * 3 to get ['a','a','a']

#
To set the range for index 'i', think of this example: An n-gram where n=2 (bigram), and the sentence is length N=5 (including two start tokens and one end token).  So the index positions are [0,1,2,3,4].  The largest index 'i' where a bigram can start is at position i=3, because the word tokens at position 3 and 4 will form the bigram.

#
Remember that the range() function excludes the value that is used for the maximum of the range.   range(3)  produces (0,1,2) but excludes 3.

#

#

#

# In[112]:

# UNQ_C8 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
### GRADED FUNCTION: count_n_grams ###
def count_n_grams(data, n, start_token='<s>', end_token = '<e>'):
    """
    Count all n-grams in the data

    Args:
        data: List of lists of words
        n: number of words in a sequence

    Returns:
        A dictionary that maps a tuple of n-words to its frequency
    """

    # Initialize dictionary of n-grams and their counts
    n_grams = {}

    ### START CODE HERE (Replace instances of 'None' with your code) ###

    # Go through each sentence in the data
    for sentence in data: # complete this line

        # prepend start token n times, and  append <e> one time
        sentence = [start_token,]*n + sentence + [end_token, ]

        # convert list to tuple
        # So that the sequence of words can be used as
        # a key in the dictionary
        sentence = tuple(sentence)

        # Use 'i' to indicate the start of the n-gram
        # from index 0
        # to the last index where the end of the n-gram
        # is within the sentence.

        for i in range(len(sentence)-n + 1): # complete this line

            # Get the n-gram from i to i+n
            n_gram = sentence[i: i+n]

            # check if the n-gram is in the dictionary
            if n_gram in n_grams.keys(): # complete this line

                # Increment the count for this n-gram
                n_grams[n_gram] += 1
            else:
                # Initialize this n-gram count to 1
                n_grams[n_gram] = 1

            ### END CODE HERE ###
    return n_grams

# In[113]:

# test your code
# CODE REVIEW COMMENT: Outcome does not match expected outcome
sentences = [['i', 'like', 'a', 'cat'],
             ['this', 'dog', 'is', 'like', 'a', 'cat']]
print("Uni-gram:")
print(count_n_grams(sentences, 1))
print("Bi-gram:")
print(count_n_grams(sentences, 2))

# Expected outcome:
#
# ```CPP
# Uni-gram:
# {('<s>',): 2, ('i',): 1, ('like',): 2, ('a',): 2, ('cat',): 2, ('<e>',): 2, ('this',): 1, ('dog',): 1, ('is',): 1}
# Bi-gram:
# {('<s>', '<s>'): 2, ('<s>', 'i'): 1, ('i', 'like'): 1, ('like', 'a'): 2, ('a', 'cat'): 2, ('cat', '<e>'): 2, ('<s>', 'this'): 1, ('this', 'dog'): 1, ('dog', 'is'): 1, ('is', 'like'): 1}
# ```

#
# ### Exercise 09
#
# Next, estimate the probability of a word given the prior 'n' words using the n-gram counts.
#
# $$ \hat{P}(w_t | w_{t-1}\dots w_{t-n}) = \frac{C(w_{t-1}\dots w_{t-n}, w_n)}{C(w_{t-1}\dots w_{t-n})} \tag{2} $$
#
# This formula doesn't work when a count of an n-gram is zero..
# - Suppose we encounter an n-gram that did not occur in the training data.
# - Then, the equation (2) cannot be evaluated (it becomes zero divided by zero).
#
# A way to handle zero counts is to add k-smoothing.
# - K-smoothing adds a positive constant $k$ to each numerator and $k \times |V|$ in the denominator, where $|V|$ is the number of words in the vocabulary.
#
# $$ \hat{P}(w_t | w_{t-1}\dots w_{t-n}) = \frac{C(w_{t-1}\dots w_{t-n}, w_n) + k}{C(w_{t-1}\dots w_{t-n}) + k|V|} \tag{3} $$
#
#
# For n-grams that have a zero count, the equation (3) becomes $\frac{1}{|V|}$.
# - This means that any n-gram with zero count has the same probability of $\frac{1}{|V|}$.
#
# Define a function that computes the probability estimate (3) from n-gram counts and a constant $k$.
#
# - The function takes in a dictionary 'n_gram_counts', where the key is the n-gram and the value is the count of that n-gram.
# - The function also takes another dictionary n_plus1_gram_counts, which you'll use to find the count for the previous n-gram plus the current word.

#
#
#     Hints
#
#

#

#
To define a tuple containing a single value, add a comma after that value.  For example: ('apple',) is a tuple containing a single string 'apple'

#
To concatenate two tuples, use the '+' operator

#
 words

#

#

#

# In[114]:

# UNQ_C9 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
### GRADED FUNCTION: estimate_probability ###
def estimate_probability(word, previous_n_gram,
                         n_gram_counts, n_plus1_gram_counts, vocabulary_size, k=1.0):
    """
    Estimate the probabilities of a next word using the n-gram counts with k-smoothing

    Args:
        word: next word
        previous_n_gram: A sequence of words of length n
        n_gram_counts: Dictionary of counts of n-grams
        n_plus1_gram_counts: Dictionary of counts of (n+1)-grams
        vocabulary_size: number of words in the vocabulary
        k: positive constant, smoothing parameter

    Returns:
        A probability
    """
    # convert list to tuple to use it as a dictionary key
    previous_n_gram = tuple(previous_n_gram)

    ### START CODE HERE (Replace instances of 'None' with your code) ###

    # Set the denominator
    # If the previous n-gram exists in the dictionary of n-gram counts,
    # Get its count.  Otherwise set the count to zero
    # Use the dictionary that has counts for n-grams
    if previous_n_gram in n_gram_counts.keys():
        previous_n_gram_count = n_gram_counts[previous_n_gram]
    else: previous_n_gram_count = 0

    # Calculate the denominator using the count of the previous n gram
    # and apply k-smoothing
    denominator = previous_n_gram_count + vocabulary_size*k

    # Define n plus 1 gram as the previous n-gram plus the current word as a tuple
    n_plus1_gram = previous_n_gram + (word,)

    # Set the count to the count in the dictionary,
    # otherwise 0 if not in the dictionary
    # use the dictionary that has counts for the n-gram plus current word
    if n_plus1_gram in n_plus1_gram_counts.keys():
        n_plus1_gram_count = n_plus1_gram_counts[n_plus1_gram]
    else: n_plus1_gram_count = 0

    # Define the numerator use the count of the n-gram plus current word,
    # and apply smoothing
    numerator = n_plus1_gram_count + k

    # Calculate the probability as the numerator divided by denominator
    probability = numerator/denominator

    ### END CODE HERE ###

    return probability

# In[115]:

# test your code
sentences = [['i', 'like', 'a', 'cat'],
             ['this', 'dog', 'is', 'like', 'a', 'cat']]
unique_words = list(set(sentences[0] + sentences[1]))

unigram_counts = count_n_grams(sentences, 1)
bigram_counts = count_n_grams(sentences, 2)
tmp_prob = estimate_probability("cat", "a", unigram_counts, bigram_counts, len(unique_words), k=1)

print(f"The estimated probability of word 'cat' given the previous n-gram 'a' is: {tmp_prob:.4f}")

# ##### Expected output
#
# ```CPP
# The estimated probability of word 'cat' given the previous n-gram 'a' is: 0.3333
# ```

# ### Estimate probabilities for all words
#
# The function defined below loops over all words in vocabulary to calculate probabilities for all possible words.
# - This function is provided for you.

# In[116]:

def estimate_probabilities(previous_n_gram, n_gram_counts, n_plus1_gram_counts, vocabulary, k=1.0):
    """
    Estimate the probabilities of next words using the n-gram counts with k-smoothing

    Args:
        previous_n_gram: A sequence of words of length n
        n_gram_counts: Dictionary of counts of (n+1)-grams
        n_plus1_gram_counts: Dictionary of counts of (n+1)-grams
        vocabulary: List of words
        k: positive constant, smoothing parameter

    Returns:
        A dictionary mapping from next words to the probability.
    """

    # convert list to tuple to use it as a dictionary key
    previous_n_gram = tuple(previous_n_gram)

    # add <e> <unk> to the vocabulary
    # <s> is not needed since it should not appear as the next word
    vocabulary = vocabulary + ["<e>", "<unk>"]
    vocabulary_size = len(vocabulary)

    probabilities = {}
    for word in vocabulary:
        probability = estimate_probability(word, previous_n_gram,
                                           n_gram_counts, n_plus1_gram_counts,
                                           vocabulary_size, k=k)
        probabilities[word] = probability

    return probabilities

# In[117]:

# test your code
sentences = [['i', 'like', 'a', 'cat'],
             ['this', 'dog', 'is', 'like', 'a', 'cat']]
unique_words = list(set(sentences[0] + sentences[1]))
unigram_counts = count_n_grams(sentences, 1)
bigram_counts = count_n_grams(sentences, 2)
estimate_probabilities("a", unigram_counts, bigram_counts, unique_words, k=1)

# ##### Expected output
#
# ```CPP
# {'cat': 0.2727272727272727,
#  'i': 0.09090909090909091,
#  'this': 0.09090909090909091,
#  'a': 0.09090909090909091,
#  'is': 0.09090909090909091,
#  'like': 0.09090909090909091,
#  'dog': 0.09090909090909091,
#  '<e>': 0.09090909090909091,
#  '<unk>': 0.09090909090909091}
# ```

# In[118]:

# Additional test
trigram_counts = count_n_grams(sentences, 3)
estimate_probabilities(["<s>", "<s>"], bigram_counts, trigram_counts, unique_words, k=1)

# ##### Expected output
#
# ```CPP
# {'cat': 0.09090909090909091,
#  'i': 0.18181818181818182,
#  'this': 0.18181818181818182,
#  'a': 0.09090909090909091,
#  'is': 0.09090909090909091,
#  'like': 0.09090909090909091,
#  'dog': 0.09090909090909091,
#  '<e>': 0.09090909090909091,
#  '<unk>': 0.09090909090909091}
# ```

# ### Count and probability matrices
#
# As we have seen so far, the n-gram counts computed above are sufficient for computing the probabilities of the next word.
# - It can be more intuitive to present them as count or probability matrices.
# - The functions defined in the next cells return count or probability matrices.
# - This function is provided for you.

# In[119]:

def make_count_matrix(n_plus1_gram_counts, vocabulary):
    # add <e> <unk> to the vocabulary
    # <s> is omitted since it should not appear as the next word
    vocabulary = vocabulary + ["<e>", "<unk>"]

    # obtain unique n-grams
    n_grams = []
    for n_plus1_gram in n_plus1_gram_counts.keys():
        n_gram = n_plus1_gram[0:-1]
        n_grams.append(n_gram)
    n_grams = list(set(n_grams))

    # mapping from n-gram to row
    row_index = {n_gram:i for i, n_gram in enumerate(n_grams)}
    # mapping from next word to column
    col_index = {word:j for j, word in enumerate(vocabulary)}

    nrow = len(n_grams)
    ncol = len(vocabulary)
    count_matrix = np.zeros((nrow, ncol))
    for n_plus1_gram, count in n_plus1_gram_counts.items():
        n_gram = n_plus1_gram[0:-1]
        word = n_plus1_gram[-1]
        if word not in vocabulary:
            continue
        i = row_index[n_gram]
        j = col_index[word]
        count_matrix[i, j] = count

    count_matrix = pd.DataFrame(count_matrix, index=n_grams, columns=vocabulary)
    return count_matrix

# In[120]:

sentences = [['i', 'like', 'a', 'cat'],
                 ['this', 'dog', 'is', 'like', 'a', 'cat']]
unique_words = list(set(sentences[0] + sentences[1]))
bigram_counts = count_n_grams(sentences, 2)

print('bigram counts')
display(make_count_matrix(bigram_counts, unique_words))

# ##### Expected output
#
# ```CPP
# bigram counts
#           cat    i   this   a  is   like  dog  <e>   <unk>
# (<s>,)    0.0   1.0  1.0  0.0  0.0  0.0   0.0  0.0    0.0
# (a,)      2.0   0.0  0.0  0.0  0.0  0.0   0.0  0.0    0.0
# (this,)   0.0   0.0  0.0  0.0  0.0  0.0   1.0  0.0    0.0
# (like,)   0.0   0.0  0.0  2.0  0.0  0.0   0.0  0.0    0.0
# (dog,)    0.0   0.0  0.0  0.0  1.0  0.0   0.0  0.0    0.0
# (cat,)    0.0   0.0  0.0  0.0  0.0  0.0   0.0  2.0    0.0
# (is,)     0.0   0.0  0.0  0.0  0.0  1.0   0.0  0.0    0.0
# (i,)      0.0   0.0  0.0  0.0  0.0  1.0   0.0  0.0    0.0
# ```

# In[121]:

# Show trigram counts
print('\ntrigram counts')
trigram_counts = count_n_grams(sentences, 3)
display(make_count_matrix(trigram_counts, unique_words))

# ##### Expected output
#
# ```CPP
# trigram counts
#               cat    i   this   a  is   like  dog  <e>   <unk>
# (dog, is)     0.0   0.0  0.0  0.0  0.0  1.0   0.0  0.0    0.0
# (this, dog)   0.0   0.0  0.0  0.0  1.0  0.0   0.0  0.0    0.0
# (a, cat)      0.0   0.0  0.0  0.0  0.0  0.0   0.0  2.0    0.0
# (like, a)     2.0   0.0  0.0  0.0  0.0  0.0   0.0  0.0    0.0
# (is, like)    0.0   0.0  0.0  1.0  0.0  0.0   0.0  0.0    0.0
# (<s>, i)      0.0   0.0  0.0  0.0  0.0  1.0   0.0  0.0    0.0
# (i, like)     0.0   0.0  0.0  1.0  0.0  0.0   0.0  0.0    0.0
# (<s>, <s>)    0.0   1.0  1.0  0.0  0.0  0.0   0.0  0.0    0.0
# (<s>, this)   0.0   0.0  0.0  0.0  0.0  0.0   1.0  0.0    0.0
# ```

# The following function calculates the probabilities of each word given the previous n-gram, and stores this in matrix form.
# - This function is provided for you.

# In[122]:

def make_probability_matrix(n_plus1_gram_counts, vocabulary, k):
    count_matrix = make_count_matrix(n_plus1_gram_counts, unique_words)
    count_matrix += k
    prob_matrix = count_matrix.div(count_matrix.sum(axis=1), axis=0)
    return prob_matrix

# In[123]:

sentences = [['i', 'like', 'a', 'cat'],
                 ['this', 'dog', 'is', 'like', 'a', 'cat']]
unique_words = list(set(sentences[0] + sentences[1]))
bigram_counts = count_n_grams(sentences, 2)
print("bigram probabilities")
display(make_probability_matrix(bigram_counts, unique_words, k=1))

# In[124]:

print("trigram probabilities")
trigram_counts = count_n_grams(sentences, 3)
display(make_probability_matrix(trigram_counts, unique_words, k=1))

# Confirm that you obtain the same results as for the `estimate_probabilities` function that you implemented.

#
# ## Part 3: Perplexity
#
# In this section, you will generate the perplexity score to evaluate your model on the test set.
# - You will also use back-off when needed.
# - Perplexity is used as an evaluation metric of your language model.
# - To calculate the  the perplexity score of the test set on an n-gram model, use:
#
# $$ PP(W) =\sqrt[N]{ \prod_{t=n+1}^N \frac{1}{P(w_t | w_{t-n} \cdots w_{t-1})} } \tag{4}$$
#
# - where $N$ is the length of the sentence.
# - $n$ is the number of words in the n-gram (e.g. 2 for a bigram).
# - In math, the numbering starts at one and not zero.
#
# In code, array indexing starts at zero, so the code will use ranges for $t$ according to this formula:
#
# $$ PP(W) =\sqrt[N]{ \prod_{t=n}^{N-1} \frac{1}{P(w_t | w_{t-n} \cdots w_{t-1})} } \tag{4.1}$$
#
# The higher the probabilities are, the lower the perplexity will be.
# - The more the n-grams tell us about the sentence, the lower the perplexity score will be.

#
# ### Exercise 10
# Compute the perplexity score given an N-gram count matrix and a sentence.

#
#
#     Hints
#
#

#

#
Remember that range(2,4) produces the integers [2, 3] (and excludes 4).

#

#

#

# In[125]:

# UNQ_C10 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
# GRADED FUNCTION: calculate_perplexity
def calculate_perplexity(sentence, n_gram_counts, n_plus1_gram_counts, vocabulary_size, k=1.0):
    """
    Calculate perplexity for a list of sentences

    Args:
        sentence: List of strings
        n_gram_counts: Dictionary of counts of (n+1)-grams
        n_plus1_gram_counts: Dictionary of counts of (n+1)-grams
        vocabulary_size: number of unique words in the vocabulary
        k: Positive smoothing constant

    Returns:
        Perplexity score
    """

    # length of previous words
    n = len(list(n_gram_counts.keys())[0])

    # prepend <s> and append <e>
    sentence = ["<s>"] * n + sentence + ["<e>"]

    # Cast the sentence from a list to a tuple
    sentence = tuple(sentence)

    # length of sentence (after adding <s> and <e> tokens)
    N = len(sentence)

    # The variable p will hold the product
    # that is calculated inside the n-root
    # Update this in the code below
    product_pi = 1.0

    ### START CODE HERE (Replace instances of 'None' with your code) ###

    # Index t ranges from n to N - 1, inclusive on both ends
    for t in range(n, N): # complete this line

        # get the n-gram preceding the word at position t
        n_gram = sentence[t-n:t]

        # get the word at position t
        word = sentence[t]

        # Estimate the probability of the word given the n-gram
        # using the n-gram counts, n-plus1-gram counts,
        # vocabulary size, and smoothing constant
        probability = estimate_probability(word, n_gram, n_gram_counts,
                                             n_plus1_gram_counts, vocabulary_size, k=1.0)

        #estimate_probability("cat", "a", unigram_counts, bigram_counts, len(unique_words), k=1)
        # Update the product of the probabilities
        # This 'product_pi' is a cumulative product
        # of the (1/P) factors that are calculated in the loop
        product_pi *= probability

    # Take the Nth root of the product
    perplexity = product_pi**(-1/N)

    ### END CODE HERE ###
    return perplexity

# In[126]:

# test your code

sentences = [['i', 'like', 'a', 'cat'],
                 ['this', 'dog', 'is', 'like', 'a', 'cat']]
unique_words = list(set(sentences[0] + sentences[1]))

unigram_counts = count_n_grams(sentences, 1)
bigram_counts = count_n_grams(sentences, 2)

perplexity_train1 = calculate_perplexity(sentences[0],
                                         unigram_counts, bigram_counts,
                                         len(unique_words), k=1.0)
print(f"Perplexity for first train sample: {perplexity_train1:.4f}")

test_sentence = ['i', 'like', 'a', 'dog']
perplexity_test = calculate_perplexity(test_sentence,
                                       unigram_counts, bigram_counts,
                                       len(unique_words), k=1.0)
print(f"Perplexity for test sample: {perplexity_test:.4f}")

# ### Expected Output
#
# ```CPP
# Perplexity for first train sample: 2.8040
# Perplexity for test sample: 3.9654
# ```
#
#  Note:  If your sentence is really long, there will be underflow when multiplying many fractions.
# - To handle longer sentences, modify your implementation to take the sum of the log of the probabilities.

#
# ## Part 4: Build an auto-complete system
#
# In this section, you will combine the language models developed so far to implement an auto-complete system.
#

#
# ### Exercise 11
# Compute probabilities for all possible next words and suggest the most likely one.
# - This function also take an optional argument `start_with`, which specifies the first few letters of the next words.

#
#
#     Hints
#
#

#

#
estimate_probabilities returns a dictionary where the key is a word and the value is the word's probability.

#
 Use str1.startswith(str2) to determine if a string starts with the letters of another string.  For example, 'learning'.startswith('lea') returns True, whereas 'learning'.startswith('ear') returns False. There are two additional parameters in str.startswith(), but you can use the default values for those parameters in this case.

#

#

# In[137]:

# UNQ_C11 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
# GRADED FUNCTION: suggest_a_word
def suggest_a_word(previous_tokens, n_gram_counts, n_plus1_gram_counts, vocabulary, k=1.0, start_with=None):
    """
    Get suggestion for the next word

    Args:
        previous_tokens: The sentence you input where each token is a word. Must have length > n
        n_gram_counts: Dictionary of counts of (n+1)-grams
        n_plus1_gram_counts: Dictionary of counts of (n+1)-grams
        vocabulary: List of words
        k: positive constant, smoothing parameter
        start_with: If not None, specifies the first few letters of the next word

    Returns:
        A tuple of
          - string of the most likely next word
          - corresponding probability
    """

    # length of previous words
    n = len(list(n_gram_counts.keys())[0])

    # From the words that the user already typed
    # get the most recent 'n' words as the previous n-gram
    previous_n_gram = previous_tokens[-n:]

    # Estimate the probabilities that each word in the vocabulary
    # is the next word,
    # given the previous n-gram, the dictionary of n-gram counts,
    # the dictionary of n plus 1 gram counts, and the smoothing constant
    probabilities = estimate_probabilities(previous_n_gram,
                                           n_gram_counts, n_plus1_gram_counts,
                                           vocabulary, k=k)

    # Initialize suggested word to None
    # This will be set to the word with highest probability
    suggestion = None

    # Initialize the highest word probability to 0
    # this will be set to the highest probability
    # of all words to be suggested
    max_prob = 0

    ### START CODE HERE (Replace instances of 'None' with your code) ###

    # For each word and its probability in the probabilities dictionary:
    for word, prob in probabilities.items(): # complete this line

        # If the optional start_with string is set
        if isinstance(start_with, str): # complete this line

            # Check if the beginning of word does not match with the letters in 'start_with'
            if not(word.startswith(start_with)): # complete this line

                # if they don't match, skip this word (move onto the next word)
                continue # complete this line

        # Check if this word's probability
        # is greater than the current maximum probability
        if prob > max_prob: # complete this line

            # If so, save this word as the best suggestion (so far)
            suggestion = word

            # Save the new maximum probability
            max_prob = prob

    ### END CODE HERE

    return suggestion, max_prob

# In[138]:

# test your code
sentences = [['i', 'like', 'a', 'cat'],
             ['this', 'dog', 'is', 'like', 'a', 'cat']]
unique_words = list(set(sentences[0] + sentences[1]))

unigram_counts = count_n_grams(sentences, 1)
bigram_counts = count_n_grams(sentences, 2)

previous_tokens = ["i", "like"]
tmp_suggest1 = suggest_a_word(previous_tokens, unigram_counts, bigram_counts, unique_words, k=1.0)
print(f"The previous words are 'i like',\n\tand the suggested word is `{tmp_suggest1[0]}` with a probability of {tmp_suggest1[1]:.4f}")

print()
# test your code when setting the starts_with
tmp_starts_with = 'c'
tmp_suggest2 = suggest_a_word(previous_tokens, unigram_counts, bigram_counts, unique_words, k=1.0, start_with=tmp_starts_with)
print(f"The previous words are 'i like', the suggestion must start with `{tmp_starts_with}`\n\tand the suggested word is `{tmp_suggest2[0]}` with a probability of {tmp_suggest2[1]:.4f}")

# ### Expected output
#
# ```CPP
# The previous words are 'i like',
# 	and the suggested word is `a` with a probability of 0.2727
#
# The previous words are 'i like', the suggestion must start with `c`
# 	and the suggested word is `cat` with a probability of 0.0909
#
# ```

# ### Get multiple suggestions
#
# The function defined below loop over varioud n-gram models to get multiple suggestions.

# In[139]:

def get_suggestions(previous_tokens, n_gram_counts_list, vocabulary, k=1.0, start_with=None):
    model_counts = len(n_gram_counts_list)
    suggestions = []
    for i in range(model_counts-1):
        n_gram_counts = n_gram_counts_list[i]
        n_plus1_gram_counts = n_gram_counts_list[i+1]

        suggestion = suggest_a_word(previous_tokens, n_gram_counts,
                                    n_plus1_gram_counts, vocabulary,
                                    k=k, start_with=start_with)
        suggestions.append(suggestion)
    return suggestions

# In[140]:

# test your code
sentences = [['i', 'like', 'a', 'cat'],
             ['this', 'dog', 'is', 'like', 'a', 'cat']]
unique_words = list(set(sentences[0] + sentences[1]))

unigram_counts = count_n_grams(sentences, 1)
bigram_counts = count_n_grams(sentences, 2)
trigram_counts = count_n_grams(sentences, 3)
quadgram_counts = count_n_grams(sentences, 4)
qintgram_counts = count_n_grams(sentences, 5)

n_gram_counts_list = [unigram_counts, bigram_counts, trigram_counts, quadgram_counts, qintgram_counts]
previous_tokens = ["i", "like"]
tmp_suggest3 = get_suggestions(previous_tokens, n_gram_counts_list, unique_words, k=1.0)

print(f"The previous words are 'i like', the suggestions are:")
display(tmp_suggest3)

# ### Suggest multiple words using n-grams of varying length
#
# Congratulations!  You have developed all building blocks for implementing your own auto-complete systems.
#
# Let's see this with n-grams of varying lengths (unigrams, bigrams, trigrams, 4-grams...6-grams).

# In[141]:

n_gram_counts_list = []
for n in range(1, 6):
    print("Computing n-gram counts with n =", n, "...")
    n_model_counts = count_n_grams(train_data_processed, n)
    n_gram_counts_list.append(n_model_counts)

# In[142]:

previous_tokens = ["i", "am", "to"]
tmp_suggest4 = get_suggestions(previous_tokens, n_gram_counts_list, vocabulary, k=1.0)

print(f"The previous words are {previous_tokens}, the suggestions are:")
display(tmp_suggest4)

# In[143]:

previous_tokens = ["i", "want", "to", "go"]
tmp_suggest5 = get_suggestions(previous_tokens, n_gram_counts_list, vocabulary, k=1.0)

print(f"The previous words are {previous_tokens}, the suggestions are:")
display(tmp_suggest5)

# In[144]:

previous_tokens = ["hey", "how", "are"]
tmp_suggest6 = get_suggestions(previous_tokens, n_gram_counts_list, vocabulary, k=1.0)

print(f"The previous words are {previous_tokens}, the suggestions are:")
display(tmp_suggest6)

# In[145]:

previous_tokens = ["hey", "how", "are", "you"]
tmp_suggest7 = get_suggestions(previous_tokens, n_gram_counts_list, vocabulary, k=1.0)

print(f"The previous words are {previous_tokens}, the suggestions are:")
display(tmp_suggest7)

# In[146]:

previous_tokens = ["hey", "how", "are", "you"]
tmp_suggest8 = get_suggestions(previous_tokens, n_gram_counts_list, vocabulary, k=1.0, start_with="d")

print(f"The previous words are {previous_tokens}, the suggestions are:")
display(tmp_suggest8)

# # Congratulations!
#
# You've completed this assignment by building an autocomplete model using an n-gram language model!
#
# Please continue onto the fourth and final week of this course!


import warnings
warnings.filterwarnings("ignore")
import os
import cv2
import numpy as np
from keras.models import model_from_json
from keras.preprocessing import image

# Load Model
model=model_from_json(open("Trained Model/trainedModel.json","r").read())
# Load Weights
model.load_weights("Trained Model/trainedModel.h5")

face_haar_cascade=cv2.CascadeClassifier('Cascade Classifier/haarcascade_frontalface_default.xml')

cap=cv2.VideoCapture(0)

while True:
	# Capture Image And Returns a Bool value
	ret,test_img=cap.read()
	if not ret:
		continue
	gray_img=cv2.cvtColor(test_img,cv2.COLOR_BGR2GRAY)

	faces_detected=face_haar_cascade.detectMultiScale(gray_img,scaleFactor=1.3,minNeighbors=5)

	for(x,y,w,h) in faces_detected:
		cv2.rectangle(test_img,(x,y),(x+w,y+h),(255,0,0),thickness=2)
		roi_gray=gray_img[y:y+w,x:x+h] # Cropping Face From The Frame Captured
		roi_gray=cv2.resize(roi_gray,(48,48))
		img_pixels=image.img_to_array(roi_gray)
		img_pixels=np.expand_dims(img_pixels,axis=0)
		img_pixels/=255

		predictions=model.predict(img_pixels)

		# Finding Emotion Index
		max_index=np.argmax(predictions[0])

		emotions=('Angry','Disgust','Fear','Happy','Sad','Surprise','Nuetral')
		predicted_emotion=emotions[max_index]

		cv2.putText(test_img,predicted_emotion,(int(x),int(y)),cv2.FONT_HERSHEY_SIMPLEX,1,(0,0,255),2)

	resized_img=cv2.resize(test_img,(1000,700))
	cv2.imshow('FACIAL EMOTION DETECTION',resized_img)

	# Press q to exit
	if cv2.waitKey(10)==ord('q'):
		break
cap.release()


# encoding = utf-8

import numpy as np
import tensorflow as tf
import tensorflow.contrib.slim as slim
from tensorflow.contrib.slim import arg_scope

from config import cfg
from utils.anchors import generate_anchors_pre
from utils.layer_utils import proposal_layer_tf

class BaseModel(object):
    def __init__(self):
        self._image = None
        self._im_info = None
        self._gt_boxes = None
        self._num_classes = 0
        self._anchors = None
        self._anchor_length = 0
        self._tag = None
        self._training = None

        self._predictions = {}
        self._losses = {}
        self._anchor_targets = {}
        self._proposal_targets = {}
        self._layers = {}
        self._gt_image = None
        self._act_summaries = []
        self._score_summaries = {}
        self._train_summaries = []
        self._event_summaries = {}
        self._variables_to_fix = {}

        self._debug = {}

    def create_architecture(self, num_classes, training=True, tag='default',
                            anchor_scales=(8, 16, 32), anchor_ratios=(0.5, 1, 2)):

        assert num_classes != 0, '[error] num_classes = 0'

        # placeholders
        self._image = tf.placeholder(tf.float32, shape=[1, None, None, 3])  # input image
        self._im_info = tf.placeholder(tf.float32, shape=[3])  # height, width, scale
        self._gt_boxes = tf.placeholder(tf.float32, shape=[None, 5])  # x,y,w,h,label

        self._num_classes = num_classes
        self._tag = tag
        self._training = training
        self._anchor_scales = anchor_scales
        self._anchor_ratios = anchor_ratios

        self._num_scales = len(anchor_scales)
        self._num_ratios = len(anchor_ratios)

        self._num_anchors = self._num_scales * self._num_ratios  # k = 9

        weights_regularizer = tf.contrib.layers.l2_regularizer(cfg.TRAIN_WEIGHT_DECAY)
        # 使用arg_scope减少代码重复
        with arg_scope([slim.conv2d, slim.conv2d_in_plane,
                        slim.conv2d_transpose, slim.separable_conv2d, slim.fully_connected],
                       weights_regularizer=weights_regularizer,
                       biases_regularizer=None,
                       biases_initializer=tf.constant_initializer(0.0)):

            # rois, cls_prob, bbox_pred = self._build_graph(training)
            self._build_graph(training)

        if training:
            pass
        else:
            # test
            stds = np.tile(np.array(cfg.BBOX_NORMALIZE_STDS), self._num_classes)
            means = np.tile(np.array(cfg.BBOX_NORMALIZE_MEANS), self._num_classes)
            self._predictions["bbox_pred"] *= stds
            self._predictions["bbox_pred"] += means

    def _build_graph(self, training=True):
        initializer = tf.random_normal_initializer(mean=0.0, stddev=0.01)
        initializer_bbox = tf.random_normal_initializer(mean=0.0, stddev=0.001)

        net_conv = self._image_to_head(training)  # feature maps

        self._debug['net_conv'] = net_conv

        with tf.variable_scope(self._scope, self._scope):
            # build the anchors for the image
            self._anchor_component()

            # region proposal network
            rois = self._region_proposal(net_conv, training, initializer)

            # region of interest pooling
            pool5 = self._crop_pool_layer(net_conv, rois, "pool5")  # roi pooling

        self._debug['pool5'] = pool5

        fc7 = self._head_to_tail(pool5, training)

        with tf.variable_scope(self._scope, self._scope):
            # region classification
            cls_prob, bbox_pred = self._region_classification(fc7, training,
                                                              initializer, initializer_bbox)

        self._score_summaries.update(self._predictions)

        return rois, cls_prob, bbox_pred

    def _anchor_component(self):
        with tf.variable_scope('ANCHOR_' + self._tag) as scope:
            # just to get the shape right
            height = tf.to_int32(tf.ceil(self._im_info[0] / np.float32(self._feat_stride[0])))
            width = tf.to_int32(tf.ceil(self._im_info[1] / np.float32(self._feat_stride[0])))

            anchors, anchor_length = generate_anchors_pre(
                height,
                width,
                self._feat_stride,
                self._anchor_scales,
                self._anchor_ratios
            )

            anchors.set_shape([None, 4])
            anchor_length.set_shape([])
            self._anchors = anchors
            self._anchor_length = anchor_length

    '''
    debug function
    only use in debug mode

    '''

    def debug(self, sess, image, im_info):
        feed_dict = {self._image: image,
                     self._im_info: im_info}

        S = tf.shape
        pool5, rpn_cls_score, rpn_cls_score_reshape, rois = sess.run([S(self._debug['pool5']),
                                                                      S(self._predictions["rpn_cls_score"]),
                                                                      S(self._predictions["rpn_cls_score_reshape"]),
                                                                      self._predictions['rois']]
                                                                     , feed_dict=feed_dict)

        print('_image shape:', image.shape)
        print('pool5 shape:', pool5)
        print('rpn_cls_score shape:', rpn_cls_score)
        print('rpn_cls_score_reshape shape:', rpn_cls_score_reshape)

        print('rois:', rois)

    def _region_proposal(self, net_conv, is_training, initializer):
        rpn = slim.conv2d(net_conv, cfg.RPN_CHANNELS, [3, 3], trainable=is_training, weights_initializer=initializer,
                          scope="rpn_conv/3x3")
        self._act_summaries.append(rpn)
        rpn_cls_score = slim.conv2d(rpn, self._num_anchors * 2, [1, 1], trainable=is_training,
                                    weights_initializer=initializer,
                                    padding='VALID', activation_fn=None, scope='rpn_cls_score')
        # change it so that the score has 2 as its channel size
        rpn_cls_score_reshape = self._reshape_layer(rpn_cls_score, 2, 'rpn_cls_score_reshape')
        rpn_cls_prob_reshape = self._softmax_layer(rpn_cls_score_reshape, "rpn_cls_prob_reshape")
        rpn_cls_pred = tf.argmax(tf.reshape(rpn_cls_score_reshape, [-1, 2]), axis=1, name="rpn_cls_pred")
        rpn_cls_prob = self._reshape_layer(rpn_cls_prob_reshape, self._num_anchors * 2, "rpn_cls_prob")
        rpn_bbox_pred = slim.conv2d(rpn, self._num_anchors * 4, [1, 1], trainable=is_training,
                                    weights_initializer=initializer,
                                    padding='VALID', activation_fn=None, scope='rpn_bbox_pred')
        if is_training:
            rois, roi_scores = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
            rpn_labels = self._anchor_target_layer(rpn_cls_score, "anchor")
            # Try to have a deterministic order for the computing graph, for reproducibility
            with tf.control_dependencies([rpn_labels]):
                rois, _ = self._proposal_target_layer(rois, roi_scores, "rpn_rois")
        else:
            if cfg.TEST_MODE == 'nms':
                rois, _ = self._proposal_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
            elif cfg.TEST_MODE == 'top':
                rois, _ = self._proposal_top_layer(rpn_cls_prob, rpn_bbox_pred, "rois")
            else:
                raise NotImplementedError

        self._predictions["rpn_cls_score"] = rpn_cls_score
        self._predictions["rpn_cls_score_reshape"] = rpn_cls_score_reshape
        self._predictions["rpn_cls_prob"] = rpn_cls_prob
        self._predictions["rpn_cls_pred"] = rpn_cls_pred
        self._predictions["rpn_bbox_pred"] = rpn_bbox_pred
        self._predictions["rois"] = rois

        return rois

    def _proposal_layer(self, rpn_cls_prob, rpn_bbox_pred, name):
        with tf.variable_scope(name) as scope:
            rois, rpn_scores = proposal_layer_tf(
                rpn_cls_prob,
                rpn_bbox_pred,
                self._im_info,
                self._training,
                self._feat_stride,
                self._anchors,
                self._num_anchors
            )

            rois.set_shape([None, 5])
            rpn_scores.set_shape([None, 1])

        return rois, rpn_scores

        # Only use it if you have roi_pooling op written in tf.image

    def _region_classification(self, fc7, is_training, initializer, initializer_bbox):
        cls_score = slim.fully_connected(fc7, self._num_classes,
                                         weights_initializer=initializer,
                                         trainable=is_training,
                                         activation_fn=None, scope='cls_score')
        cls_prob = self._softmax_layer(cls_score, "cls_prob")
        cls_pred = tf.argmax(cls_score, axis=1, name="cls_pred")
        bbox_pred = slim.fully_connected(fc7, self._num_classes * 4,
                                         weights_initializer=initializer_bbox,
                                         trainable=is_training,
                                         activation_fn=None, scope='bbox_pred')

        self._predictions["cls_score"] = cls_score
        self._predictions["cls_pred"] = cls_pred
        self._predictions["cls_prob"] = cls_prob
        self._predictions["bbox_pred"] = bbox_pred

        return cls_prob, bbox_pred

    def _image_to_head(self, is_training, reuse=None):
        raise NotImplementedError

    def _head_to_tail(self, pool5, is_training, reuse=None):
        raise NotImplementedError

    def _reshape_layer(self, bottom, num_dim, name):
        input_shape = tf.shape(bottom)
        with tf.variable_scope(name) as scope:
            # change the channel to the caffe format
            to_caffe = tf.transpose(bottom, [0, 3, 1, 2])
            # then force it to have channel 2
            reshaped = tf.reshape(to_caffe,
                                  tf.concat(axis=0, values=[[1, num_dim, -1], [input_shape[2]]]))
            # then swap the channel back
            to_tf = tf.transpose(reshaped, [0, 2, 3, 1])
            return to_tf

    def _softmax_layer(self, bottom, name):
        if name.startswith('rpn_cls_prob_reshape'):
            input_shape = tf.shape(bottom)
            bottom_reshaped = tf.reshape(bottom, [-1, input_shape[-1]])
            reshaped_score = tf.nn.softmax(bottom_reshaped, name=name)
            return tf.reshape(reshaped_score, input_shape)
        return tf.nn.softmax(bottom, name=name)

    def _proposal_top_layer(self, rpn_cls_prob, rpn_bbox_pred, name):
        with tf.variable_scope(name) as scope:
            if cfg.USE_E2E_TF:
                rois, rpn_scores = proposal_top_layer_tf(
                    rpn_cls_prob,
                    rpn_bbox_pred,
                    self._im_info,
                    self._feat_stride,
                    self._anchors,
                    self._num_anchors
                )
            else:
                rois, rpn_scores = tf.py_func(proposal_top_layer,
                                              [rpn_cls_prob, rpn_bbox_pred, self._im_info,
                                               self._feat_stride, self._anchors, self._num_anchors],
                                              [tf.float32, tf.float32], name="proposal_top")

            rois.set_shape([cfg.TEST.RPN_TOP_N, 5])
            rpn_scores.set_shape([cfg.TEST.RPN_TOP_N, 1])

        return rois, rpn_scores

    def _roi_pool_layer(self, bootom, rois, name):
        with tf.variable_scope(name) as scope:
            return tf.image.roi_pooling(bootom, rois,
                                        pooled_height=cfg.POOLING_SIZE,
                                        pooled_width=cfg.POOLING_SIZE,
                                        spatial_scale=1. / 16.)[0]

    def _crop_pool_layer(self, bottom, rois, name):
        with tf.variable_scope(name) as scope:
            batch_ids = tf.squeeze(tf.slice(rois, [0, 0], [-1, 1], name="batch_id"), [1])
            # Get the normalized coordinates of bounding boxes
            bottom_shape = tf.shape(bottom)
            height = (tf.to_float(bottom_shape[1]) - 1.) * np.float32(self._feat_stride[0])
            width = (tf.to_float(bottom_shape[2]) - 1.) * np.float32(self._feat_stride[0])
            x1 = tf.slice(rois, [0, 1], [-1, 1], name="x1") / width
            y1 = tf.slice(rois, [0, 2], [-1, 1], name="y1") / height
            x2 = tf.slice(rois, [0, 3], [-1, 1], name="x2") / width
            y2 = tf.slice(rois, [0, 4], [-1, 1], name="y2") / height
            # Won't be back-propagated to rois anyway, but to save time
            bboxes = tf.stop_gradient(tf.concat([y1, x1, y2, x2], axis=1))
            pre_pool_size = cfg.POOLING_SIZE * 2
            crops = tf.image.crop_and_resize(bottom, bboxes, tf.to_int32(batch_ids), [pre_pool_size, pre_pool_size],
                                             name="crops")

        return slim.max_pool2d(crops, [2, 2], padding='SAME')

    def _dropout_layer(self, bottom, name, ratio=0.5):
        return tf.nn.dropout(bottom, ratio, name=name)

    def _anchor_target_layer(self, rpn_cls_score, name):
        with tf.variable_scope(name) as scope:
            rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights = tf.py_func(
                anchor_target_layer,
                [rpn_cls_score, self._gt_boxes, self._im_info, self._feat_stride, self._anchors, self._num_anchors],
                [tf.float32, tf.float32, tf.float32, tf.float32],
                name="anchor_target")

            rpn_labels.set_shape([1, 1, None, None])
            rpn_bbox_targets.set_shape([1, None, None, self._num_anchors * 4])
            rpn_bbox_inside_weights.set_shape([1, None, None, self._num_anchors * 4])
            rpn_bbox_outside_weights.set_shape([1, None, None, self._num_anchors * 4])

            rpn_labels = tf.to_int32(rpn_labels, name="to_int32")
            self._anchor_targets['rpn_labels'] = rpn_labels
            self._anchor_targets['rpn_bbox_targets'] = rpn_bbox_targets
            self._anchor_targets['rpn_bbox_inside_weights'] = rpn_bbox_inside_weights
            self._anchor_targets['rpn_bbox_outside_weights'] = rpn_bbox_outside_weights

            self._score_summaries.update(self._anchor_targets)

        return rpn_labels

    def _proposal_target_layer(self, rois, roi_scores, name):
        with tf.variable_scope(name) as scope:
            rois, roi_scores, labels, bbox_targets, bbox_inside_weights, bbox_outside_weights = tf.py_func(
                proposal_target_layer,
                [rois, roi_scores, self._gt_boxes, self._num_classes],
                [tf.float32, tf.float32, tf.float32, tf.float32, tf.float32, tf.float32],
                name="proposal_target")

            rois.set_shape([cfg.TRAIN.BATCH_SIZE, 5])
            roi_scores.set_shape([cfg.TRAIN.BATCH_SIZE])
            labels.set_shape([cfg.TRAIN.BATCH_SIZE, 1])
            bbox_targets.set_shape([cfg.TRAIN.BATCH_SIZE, self._num_classes * 4])
            bbox_inside_weights.set_shape([cfg.TRAIN.BATCH_SIZE, self._num_classes * 4])
            bbox_outside_weights.set_shape([cfg.TRAIN.BATCH_SIZE, self._num_classes * 4])

            self._proposal_targets['rois'] = rois
            self._proposal_targets['labels'] = tf.to_int32(labels, name="to_int32")
            self._proposal_targets['bbox_targets'] = bbox_targets
            self._proposal_targets['bbox_inside_weights'] = bbox_inside_weights
            self._proposal_targets['bbox_outside_weights'] = bbox_outside_weights

            self._score_summaries.update(self._proposal_targets)

            return rois, roi_scores

    # only useful during testing mode
    def test_image(self, sess, image, im_info):
        feed_dict = {self._image: image,
                     self._im_info: im_info}

        cls_score, cls_prob, bbox_pred, rois = sess.run([self._predictions["cls_score"],
                                                         self._predictions['cls_prob'],
                                                         self._predictions['bbox_pred'],
                                                         self._predictions['rois']],
                                                        feed_dict=feed_dict)
        return cls_score, cls_prob, bbox_pred, rois


from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import tensorflow as tf
import numpy as np
import os
from datetime import datetime
import pickle
from scipy import misc
import facenet
import detect_face
from PyQt5 import QtCore, QtWidgets

img_size = 160

def align_mtcnn(img, pnet, rnet, onet, minsize):
    threshold = [0.6, 0.7, 0.9]  # three steps's threshold
    factor = 0.709  # scale factor

    if img.ndim == 2:
        img = facenet.to_rgb(img)
    img = img[:, :, 0:3]

    bounding_boxes, _ = detect_face.detect_face(img, minsize, pnet, rnet, onet, threshold, factor)
    num_of_faces = bounding_boxes.shape[0]
    aligned_list = []
    for face_pos in bounding_boxes:
        face_pos = np.rint(face_pos)
        aligned_list.append(face_pos.astype(int))

    return aligned_list

def restore_mtcnn_model():
    gpu_memory_fraction = 1.0
    with tf.Graph().as_default():
        gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=gpu_memory_fraction)
        sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options, log_device_placement=False))
        with sess.as_default():
            pnet, rnet, onet = detect_face.create_mtcnn(sess, 'models/mtcnn/')
    return pnet, rnet, onet

def restore_facenet_model(model_path):
    # load the model
    print('Loading feature extraction model')
    facenet.load_model(model_path)

def restore_classifier(classifier_path):
    # load the classifier
    print('Loading face classifier')
    if os.path.isdir(classifier_path):
        classifier = []
        class_names = []
        for root, dirs, files in os.walk(classifier_path):
            for file in files:
                with open(os.path.join(root, file), 'rb') as infile:
                    tmp1, tmp2 = pickle.load(infile)
                    classifier.append(tmp1)
                    class_names.append(tmp2)

    if os.path.isfile(classifier_path):
        with open(classifier_path, 'rb') as infile:
            classifier, class_names = pickle.load(infile)

    return classifier, class_names

def get_face_in_frame(frame, aligned_list):
    images = np.zeros((len(aligned_list), img_size, img_size, 3))
    i = 0
    for face_pos in aligned_list:
        if face_pos[0] < 0 or face_pos[1] < 0:
            continue
        else:
            img = frame[face_pos[1]:face_pos[3], face_pos[0]:face_pos[2], ]
            if img.ndim == 2:
                img = facenet.to_rgb(img)
            img = misc.imresize(img, (img_size, img_size), interp='bilinear')
            img = facenet.prewhiten(img)
            img = facenet.crop(img, False, img_size)
            images[i, :, :, :] = img
            i += 1
    return images

def identify_face(sess, frame, aligned_list, classifier, class_names, window):
    # get input and output tensors
    images_placeholder = tf.get_default_graph().get_tensor_by_name("input:0")
    embeddings = tf.get_default_graph().get_tensor_by_name("embeddings:0")
    phase_train_placeholder = tf.get_default_graph().get_tensor_by_name("phase_train:0")
    embedding_size = embeddings.get_shape()[1]

    # run forward pass to calculate embeddings
    emb_array = np.zeros((len(aligned_list), embedding_size))
    faces = get_face_in_frame(frame, aligned_list)
    feed_dict = {images_placeholder: faces, phase_train_placeholder: False}
    emb_array = sess.run(embeddings, feed_dict=feed_dict)

    # classify faces
    predict = classifier.predict_proba(emb_array)
    best_class_indices = np.argmax(predict, axis=1)
    best_class_prob = predict[np.arange(len(best_class_indices)), best_class_indices]

    name_list = []
    for i in range(len(best_class_indices)):
        item = QtWidgets.QListWidgetItem()
        item.setData(QtCore.Qt.UserRole, class_names[best_class_indices[i]])

        #print("d---"+item.data(QtCore.Qt.UserRole))
        if class_names[best_class_indices[i]] == 'others':
            print('<ERROR> %s: %.3f' % (class_names[best_class_indices[i]], best_class_prob[i]))
        #elif best_class_prob[i] < 0.6 :
            #name_list.append("")
            #window.listLow.addItem('{:0.2f}  |  {}  |  {}'.format(best_class_prob[i], datetime.now().strftime('%H:%M:%S'), class_names[best_class_indices[i]]))
            #print('%s: %.3f' % (class_names[best_class_indices[i]], best_class_prob[i]))
        else: # adds name if confidence lvl of the recognition is 0.6 and above
            if best_class_prob[i] < 0.6:
                item.setText('{:0.2f}  |  {}  |  {}'.format(best_class_prob[i], datetime.now().strftime('%H:%M:%S'), class_names[best_class_indices[i]]))
                window.listLow.addItem(item)
            else:
                item.setText('{:0.2f}  |  {}  |  {}'.format(best_class_prob[i], datetime.now().strftime('%H:%M:%S'), class_names[best_class_indices[i]]))
                window.listHigh.addItem(item)
            name_list.append(class_names[best_class_indices[i]])
            #print(datetime.now().strftime('%Y-%m-%d %H:%M:%S')) # print current system date & time
        #else: # else adds an empty string
            #name_list.append("")
    #print('-----')

    return [name_list,best_class_prob]


import pygame
import sys
import numpy as np
import threading
from time import time
from io import BytesIO, StringIO
from modules.adbmirror.capclient import CapClient
from modules.adbmirror.touchclient import TouchClient
from PIL import Image

class Main():
    def __init__(self, size, orig, path):
        #assert len(sys.argv) == 4
        self.size = list(map(int, size.split("x")))     # list(map(int, sys.argv[1].split("x")))
        orig = list(map(int, orig.split("x")))          # list(map(int, sys.argv[2].split("x")))
        self.orig = orig[0], orig[1]
        self.path = path

        self.cap = CapClient(self)
        self.cap.start()
        self.touch = TouchClient(self)
        self.touch.start()

        self.mouse_down = False
        self.mouse_pos = (0, 0)

        self.scale = self.orig[0] / float(self.size[0])
        self.ratio = self.orig[1] / float(self.orig[0])
        print('Scale, ratio:', self.scale, self.ratio)

        self.sizep = self.size[0], int(self.orig[1] / self.scale)
        self.sizel = int(self.orig[1] / self.scale), self.size[0]
        print('Raw image size l/p:', self.sizel, self.sizep)

        self.rotation = 0
        self.calc_scale()

        pygame.init()
        pygame.font.init()
        self.screen = pygame.display.set_mode(self.size)

    def calc_scale(self):
        self.landscape = self.rotation in [90, 270]
        max_w = self.size[0]
        if self.landscape:
            x = 0
            w = max_w
            h = w / self.ratio
            y = (self.size[1] - h) / 2
        else:
            y = 0
            h = self.size[1]
            w = h / self.ratio
            x = (self.size[0] - w) / 2

        self.proj = list(map(int, [x, y, w, h]))
        print('Projection:', self.proj)
        self.frame_update = True

    def blit_center(self, dst, src, rect):
        x = rect[0] - int((src.get_width() / 2) - (rect[2] / 2))
        y = rect[1] - int((src.get_height() / 2) - (rect[3] / 2))
        dst.blit(src, (x, y))

    def exit(self):
        self.running = False
        self.cap.write(["end"])
        self.touch.write(["end"])

    def save_image(self, img, fn):
        img.save(fn,"PNG")

    def events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.exit()

            if hasattr(event, "pos"):
                ix, iy = event.pos
                fx = min(max(0, (ix - self.proj[0]) / float(self.proj[2])), 1)
                fy = min(max(0, (iy - self.proj[1]) / float(self.proj[3])), 1)
                if self.rotation == 0:
                    x = fx
                    y = fy
                if self.rotation == 90:
                    x = 1.0 - fy
                    y = fx
                if self.rotation == 180:
                    x = 1.0 - fx
                    y = 1.0 - fy
                if self.rotation == 270:
                    x = fy
                    y = 1.0 - fx
                pygame.display.set_caption(str(x) + '  -  ' + str(y))

            if hasattr(event, "button"):
                if event.button is not 1:
                    continue
                if event.type == pygame.MOUSEBUTTONDOWN:
                    self.touch.write(["down", x, y])
                    self.mouse_down = True
                if event.type == pygame.MOUSEBUTTONUP:
                    self.touch.write(["up"])
                    self.mouse_down = False

            if event.type == pygame.MOUSEMOTION:
                if self.mouse_down:
                    self.touch.write(["move", x, y])
                    self.mouse_pos = (x, y)

    def run(self):
        self.running = True
        self.screen_update = True
        self.frame_update = False
        self.frame_cache = pygame.Surface(self.size)
        last_frame = None

        while self.running:
            self.events()

            msgs = self.cap.read()
            msgl = len(msgs)
            if msgl:
                msg = msgs[msgl - 1]
                cmd = msg[0]
                if cmd == "data":
                    last_frame = pygame.image.load(BytesIO(msg[1]))
                    self.frame_update = True

            if self.frame_update:
                self.frame_update = False
                if last_frame is not None:
                    if self.landscape:
                        a = last_frame.subsurface(pygame.Rect((0,0), self.sizel))
                    else:
                        a = last_frame.subsurface(pygame.Rect((0,0), self.sizep))
                    aw, ah = a.get_size()
                    if aw != self.proj[2] or ah != self.proj[3]:
                        self.frame_cache = pygame.transform.smoothscale(a, (self.proj[2], self.proj[3]))
                    else:
                        self.frame_cache = a.copy()
                self.screen_update = True

            if self.screen_update:
                self.screen.fill((0, 0, 0))
                self.screen_update = False
                self.screen.blit(self.frame_cache, (self.proj[0], self.proj[1]))
                pygame.display.update()

#a = Main()
#a.run()


# -*- coding: utf-8 -*-

import os, sys
import numpy as np
import mahotas
import itertools
from skimage import filters
from skimage import img_as_ubyte
from skimage import morphology, measure

from scipy.ndimage import binary_fill_holes
import cv2
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.functional as F

from pycontour import img, cv2_transform

def dice_loss(pred, target, smooth = 1.):
    pred = pred.contiguous()
    target = target.contiguous()

    intersection = (pred * target).sum(dim=2).sum(dim=2)
    dice = ((2. * intersection + smooth) / (pred.sum(dim=2).sum(dim=2) + target.sum(dim=2).sum(dim=2) + smooth))

    return dice.mean(dim=0)

def calc_loss(pred, target, metrics, bce_weight=0.2):
    bce = F.binary_cross_entropy_with_logits(pred, target)

    pred = torch.sigmoid(pred)
    dice = dice_loss(pred, target)

    loss = bce * bce_weight + (1.0 - dice.mean()) * (1.0 - bce_weight)

    metrics['bce'] += bce.data.cpu().numpy() * target.size(0)
    metrics['dice_mean'] += dice.mean().data.item() * target.size(0)
    metrics['dice_single'] += dice[0].data.item() * target.size(0)
    metrics['dice_overlap'] += dice[1].data.item() * target.size(0)
    metrics['loss'] += loss.data.cpu().numpy() * target.size(0)

    return loss

def print_metrics(metrics, epoch_samples, phase):
    outputs = []
    for k in metrics.keys():
        outputs.append("{}: {:.4f}".format(k, metrics[k] / epoch_samples))
    print("{}: {}".format(phase, ", ".join(outputs)))

def mask2color(mask):
    colors = np.asarray([(201, 58, 64), (242, 207, 1), (0, 152, 75), (101, 172, 228),(56, 34, 132), (160, 194, 56),
                         (0, 0, 117), (128, 128, 0), (191, 239, 69), (145, 30, 180)])
    color_img = np.zeros((mask.shape[0], mask.shape[1], 3), dtype=np.float32)

    color_img[mask==255] = colors[0]
    for ic in np.arange(1, len(colors)):
        color_img[mask==ic] = colors[ic]

    return color_img.astype(np.uint8)

def refine_seg(seg):
    fill_seg = binary_fill_holes(seg)
    clean_seg = morphology.remove_small_objects(fill_seg, min_size=32)

    return clean_seg

def remove_weak_connection(bin_img):
    final_img = np.zeros_like(bin_img, dtype=bool)
    all_labels = measure.label(bin_img)
    region_num_ttl = len(np.unique(all_labels))

    for r in np.arange(1, region_num_ttl):
        cur_region = (all_labels == r)
        open_region = morphology.opening(cur_region, morphology.square(3))
        # open_region = morphology.opening(cur_region, morphology.square(2))
        region_num = len(np.unique(measure.label(open_region))) - 1
        if region_num > 1:
            final_img += open_region
            diff = cur_region ^ open_region
            diff_labels = measure.label(diff)
            diff_num = len(np.unique(diff_labels))
            for d in np.arange(1, diff_num):
                cur_diff = diff_labels == d
                diff_add = open_region + cur_diff
                cur_region_num = len(np.unique(measure.label(diff_add))) - 1
                if  cur_region_num == region_num:
                    final_img += cur_diff
        else:
            final_img += cur_region
    return final_img

def refine_pred(single, overlap):
    combine = np.zeros_like(single, dtype=np.uint8)

    # Remove weak connnection in single prediction
    single = remove_weak_connection(single)
    single = morphology.remove_small_objects(single, min_size=32)

    combine = measure.label(single)
    overlap = binary_fill_holes(overlap)
    combine[overlap==True] = 255

    return combine

def check_single_region(combine):
    bin = combine > 0
    label_num = len(np.unique(measure.label(bin))) - 1

    single_flag = label_num == 1
    return single_flag

def check_two_chromosomes(ch1, ch2):
    flag1 = check_single_region(ch1)
    flag2 = check_single_region(ch2)

    if flag1 == False or flag2 == False:
        return False

    return True

def assign_combine(combine):
    all_labels = measure.label(combine)
    single_num = len(np.unique(all_labels)) - 2
    overlap = combine == 255
    assignments = []

    if single_num == 2:
        ch1 = (combine == 1) + overlap
        ch2 = (combine == 2) + overlap
        if check_two_chromosomes(ch1, ch2) == False:
            return None
        _, cnts1, _ = cv2.findContours(img_as_ubyte(ch1), mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)
        _, cnts2, _ = cv2.findContours(img_as_ubyte(ch2), mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)
        if len(cnts1) != 1 or len(cnts2) != 1:
            return None
        else:
            assignments.append([cnts1[0], cnts2[0]])
    if single_num == 3:
        labels = np.arange(1, single_num+1)
        for label in labels:
            ch1, ch2 = overlap.copy(), overlap.copy()
            ch1 += (combine == label)
            rest_labels = [x for x in labels if x != label]
            for rest in rest_labels:
                ch2 += (combine == rest)
            if check_two_chromosomes(ch1, ch2) == False:
                return None
            _, cnts1, _ = cv2.findContours(img_as_ubyte(ch1), mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)
            _, cnts2, _ = cv2.findContours(img_as_ubyte(ch2), mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)
            if len(cnts1) != 1 or len(cnts2) != 1:
                return None
            else:
                assignments.append([cnts1[0], cnts2[0]])
    if single_num == 4:
        all_possibles = list(itertools.combinations(np.arange(1, 5).tolist(), 2))
        half_len = int(len(all_possibles) / 2)
        for i in np.arange(half_len):
            labels1 = all_possibles[i]
            labels2 = all_possibles[len(all_possibles) - i - 1]
            ch1, ch2 = overlap.copy(), overlap.copy()
            for ele in labels1:
                ch1 += (combine == ele)
            for ele in labels2:
                ch2 += (combine == ele)
            if check_two_chromosomes(ch1, ch2) == False:
                return None
            _, cnts1, _ = cv2.findContours(img_as_ubyte(ch1), mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)
            _, cnts2, _ = cv2.findContours(img_as_ubyte(ch2), mode=cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)
            if len(cnts1) != 1 or len(cnts2) != 1:
                return None
            else:
                assignments.append([cnts1[0], cnts2[0]])
    return assignments

def cal_assignment_fea(assignment, desc):
    cnt1, cnt2 = assignment[0], assignment[1]
    arr1 = cv2_transform.cv_cnt_to_np_arr(cnt1)
    arr2 = cv2_transform.cv_cnt_to_np_arr(cnt2)
    mask1 = img.build_cnt_mask(arr1)
    mask2 = img.build_cnt_mask(arr2)
    fea1 = desc.describe(mask1)
    fea2 = desc.describe(mask2)

    return fea1, fea2

class ZernikeMoments:
    def __init__(self, radius):
        # store the size of the radius that will be
        # used when computing moments
        self.radius = radius

    def describe(self, image):
        # return the Zernike moments for the image
        return mahotas.features.zernike_moments(image, self.radius)


from glfwpy.glfw import (AUTO_POLL_EVENTS,
    OPENED, OPENGL_CORE_PROFILE,
    OPENGL_FORWARD_COMPAT, OPENGL_PROFILE,
    OPENGL_VERSION_MAJOR,
    OPENGL_VERSION_MINOR, WINDOW,
    Enable, GetWindowParam, Init,
    OpenWindow, OpenWindowHint,
    SetKeyCallback, SetWindowTitle,
    SwapBuffers, Terminate)
import numpy as np
from OpenGL.arrays import ArrayDatatype
from OpenGL.GL import (GL_ARRAY_BUFFER,
    GL_COLOR_BUFFER_BIT, GL_COMPILE_STATUS,
    GL_FALSE, GL_FLOAT, GL_FRAGMENT_SHADER,
    GL_LINK_STATUS, GL_RENDERER,
    GL_SHADING_LANGUAGE_VERSION,
    GL_STATIC_DRAW, GL_TEXTURE_2D, GL_TRIANGLES,
    GL_TRUE, GL_UNPACK_ALIGNMENT, GL_VENDOR, GL_VERSION,
    GL_VERTEX_SHADER, GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T,
    GL_CLAMP, GL_TEXTURE_MAG_FILTER, GL_TEXTURE_MIN_FILTER,
    GL_LINEAR, GL_RGB, GL_RGBA, GL_UNSIGNED_BYTE, GL_TEXTURE0,
    GL_REPEAT,
    glActiveTexture, glAttachShader,
    glBindBuffer, glBindTexture, glBindVertexArray,
    glBufferData, glClear, glClearColor,
    glCompileShader, glCreateProgram,
    glCreateShader, glDeleteProgram,
    glDeleteShader, glDrawArrays,
    glEnableVertexAttribArray, glGenBuffers, glGenTextures,
    glGenVertexArrays, glGetAttribLocation,
    glGetProgramInfoLog, glGetProgramiv,
    glGetShaderInfoLog, glGetShaderiv, glGetString,
    glGetUniformLocation, glLinkProgram, glPixelStorei,
    glShaderSource, glTexImage2D, glTexParameteri, glUniform1i, glUseProgram,
    glVertexAttribPointer)
import sys
import Image
import numpy

vertex = """
#version 330
in vec3 vin_position;
in vec2 vin_uv;
out vec2 vout_uv;

void main(void)
{
    vout_uv = vin_uv;
    gl_Position = vec4(vin_position, 1.0);
}
"""

fragment = """
#version 330
in vec2 vout_uv;
out vec4 fout_color;
uniform sampler2D texture_sampler;

void main(void)
{
    fout_color = vec4(texture(texture_sampler, vout_uv).rgb, 1);
}
"""

vertex_data = np.array([0.75, 0.75, 0.0,
    0.75, -0.75, 0.0,
    -0.75, -0.75, 0.0,
    0.75, 0.75, 0.0,
    -0.75, -0.75, 0.0,
    -0.75, 0.75, 0.0],
    dtype=np.float32)

uv_data = np.array([1, 0,
                    1, 1,
                    0, 1,
                    1, 0,
                    0, 1,
                    0, 0], dtype=np.float32)

class ShaderProgram(object):
    """ Helper class for using GLSL shader programs
    """
    def __init__(self, vertex, fragment):
        """
        Parameters
        ----------
        vertex : str
            String containing shader source code for the vertex
            shader
        fragment : str
            String containing shader source code for the fragment
            shader

        """
        self.program_id = glCreateProgram()
        vs_id = self.add_shader(vertex, GL_VERTEX_SHADER)
        frag_id = self.add_shader(fragment, GL_FRAGMENT_SHADER)

        glAttachShader(self.program_id, vs_id)
        glAttachShader(self.program_id, frag_id)
        glLinkProgram(self.program_id)

        if glGetProgramiv(self.program_id, GL_LINK_STATUS) != GL_TRUE:
            info = glGetProgramInfoLog(self.program_id)
            glDeleteProgram(self.program_id)
            glDeleteShader(vs_id)
            glDeleteShader(frag_id)
            raise RuntimeError('Error linking program: %s' % (info))
        glDeleteShader(vs_id)
        glDeleteShader(frag_id)

    def add_shader(self, source, shader_type):
        """ Helper function for compiling a GLSL shader

        Parameters
        ----------
        source : str
            String containing shader source code

        shader_type : valid OpenGL shader type
            Type of shader to compile

        Returns
        -------
        value : int
            Identifier for shader if compilation is successful

        """
        try:
            shader_id = glCreateShader(shader_type)
            glShaderSource(shader_id, source)
            glCompileShader(shader_id)
            if glGetShaderiv(shader_id, GL_COMPILE_STATUS) != GL_TRUE:
                info = glGetShaderInfoLog(shader_id)
                raise RuntimeError('Shader compilation failed: %s' % (info))
            return shader_id
        except:
            glDeleteShader(shader_id)
            raise

    def uniform_location(self, name):
        """ Helper function to get location of an
        OpenGL uniform variable

        Parameters
        ----------
        name : str
            Name of the variable for which location
            is to be returned

        Returns
        -------
        value : int
            Integer describing location

        """
        return glGetUniformLocation(self.program_id, name)

    def attribute_location(self, name):
        """ Helper function to get location of an
        OpenGL attribute variable

        Parameters
        ----------
        name : str
            Name of the variable for which location is
            to be returned

        Returns
        -------
        value : int
            Integer describing location

        """
        return glGetAttribLocation(self.program_id, name)

def key_callback(key, action):
    """ Sample keyboard callback function """
    print 'Key: %s Action: %s pressed' % (key, action)

if __name__ == "__main__":
    if not Init():
        print 'GLFW initialization failed'
        sys.exit(-1)

    # These Window hints are used to specify
    # which opengl version to use and other details
    # for the opengl context that will be created
    OpenWindowHint(OPENGL_VERSION_MAJOR, 3)
    OpenWindowHint(OPENGL_VERSION_MINOR, 2)
    OpenWindowHint(OPENGL_PROFILE, OPENGL_CORE_PROFILE)
    OpenWindowHint(OPENGL_FORWARD_COMPAT, GL_TRUE)

    if not OpenWindow(600, 600, 0, 0, 0, 0, 32, 0, WINDOW):
        print "OpenWindow failed"
        Terminate()
        sys.exit(-1)

    # Every time a key on the keyboard is clicked
    # call our callback function
    SetKeyCallback(key_callback)

    SetWindowTitle("Texturing example")
    Enable(AUTO_POLL_EVENTS)

    # If everything went well the following calls
    # will display the version of opengl being used
    print 'Vendor: %s' % (glGetString(GL_VENDOR))
    print 'Opengl version: %s' % (glGetString(GL_VERSION))
    print 'GLSL Version: %s' % (glGetString(GL_SHADING_LANGUAGE_VERSION))
    print 'Renderer: %s' % (glGetString(GL_RENDERER))

    glClearColor(0.95, 1.0, 0.95, 0)

    # Lets compile our shaders since the use of shaders is now
    # mandatory. We need at least a vertex and fragment shader
    # begore we can draw anything
    program = ShaderProgram(fragment=fragment, vertex=vertex)

    # Lets create a VAO and bind it
    # Think of VAO's as object that encapsulate buffer state
    # Using a VAO enables you to cut down on calls in your draw
    # loop which generally makes things run faster
    vao_id = glGenVertexArrays(1)
    glBindVertexArray(vao_id)

    # Lets create our Vertex Buffer objects - these are the buffers
    # that will contain our per vertex data
    vbo_id = glGenBuffers(2)

    # Bind a buffer before we can use it
    glBindBuffer(GL_ARRAY_BUFFER, vbo_id[0])

    # Now go ahead and fill this bound buffer with some data
    glBufferData(GL_ARRAY_BUFFER,
        ArrayDatatype.arrayByteCount(vertex_data),
        vertex_data, GL_STATIC_DRAW)

    # Now specify how the shader program will be receiving this data
    # In this case the data from this buffer will be
    # available in the shader as the vin_position vertex attribute
    glVertexAttribPointer(program.attribute_location('vin_position'),
        3, GL_FLOAT, GL_FALSE, 0, None)

    # Turn on this vertex attribute in the shader
    glEnableVertexAttribArray(0)

    # Now do the same for the other vertex buffer
    glBindBuffer(GL_ARRAY_BUFFER, vbo_id[1])
    glBufferData(GL_ARRAY_BUFFER,
        ArrayDatatype.arrayByteCount(uv_data),
        uv_data, GL_STATIC_DRAW)
    glVertexAttribPointer(program.attribute_location('vin_uv'),
        2, GL_FLOAT, GL_FALSE, 0, None)
    glEnableVertexAttribArray(1)

    # Lets unbind our vbo and vao state
    # We will bind these again in the draw loop
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    glBindVertexArray(0)

    running = True
    img = Image.open(sys.path[0] + '/../art/enthought.png')
    img_data = numpy.array(list(img.getdata()), numpy.int8)

    texture_id = glGenTextures(1)
    glPixelStorei(GL_UNPACK_ALIGNMENT, 4)
    glBindTexture(GL_TEXTURE_2D, texture_id)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, img.size[0], img.size[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data)

    while running:
        glClear(GL_COLOR_BUFFER_BIT)

        # Specify shader to be used
        glUseProgram(program.program_id)

        glActiveTexture(GL_TEXTURE0)
        glBindTexture(GL_TEXTURE_2D, texture_id)
        sampler_loc = program.attribute_location('texture_sampler')
        glUniform1i(sampler_loc, 0)

        # Bind VAO - this will automatically
        # bind all the vbo's saving us a bunch
        # of calls
        glBindVertexArray(vao_id)

        # Modern GL makes the draw call really simple
        # All the complexity has been pushed elsewhere
        glDrawArrays(GL_TRIANGLES, 0, 6)

        # Lets unbind the shader and vertex array state
        glUseProgram(0)
        glBindVertexArray(0)

        # Now lets show our master piece on the screen
        SwapBuffers()

        # If the user has closed the window in anger
        # then terminate this program
        running = running and GetWindowParam(OPENED)