#!/usr/bin/env python

"""Test all the .py files in the examples/ dir.

Each module should have its own test case.
"""

from __future__ import print_function

import os.path
import shlex
import subprocess
import sys
import unittest

class _PyTestCase(unittest.TestCase):
    """Helper class for testing .py scripts."""

    def runpy(self, py_file, stdin=None):
        """Run python script and return output.

        This is sort of like check_output, which was introduced in python 2.7.
        """
        cmd = [sys.executable]
        cmd += shlex.split(py_file)
        stdin_val = subprocess.PIPE if stdin else None
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                             stderr=subprocess.STDOUT, stdin=stdin_val)
        output, _ = p.communicate(input=stdin)
        return output

class PyToChplTests(_PyTestCase):

    def test_arrays_init(self):
        self.assertEqual(
            """[ 1.  2.  3.  4.  5.  6.  7.  8.  9. 10.]
""",
            self.runpy('arrays.init.py'))

    def test_arrays_promo(self):
        self.assertEqual(
            """[1.         1.41421356 1.73205081 2.         2.23606798 2.44948974
 2.64575131 2.82842712 3.         3.16227766]
[3.0, 6.0, 9.0, 12.0, 15.0, 18.0, 21.0, 24.0, 27.0, 30.0]
[6.0, 12.0, 18.0, 24.0, 30.0, 36.0, 42.0, 48.0, 54.0, 60.0]
""",
            self.runpy('arrays.promo.py'))

    def test_arrays(self):
        self.assertEqual(
            """0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
(0,0) = 0.000000
(0,1) = 0.000000
(0,2) = 0.000000
(0,3) = 0.000000
(0,4) = 0.000000
(0,5) = 0.000000
(0,6) = 0.000000
(0,7) = 0.000000
(0,8) = 0.000000
(0,9) = 0.000000
(1,0) = 0.000000
(1,1) = 0.000000
(1,2) = 0.000000
(1,3) = 0.000000
(1,4) = 0.000000
(1,5) = 0.000000
(1,6) = 0.000000
(1,7) = 0.000000
(1,8) = 0.000000
(1,9) = 0.000000
(2,0) = 0.000000
(2,1) = 0.000000
(2,2) = 0.000000
(2,3) = 0.000000
(2,4) = 0.000000
(2,5) = 0.000000
(2,6) = 0.000000
(2,7) = 0.000000
(2,8) = 0.000000
(2,9) = 0.000000
(3,0) = 0.000000
(3,1) = 0.000000
(3,2) = 0.000000
(3,3) = 0.000000
(3,4) = 0.000000
(3,5) = 0.000000
(3,6) = 0.000000
(3,7) = 0.000000
(3,8) = 0.000000
(3,9) = 0.000000
(4,0) = 0.000000
(4,1) = 0.000000
(4,2) = 0.000000
(4,3) = 0.000000
(4,4) = 0.000000
(4,5) = 0.000000
(4,6) = 0.000000
(4,7) = 0.000000
(4,8) = 0.000000
(4,9) = 0.000000
(5,0) = 0.000000
(5,1) = 0.000000
(5,2) = 0.000000
(5,3) = 0.000000
(5,4) = 0.000000
(5,5) = 0.000000
(5,6) = 0.000000
(5,7) = 0.000000
(5,8) = 0.000000
(5,9) = 0.000000
(6,0) = 0.000000
(6,1) = 0.000000
(6,2) = 0.000000
(6,3) = 0.000000
(6,4) = 0.000000
(6,5) = 0.000000
(6,6) = 0.000000
(6,7) = 0.000000
(6,8) = 0.000000
(6,9) = 0.000000
(7,0) = 0.000000
(7,1) = 0.000000
(7,2) = 0.000000
(7,3) = 0.000000
(7,4) = 0.000000
(7,5) = 0.000000
(7,6) = 0.000000
(7,7) = 0.000000
(7,8) = 0.000000
(7,9) = 0.000000
(8,0) = 0.000000
(8,1) = 0.000000
(8,2) = 0.000000
(8,3) = 0.000000
(8,4) = 0.000000
(8,5) = 0.000000
(8,6) = 0.000000
(8,7) = 0.000000
(8,8) = 0.000000
(8,9) = 0.000000
(9,0) = 0.000000
(9,1) = 0.000000
(9,2) = 0.000000
(9,3) = 0.000000
(9,4) = 0.000000
(9,5) = 0.000000
(9,6) = 0.000000
(9,7) = 0.000000
(9,8) = 0.000000
(9,9) = 0.000000
""",
            self.runpy('arrays.py'))

    def test_arrays_reduc(self):
        self.assertEqual(
            """55.0
[ 1.  3.  6. 10. 15. 21. 28. 36. 45. 55.]
""",
            self.runpy('arrays.reduc.py'))

    def test_arrays_whole(self):
        """Check that 100 random lines are printed."""
        self.assertEqual(
            100, len(self.runpy('arrays_whole.py').splitlines()))
        # Also spot check the results when seeded with 173...
        seed_173_results = self.runpy(
            '-c "import numpy; numpy.random.seed(173); import arrays_whole"').splitlines()
        self.assertAlmostEqual(1.93667466205, eval(seed_173_results[0]))
        self.assertAlmostEqual(2.5402632511, eval(seed_173_results[42]))
        self.assertAlmostEqual(0.964120007336, eval(seed_173_results[-1]))

    def test_classes(self):
        self.assertEqual('Green\n', self.runpy('classes.py'))

    def test_comments(self):
        self.assertEqual('', self.runpy('comments.py'))

    def test_cond_if_green(self):
        self.assertEqual(
            """Which color is the traffic light?
You can cross the street now.
You can cross the street now.
You can cross the street now.
You can cross the street now.
""",
            self.runpy('cond.if.py', 'green\n'))

    def test_conf_if_red(self):
        self.assertEqual(
            """Which color is the traffic light?
Wait for the green light.
Do not cross!
""",
            self.runpy('cond.if.py', 'red\n'))

    def test_conf_if_yellow(self):
        self.assertEqual(
            """Which color is the traffic light?
Wait for the green light.
CAUTION!
CAUTION!
""",
            self.runpy('cond.if.py', 'yellow\n'))

    def test_cond_switch_green(self):
        self.assertEqual(
            """Which color is the traffic light?
You can cross the street now.
""",
            self.runpy('cond.switch.py', 'green\n'))

    def test_cond_switch_red(self):
        self.assertEqual(
            """Which color is the traffic light?
Do not cross!
""",
            self.runpy('cond.switch.py', 'red\n'))

    def test_cond_switch_yellow(self):
        self.assertEqual(
            """Which color is the traffic light?
CAUTION!
""",
            self.runpy('cond.switch.py', 'yellow\n'))

    def test_cond_switch_garbage(self):
        self.assertEqual(
            """Which color is the traffic light?
WARNING! Traffic-light is broken!
""",
            self.runpy('cond.switch.py', 'Hawt Garbage\n'))

    def test_console(self):
        self.assertEqual(
            """Hello, you.
Hello, you.
""",
            self.runpy('console.py'))

    def test_console_read__true_true(self):
        self.assertEqual(
            """The Answer to the ultimate question is?
That is True
What is the largest biological computer?
That is True
""",
            self.runpy('console.read.py', '42\nEarth\n'))

    def test_console_read__true_false(self):
        self.assertEqual(
            """The Answer to the ultimate question is?
That is True
What is the largest biological computer?
That is False
""",
            self.runpy('console.read.py', '42\nCray XC\n'))

    def test_console_read__false_true(self):
        self.assertEqual(
            """The Answer to the ultimate question is?
That is False
What is the largest biological computer?
That is True
""",
            self.runpy('console.read.py', '17\nEarth\n'))

    def test_console_read__false_false(self):
        self.assertEqual(
            """The Answer to the ultimate question is?
That is False
What is the largest biological computer?
That is False
""",
            self.runpy('console.read.py', '101010\nGoogel\n'))

    def test_dicts(self):
        self.assertEqual(
            """Key=0, Value=Green
Key=1, Value=Yellow
Key=2, Value=Red
""",
            self.runpy('dicts.py'))

    def test_division(self):
        self.assertEqual(
            """Result of 9.0 / 4 = 2.25
Result of 9 / 4.0 = 2.25
Result of 9 / 4 = 2
Result of 9.0 / 4.0 = 2.25
""",
            self.runpy('division.py'))

    def test_func_decl(self):
        import func_decl
        self.assertEqual('', self.runpy('func_decl.py'))
        self.assertEqual(10, func_decl.abs(10))
        self.assertEqual(10, func_decl.abs(-10))
        self.assertEqual(0, func_decl.abs(0))

    def test_hw(self):
        self.assertEqual('Hello, World!\n', self.runpy('hw.py'))

    def test_hw_main(self):
        self.assertEqual('Hello, World!\n', self.runpy('hw.main.py'))

    def test_literals(self):
        self.assertEqual('', self.runpy('literals.py'))
        import literals
        self.assertFalse(literals.bl)
        self.assertEqual(42, literals.ud)
        self.assertEqual(-42, literals.sd)
        self.assertEqual(42, literals.hd)
        self.assertEqual(42, literals.bd)
        self.assertEqual(42.0, literals.r)
        self.assertEqual('42', literals.s)
        self.assertEqual(1.0, literals.z.real)
        self.assertEqual(2.0, literals.z.imag)

    def test_loops_enumerate(self):
        self.assertEqual(
            """0 running
1 with
2 scissors
""",
            self.runpy('loops.enumerate.py'))

    def test_loops_for(self):
        self.assertEqual(
            """1
2
3
4
5
6
7
8
9
""",
            self.runpy('loops.for.py'))

    def test_loops_while(self):
        self.assertEqual(
            """1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10
""",
            self.runpy('loops.while.py'))

    def test_modules_import(self):
        self.assertEqual('', self.runpy('modules_import.py'))
        import modules_import
        self.assertEqual(modules_import.Random, modules_import.random.Random)

    def test_modules_main(self):
        self.assertEqual('', self.runpy('modules_main.py'))
        import modules_main
        self.assertEqual(None, modules_main.main())

    def test_numpy_random(self):
        self.assertEqual('', self.runpy('numpy_random.py'))

        import numpy
        numpy.random.seed(173)
        import numpy_random

        self.assertEqual(9, numpy_random.a.size)
        self.assertEqual((3, 3), numpy_random.a.shape)
        self.assertEqual(0.093257479552558031, numpy_random.a[0][0])
        self.assertEqual(0.60640382464326914, numpy_random.a[1][1])
        self.assertEqual(0.078458465901735885, numpy_random.a[2][2])

    def test_par_task_nosync(self):
        self.assertEqual('Hello, bob\n', self.runpy('par.task.nosync.py'))

    def test_par_task_pool(self):
        self.assertEqual('', self.runpy('-c "import par_task_pool; par_task_pool.do_work()"'))

    def test_par_task(self):
        self.assertEqual('Hello, bob\n', self.runpy('par.task.py'))

    def test_ranges_inf(self):
        self.assertEqual('', self.runpy('ranges_inf.py'))

    def test_ranges(self):
        self.assertEqual('', self.runpy('ranges.py'))
        import ranges
        self.assertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], list(ranges.r1))
        self.assertEqual([], list(ranges.r2))

    def test_ranges_short(self):
        self.assertEqual('', self.runpy('ranges_short.py'))
        import ranges_short
        self.assertEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], list(ranges_short.ns))

    def test_ranges_skip(self):
        self.assertEqual('', self.runpy('ranges_skip.py'))
        import ranges_skip
        self.assertEqual([1, 2, 3, 4, 5, 6, 7, 8, 9], list(ranges_skip.r1))
        self.assertEqual([1, 3, 5, 7, 9], list(ranges_skip.r2))
        self.assertEqual([9, 8, 7, 6, 5, 4, 3, 2, 1], list(ranges_skip.r3))
        self.assertEqual([9, 7, 5, 3, 1], list(ranges_skip.r4))

    def test_tuples(self):
        self.assertEqual(
            """coord = ('47.606165', '-122.332233')
Latitude = 47.606165 , Longitude = -122.332233
Latitude = 47.606165 , Longitude = -122.332233
""",
            self.runpy('tuples.py'))

    def test_vars_decl(self):
        self.assertEqual('', self.runpy('vars_decl.py'))
        import vars_decl
        self.assertEqual(42, vars_decl.answer)
        self.assertEqual(123.45, vars_decl.distance)
        self.assertEqual('Earth', vars_decl.computer)

if __name__ == '__main__':
    unittest.main()


import helper
data_dir = './data/Seinfeld_Scripts.txt'
from string import punctuation
from collections import Counter

def create_lookup_tables(text):
    """
    Create lookup tables for vocabulary
    :param text: The text of tv scripts split into a list of words
    :return: A tuple of dicts (vocab_to_int, int_to_vocab)
    """
    print("Entering create_lookup_tables: input text:", text[:20])

    ##########
    # remove all punctuation (handles input as a list of words, possibly with punctuation,
    # including wrapping quotes (single or double))
    #.....turns out don't need this section since punctuation will be removed prior to calling this func

    #words_only = ' '.join([c for c in text if c not in punctuation])
    #words_only = ''.join([c for c in words_only if c not in punctuation]) #2nd pass to get embedded quotes

    ##########
    # create a set of words, forcing the contents to be the unique words in all scripts

    unique_words = Counter()
    unique_words.update(text)
    #print(len(unique_words), " unique words. Most common words:\n", unique_words.most_common(10))

    ##########
    # create the dictionary of words to ints (0-based indexing) and its inverse

    vocab_to_int = {}
    i = 0
    for w, c in unique_words.most_common(len(unique_words)):
        vocab_to_int[w] = i
        #if i < 60:
        #    print(w, ": ", vocab_to_int[w])
        i += 1

    int_to_vocab = {}
    #print()
    for w in vocab_to_int:
        index = vocab_to_int[w]
        int_to_vocab[index] = w
        #if index < 10:
            #print(index, ": ", w)

    #print("vocab_to_int size = ", len(vocab_to_int), ", int_to_vocab size = ", len(int_to_vocab))

    # return tuple
    return (vocab_to_int, int_to_vocab)

def token_lookup():
    """
    Generate a dict to turn punctuation into a token.
    :return: Tokenized dictionary where the key is the punctuation and the value is the token
    """
    ##########
    # need to manually assign everything
    #print(punctuation)
    d = {
        '<': "<LESSTHAN>",    #these 2 need to be first since we use the to delimit everything. When punctuation
        '>': "<GREATERTHAN>", #is substituted for these tokens the symbols will be done in this order.
        '-': "<HYPHEN>",       #this needs to be next since it is used inside several others
        '\n': "<RETURN>",
        '!': "<EXCLAMATION>",
        '"': "<DOUBLE-QUOTE>",
        '#': "<POUND>",
        '$': "<DOLLAR>",
        '%': "<PERCENT>",
        '&': "<AMPERSAND>",
        "'": "<SINGLE-QUOTE>",
        '(': "<LEFT-PAREN>",
        ')': "<RIGHT-PAREN>",
        '*': "<STAR>",
        '+': "<PLUS>",
        ',': "<COMMA>",
        '.': "<PERIOD>",
        '/': "<SLASH>",
        ':': "<COLON>",
        ';': "<SEMICOLON>",
        '=': "<EQUALS>",
        '?': "<QUESTION>",
        '@': "<AT>",
        '[': "<LEFT-BRACKET>",
        '\\': "<BACKSLASH>",
        ']': "<RIGHT-BRACKET>",
        '^': "<CARET>",
        '_': "<UNDERSCORE>",
        '`': "<TICK>",
        '{': "<LEFT-BRACE>",
        '|': "<PIPE>",
        '}': "<RIGHT-BRACE>",
        '~': "<TILDE>",
        '\t': "<TAB>"
    }
    #print(d)

    return d

import os
import pickle
import torch

SPECIAL_WORDS = {'PADDING': '<PAD>'}

# # Check Point
# This is your first checkpoint. If you ever decide to come back to this notebook or have to restart the notebook, you can start from here. The preprocessed data has been saved to disk.

int_text, vocab_to_int, int_to_vocab, token_dict = helper.load_preprocess()

import torch

# Check for a GPU
train_on_gpu = torch.cuda.is_available()
if not train_on_gpu:
    print('No GPU found. Please use a GPU to train your neural network.')

# ## Input
# Let's start with the preprocessed input data. We'll use [TensorDataset](http://pytorch.org/docs/master/data.html#torch.utils.data.TensorDataset) to provide a known format to our dataset; in combination with [DataLoader](http://pytorch.org/docs/master/data.html#torch.utils.data.DataLoader), it will handle batching, shuffling, and other dataset iteration functions.
#
# You can create data with TensorDataset by passing in feature and target tensors. Then create a DataLoader as usual.
# ```
# data = TensorDataset(feature_tensors, target_tensors)
# data_loader = torch.utils.data.DataLoader(data,
#                                           batch_size=batch_size)
# ```
#
# ### Batching
# Implement the `batch_data` function to batch `words` data into chunks of size `batch_size` using the `TensorDataset` and `DataLoader` classes.
#
# >You can batch words using the DataLoader, but it will be up to you to create `feature_tensors` and `target_tensors` of the correct size and content for a given `sequence_length`.
#
# For example, say we have these as input:
# ```
# words = [1, 2, 3, 4, 5, 6, 7]
# sequence_length = 4
# ```
#
# Your first `feature_tensor` should contain the values:
# ```
# [1, 2, 3, 4]
# ```
# And the corresponding `target_tensor` should just be the next "word"/tokenized word value:
# ```
# 5
# ```
# This should continue with the second `feature_tensor`, `target_tensor` being:
# ```
# [2, 3, 4, 5]  # features
# 6             # target
# ```

# In[9]:

from torch.utils.data import TensorDataset, DataLoader
import numpy as np

def batch_data(words, sequence_length, batch_size):
    """
    Batch the neural network data using DataLoader
    :param words: The word ids of the TV scripts
    :param sequence_length: The sequence length of each batch
    :param batch_size: The size of each batch; the number of sequences in a batch
    :return: DataLoader with batched data
    """
    ##########
    # iterate through all the words to construct features & targets based on sequence length

    total_words = len(words)
    end = total_words - sequence_length - 1 #account for the target word at end
    fl = []
    tl = []
    for i, w in enumerate(words):
        #print("i = ", i, ", w =", w, end=' ')
        if i > end:
            break
        fl.append(words[i:i+sequence_length])
        tl.append(words[i+sequence_length])
        #print("fl = ", fl, ", tl = ", tl)

    features = torch.from_numpy(np.array(fl))
    targets = torch.from_numpy(np.array(tl))
    #print("features = ", features, "\ntargets = ", targets)

    ##########
    # batch up the data

    data = TensorDataset(features, targets)
    data_loader = torch.utils.data.DataLoader(data, batch_size=batch_size, shuffle=True)
    return data_loader

# ### Test your dataloader
#
# You'll have to modify this code to test a batching function, but it should look fairly similar.
#
# Below, we're generating some test text data and defining a dataloader using the function you defined, above. Then, we are getting some sample batch of inputs `sample_x` and targets `sample_y` from our dataloader.
#
# Your code should return something like the following (likely in a different order, if you shuffled your data):
#
# ```
# torch.Size([10, 5])
# tensor([[ 28,  29,  30,  31,  32],
#         [ 21,  22,  23,  24,  25],
#         [ 17,  18,  19,  20,  21],
#         [ 34,  35,  36,  37,  38],
#         [ 11,  12,  13,  14,  15],
#         [ 23,  24,  25,  26,  27],
#         [  6,   7,   8,   9,  10],
#         [ 38,  39,  40,  41,  42],
#         [ 25,  26,  27,  28,  29],
#         [  7,   8,   9,  10,  11]])
#
# torch.Size([10])
# tensor([ 33,  26,  22,  39,  16,  28,  11,  43,  30,  12])
# ```
#
# ### Sizes
# Your sample_x should be of size `(batch_size, sequence_length)` or (10, 5) in this case and sample_y should just have one dimension: batch_size (10).
#
# ### Values
#
# You should also notice that the targets, sample_y, are the *next* value in the ordered test_text data. So, for an input sequence `[ 28,  29,  30,  31,  32]` that ends with the value `32`, the corresponding output should be `33`.

# ---
# ## Build the Neural Network
# Implement an RNN using PyTorch's [Module class](http://pytorch.org/docs/master/nn.html#torch.nn.Module). You may choose to use a GRU or an LSTM. To complete the RNN, you'll have to implement the following functions for the class:
#  - `__init__` - The initialize function.
#  - `init_hidden` - The initialization function for an LSTM/GRU hidden state
#  - `forward` - Forward propagation function.
#
# The initialize function should create the layers of the neural network and save them to the class. The forward propagation function will use these layers to run forward propagation and generate an output and a hidden state.
#
# **The output of this model should be the *last* batch of word scores** after a complete sequence has been processed. That is, for each input sequence of words, we only want to output the word scores for a single, most likely, next word.
#
# ### Hints
#
# 1. Make sure to stack the outputs of the lstm to pass to your fully-connected layer, you can do this with `lstm_output = lstm_output.contiguous().view(-1, self.hidden_dim)`
# 2. You can get the last batch of word scores by shaping the output of the final, fully-connected layer like so:
#
# ```
# # reshape into (batch_size, seq_length, output_size)
# output = output.view(batch_size, -1, self.output_size)
# # get last batch
# out = output[:, -1]
# ```

# In[11]:

import torch.nn as nn

class RNN(nn.Module):

    def __init__(self, vocab_size, output_size, embedding_dim, hidden_dim, n_layers, dropout=0.5):
        """
        Initialize the PyTorch RNN Module
        :param vocab_size: The number of input dimensions of the neural network (the size of the vocabulary)
        :param output_size: The number of output dimensions of the neural network
        :param embedding_dim: The size of embeddings, should you choose to use them
        :param hidden_dim: The size of the hidden layer outputs
        :param dropout: dropout to add in between LSTM/GRU layers
        """
        super(RNN, self).__init__()

        self.vocab_size = vocab_size
        self.output_size = output_size
        self.embedding_dim = embedding_dim
        self.hidden_dim = hidden_dim
        self.n_layers = n_layers
        self.dropout = dropout

        self.emb = nn.Embedding(vocab_size, embedding_dim)
        self.lstm = nn.LSTM(embedding_dim, hidden_dim, n_layers, batch_first=True, dropout=dropout)
        self.drop = nn.Dropout(dropout)
        self.fc0 = nn.Linear(hidden_dim, output_size)

    def forward(self, nn_input, hidden):
        """
        Forward propagation of the neural network
        :param nn_input: The input to the neural network
        :param hidden: The hidden state
        :return: Two Tensors, the output of the neural network and the latest hidden state
        """
        batch_size = nn_input.shape[0]

        x = self.emb(nn_input)
        x, hidden = self.lstm(x, hidden)
        x = x.contiguous().view(-1, self.hidden_dim)
        x = self.drop(x)
        x = self.fc0(x) #no activation
        #print("After fc: x.shape = ", x.shape)

        #reshape to have result as [batch_size, output]
        x = x.view(batch_size, -1, self.output_size)
        #print("After reshaping, x.shape = ", x.shape)
        #print("x[0] = ", x[0])
        rtn = x[:, -1]
        #print("Returning rtn.shape = ", rtn.shape)
        #print("x[0] = ", rtn[0])

        # return one batch of output words and the hidden state
        return rtn, hidden

    def init_hidden(self, batch_size):
        '''
        Initialize the hidden state of an LSTM/GRU
        :param batch_size: The batch_size of the hidden state
        :return: hidden state of dims (n_layers, batch_size, hidden_dim)
        '''
        weights = next(self.parameters()).data

        h = weights.new(self.n_layers, batch_size, self.hidden_dim).zero_()
        c = weights.new(self.n_layers, batch_size, self.hidden_dim).zero_()
        if train_on_gpu:
            h = h.cuda()
            c = c.cuda()

        hidden = (h, c)

        return hidden

# ### Define forward and backpropagation
#
# Use the RNN class you implemented to apply forward and back propagation. This function will be called, iteratively, in the training loop as follows:
# ```
# loss = forward_back_prop(decoder, decoder_optimizer, criterion, inp, target)
# ```
#
# And it should return the average loss over a batch and the hidden state returned by a call to `RNN(inp, hidden)`. Recall that you can get this loss by computing it, as usual, and calling `loss.item()`.
#
# **If a GPU is available, you should move your data to that GPU device, here.**

# In[12]:

def forward_back_prop(rnn, optimizer, criterion, inp, target, hidden):
    """
    Forward and backward propagation on the neural network
    :param rnn: The PyTorch Module that holds the neural network
    :param optimizer: The PyTorch optimizer for the neural network
    :param criterion: The PyTorch loss function
    :param inp: A batch of input to the neural network
    :param target: The target output for the batch of input
    :param hidden: The hidden state from previous iteration
    :return: The loss and the latest hidden state Tensor
    """
    CLIP_LIMIT = 5.0

    if train_on_gpu:
        inp = inp.cuda()
        target = target.cuda()

    ##########
    # perform backpropagation and optimization

    #reset backprop data
    hidden = tuple([i.data for i in hidden])
    rnn.zero_grad()

    #run the rnn
    output, hidden = rnn(inp, hidden)
    #print("After running rnn: output = ", output.shape, ", hidden0 = ", hidden[0].shape, ", hidden1 = ", hidden[1].shape)

    #compute the loss
    #print("target = ", target)
    loss = criterion(output, target)
    #print("loss = ", loss, ", loss.item = ", loss.item())

    #backprop with gradient clipping
    loss.backward()
    nn.utils.clip_grad_norm_(rnn.parameters(), CLIP_LIMIT)
    optimizer.step()

    # return the loss over a batch and the hidden state produced by our model
    return loss.item(), hidden

# ### Hyperparameters
#
# Set and train the neural network with the following parameters:
# - Set `sequence_length` to the length of a sequence.
# - Set `batch_size` to the batch size.
# - Set `num_epochs` to the number of epochs to train for.
# - Set `learning_rate` to the learning rate for an Adam optimizer.
# - Set `vocab_size` to the number of uniqe tokens in our vocabulary.
# - Set `output_size` to the desired size of the output.
# - Set `embedding_dim` to the embedding dimension; smaller than the vocab_size.
# - Set `hidden_dim` to the hidden dimension of your RNN.
# - Set `n_layers` to the number of layers/cells in your RNN.
# - Set `show_every_n_batches` to the number of batches at which the neural network should print progress.
#
# If the network isn't getting the desired results, tweak these parameters and/or the layers in the `RNN` class.

# Data params
# Sequence Length
sequence_length = 50  # of words in a sequence; was 5; submitted with 20
# Batch Size
batch_size = 256

# data loader - do not change
train_loader = batch_data(int_text, sequence_length, batch_size)

# Training parameters
# Number of Epochs
num_epochs = 10
# Learning Rate
learning_rate = 0.001

# Model parameters
# Vocab size
vocab_size = len(vocab_to_int)
print("vocab_size = ", vocab_size)
# Output size
#jas - I believe this needs to be equal to vocab_size so that each column of the output matrix represents
#      one of the possible words, and a row of this matrix is essentially trying to immitate a one-hot vector.
#      However, the forward() method only returns the final column of the output, which, if this is true, would
#      represent the final word in the vocabulary. Therefore, this doesn't seem right, but it is at least
#      avoiding runtime errors for now.
output_size = vocab_size #num words it will generate for each pass?
# Embedding Dimension
embedding_dim = 400
# Hidden Dimension
hidden_dim = 512
# Number of RNN Layers
n_layers = 3

# Show stats for every n number of batches
show_every_n_batches = 1000

print("Torch version: ", torch.__version__)
if train_on_gpu:
    print("///// Using GPU!")
else:
    print("///// limited to using cpu")

'''
# create model and move to gpu if available
rnn = RNN(vocab_size, output_size, embedding_dim, hidden_dim, n_layers, dropout=0.5)
if train_on_gpu:
    rnn.cuda()

# defining loss and optimization functions for training
optimizer = torch.optim.Adam(rnn.parameters(), lr=learning_rate)
criterion = nn.CrossEntropyLoss()

# training the model
#with active_session():
trained_rnn = train_rnn(rnn, batch_size, optimizer, criterion, num_epochs, show_every_n_batches)

# saving the trained model
helper.save_model('./save/trained_rnn', trained_rnn)
print('Model Trained and Saved')
'''

"""
DON'T MODIFY ANYTHING IN THIS CELL
"""
import torch
import helper
import problem_unittests as tests

_, vocab_to_int, int_to_vocab, token_dict = helper.load_preprocess()
trained_rnn = helper.load_model('./save/trained_rnn')

# ## Generate TV Script
# With the network trained and saved, you'll use it to generate a new, "fake" Seinfeld TV script in this section.
#
# ### Generate Text
# To generate the text, the network needs to start with a single word and repeat its predictions until it reaches a set length. You'll be using the `generate` function to do this. It takes a word id to start with, `prime_id`, and generates a set length of text, `predict_len`. Also note that it uses topk sampling to introduce some randomness in choosing the most likely next word, given an output set of word scores!

import torch.nn.functional as F

def generate(rnn, prime_ids, int_to_vocab, token_dict, pad_value, predict_len=100):
    """
    Generate text using the neural network
    :param rnn: The PyTorch Module that holds the trained neural network
    :param prime_ids: The word ids to start the first prediction (a list)
    :param int_to_vocab: Dict of word id keys to word values
    :param token_dict: Dict of puncuation tokens keys to puncuation values
    :param pad_value: The value used to pad a sequence
    :param predict_len: The length of text to generate
    :return: The generated text
    """
    rnn.eval()

    # create a sequence (batch_size=1) with the prime_id
    current_seq = np.full((1, sequence_length), pad_value)
    #print("1. current_seq = ", current_seq)
    num_primes = len(prime_ids)
    #current_seq[-1][-num_primes] = prime_ids
    seq_size = sequence_length
    print("seq_size = ", seq_size, ", num_primes = ", num_primes)
    for i in range(num_primes):
        current_seq[-1][seq_size - num_primes + i] = prime_ids[i]
    print("2. current_seq = ", current_seq)
    #predicted = [int_to_vocab[prime_ids]]
    predicted = []
    for i in range(num_primes):
        predicted.append(int_to_vocab[prime_ids[i]])
    print("Predicted = ", predicted)

    for _ in range(predict_len):
        if train_on_gpu:
            current_seq = torch.LongTensor(current_seq).cuda()
        else:
            current_seq = torch.LongTensor(current_seq)

        # initialize the hidden state
        hidden = rnn.init_hidden(current_seq.size(0))

        # get the output of the rnn
        output, _ = rnn(current_seq, hidden)

        # get the next word probabilities
        p = F.softmax(output, dim=1).data
        if(train_on_gpu):
            p = p.cpu() # move to cpu

        # use top_k sampling to get the index of the next word
        top_k = 5
        p, top_i = p.topk(top_k)
        top_i = top_i.numpy().squeeze()
        #print("\nTop of loop.")
        #print("p     = ", p)
        #print("top_i = ", top_i)

        # select the likely next word index with some element of randomness
        p = p.numpy().squeeze()
        word_i = np.random.choice(top_i, p=p/p.sum())

        # retrieve that word from the dictionary
        word = int_to_vocab[word_i]
        predicted.append(word)
        #print("word_i = ", word_i, ", word = ", word)
        #print("New predicted = ", predicted)

        # the generated word becomes the next "current sequence" and the cycle can continue
        current_seq = current_seq.cpu()
        current_seq = np.roll(current_seq, -1, 1)
        current_seq[-1][-1] = word_i

    gen_sentences = ' '.join(predicted)

    # Replace punctuation tokens
    for key, token in token_dict.items():
        ending = ' ' if key in ['\n', '(', '"'] else ''
        gen_sentences = gen_sentences.replace(' ' + token.lower(), key)
    gen_sentences = gen_sentences.replace('\n ', '\n')
    gen_sentences = gen_sentences.replace('( ', '(')

    # return all the sentences
    return gen_sentences

# ### Generate a New Script
# It's time to generate the text. Set `gen_length` to the length of TV script you want to generate and set `prime_word` to one of the following to start the prediction:
# - "jerry"
# - "elaine"
# - "george"
# - "kramer"
#
# You can set the prime word to _any word_ in our dictionary, but it's best to start with a name for generating a TV script. (You can also start with any other names you find in the original text file!)

# run the cell multiple times to get different results!
gen_length = 400 # modify the length to your preference
prime_word = 'newman' # name for starting the script
prime_words = ['jerry', 'hello', 'newman']
prime_ints = []
for w in prime_words:
    prime_ints.append(vocab_to_int[w])
print("Prime ints = ", prime_ints)

"""
DON'T MODIFY ANYTHING IN THIS CELL THAT IS BELOW THIS LINE
"""
pad_word = helper.SPECIAL_WORDS['PADDING']
#generated_script = generate(trained_rnn, vocab_to_int[prime_word], int_to_vocab, token_dict, vocab_to_int[pad_word], gen_length)
generated_script = generate(trained_rnn, prime_ints, int_to_vocab, token_dict, vocab_to_int[pad_word], gen_length)
print(generated_script)

# save script to a text file
f =  open("generated_script_1.txt","w")
f.write(generated_script)
f.close()


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

# In[1]:

import os
import tarfile
import urllib

DOWNLOAD_ROOT = "https://raw.githubusercontent.com/ageron/handson-ml2/master/"
HOUSING_PATH = os.path.join("datasets", "housing")
HOUSING_URL = DOWNLOAD_ROOT + "datasets/housing/housing.tgz"

def fetch_housing_data( housing_url = HOUSING_URL, housing_path = HOUSING_PATH):
    os.makedirs(housing_path, exist_ok = True)
    tgz_path = os.path.join(housing_path, "housing.tgz")
    urllib.request.urlretrieve( housing_url, tgz_path)
    housing_tgz = tarfile.open( tgz_path)
    housing_tgz.extractall( path = housing_path)
    housing_tgz.close()

# In[2]:

fetch_housing_data()

# In[3]:

import pandas as pd
def load_housing_data( housing_path = HOUSING_PATH):
    csv_path = os.path.join( housing_path, "housing.csv")
    return pd.read_csv( csv_path)

# In[4]:

housing = load_housing_data()
housing.head()

# In[5]:

housing.info()

# In[6]:

housing["ocean_proximity"]. value_counts()

# In[7]:

housing.describe()

# In[8]:

get_ipython().run_line_magic('matplotlib', 'inline')

import matplotlib.pyplot as plt
housing.hist( bins = 50, figsize =( 20,15))
plt.show()

# In[9]:

import numpy as np

def split_train_test( data, test_ratio):
    shuffled_indices = np.random.permutation( len( data))
    test_set_size = int( len( data) * test_ratio)
    test_indices = shuffled_indices[:test_set_size]
    train_indices = shuffled_indices[test_set_size:]
    return data.iloc[train_indices], data.iloc[test_indices]

# In[10]:

train_set, test_set = split_train_test(housing, 0.2)

# In[11]:

len(train_set)

# In[12]:

len(test_set)

# In[13]:

from zlib import crc32
def test_set_check( identifier, test_ratio):
    return crc32( np.int64( identifier)) & 0xffffffff < test_ratio * 2** 32

def split_train_test_by_id( data, test_ratio, id_column):
    ids = data[id_column]
    in_test_set = ids.apply( lambda id_: test_set_check( id_, test_ratio))
    return data.loc[~in_test_set], data.loc[ in_test_set]

# In[14]:

# Unfortunately, the housing dataset does not have an identifier column.
# The simplest solution is to use the row index as the ID:

housing_with_id = housing.reset_index() # adds an ` index ` column
train_set, test_set = split_train_test_by_id( housing_with_id, 0.2, "index")

# In[15]:

#
#
#

housing["income_cat"] = pd.cut(housing["median_income"],
                                bins =[ 0., 1.5, 3.0, 4.5, 6., np.inf],
                                labels =[ 1, 2, 3, 4, 5])
housing["income_cat"].hist()

# In[16]:

# Now you are ready to do stratified sampling based on the income category.
# For this you can use Scikit-Learn’s StratifiedShuffleSplit class:

from sklearn.model_selection import StratifiedShuffleSplit

split = StratifiedShuffleSplit(n_splits = 1, test_size = 0.2, random_state = 42)
for train_index, test_index in split.split(housing, housing["income_cat"]):
    strat_train_set = housing.loc[train_index]
    strat_test_set = housing.loc[test_index]

# In[17]:

# Let’s see if this worked as expected.
# You can start by looking at the income category proportions in the test set:

strat_test_set["income_cat"].value_counts() / len(strat_test_set)

# In[18]:

# Picture 2.10 .... no idea how it was created

# In[19]:

# Now you should remove the income_cat attribute so the data is back to its original state:

for set_ in (strat_train_set, strat_test_set):
    set_.drop("income_cat", axis = 1, inplace = True)

# In[20]:

# make a copy to protect original data
housing = strat_train_set.copy()

# ## Visualizing Geographical Data
# Since there is geographical information (latitude and longitude),
# it is a good idea to create a scatterplot of all districts to visualize the data (Figure   2-11):

# In[21]:

housing.plot(kind = "scatter", x = "longitude", y = "latitude")

# In[22]:

housing.plot(kind = "scatter", x = "longitude", y = "latitude", alpha = 0.1)

# In[23]:

# Now let’s look at the housing prices (Figure   2-13).
# The radius of each circle represents the district’s population (option s),
# and the color represents the price (option c). Wewill use a predefined color map (option cmap) called jet, which ranges
# from blue (low values) to red (high prices): 16

housing.plot(kind ="scatter", x = "longitude", y = "latitude", alpha = 0.4,
             s = housing["population"] / 100, label = "population", figsize =( 10,7),
             c = "median_house_value", cmap = plt.get_cmap("jet"), colorbar = True, )
plt.legend()

# In[24]:

# Looking for Correlations Since the dataset is not too large,
# you can easily compute the standard correlation coefficient
# (also called Pearson’s r) between every pair of attributes using the corr() method:

corr_matrix = housing.corr()
corr_matrix["median_house_value"].sort_values(ascending = False)

# In[25]:

# Another way to check for correlation between attributes is to use the
# pandas scatter_matrix() function, which plots every numerical attribute
# against every other numerical attribute. Since there are now 11 numerical attributes,
# you would get 112 = 121 plots, which would not fit on a page — so let’s just focus
# on a few promising attributes that seem most correlated with the median
# housing value (Figure   2-15):

from pandas.plotting import scatter_matrix
attributes = ["median_house_value", "median_income", "total_rooms", "housing_median_age"]
scatter_matrix(housing[attributes], figsize = (12, 8))

# In[26]:

# The most promising attribute to predict the median house value is the median income,
# so let’s zoom in on their correlation scatterplot (Figure   2-16):

housing.plot(kind = "scatter", x = "median_income", y = "median_house_value", alpha = 0.1)

# ## Experimenting with Attribute Combinations
# Hopefully the previous sections gave you an idea of a few ways you can explore the data and gain insights. You identified a few data quirks that you may want to clean up before feeding the data to a Machine Learning algorithm, and you found interesting correlations between attributes, in particular with the target attribute. You also noticed that some attributes have a tail-heavy distribution, so you may want to transform them (e.g., by computing their logarithm). Of course, your mileage will vary considerably with each project, but the general ideas are similar.
#
# One last thing you may want to do before preparing the data for Machine Learning algorithms is to try out various attribute combinations. For example, the total number of rooms in a district is not very useful if you don’t know how many households there are. What you really want is the number of rooms per household. Similarly, the total number of bedrooms by itself is not very useful: you probably want to compare it to the number of rooms. And the population per household also seems like an interesting attribute combination to look at. Let’s create these new attributes:
#

# In[27]:

housing["rooms_per_household"] = housing["total_rooms"] / housing["households"]
housing["bedrooms_per_room"] = housing["total_bedrooms"]/ housing["total_rooms"]
housing["population_per_household"] = housing["population"]/ housing["households"]

# And now let’s look at the correlation matrix again:

corr_matrix = housing.corr()
corr_matrix["median_house_value"].sort_values(ascending = False)

# ## Prepare the Data for Machine Learning Algorithms
#
# But first let’s revert to a clean training set (by copying strat_train_set once again). Let’s also separate the predictors and the labels, since we don’t necessarily want to apply the same transformations to the predictors and the target values (note that drop() creates a copy of the data and does not affect strat_train_set):

# In[28]:

housing = strat_train_set.drop("median_house_value", axis = 1)
housing_labels = strat_train_set["median_house_value"].copy()

# In[29]:

# SimpleImputer. Here is how to use it.
# First, you need to create a SimpleImputer instance,
# specifying that you want to replace each attribute’s missing values with the median of that attribute:

from sklearn.impute import SimpleImputer

imputer = SimpleImputer( strategy = "median")

# Since the median can only be computed on numerical attributes,
# you need to create a copy of the data without the text attribute ocean_proximity:

housing_num = housing.drop("ocean_proximity", axis = 1)

# Now you can fit the imputer instance to the training data using the fit() method:

imputer.fit(housing_num)

# Géron, Aurélien. Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow (pp. 63-64). O'Reilly Media. Kindle Edition.

# ## Handling Text and Categorical Attributes
#
# So far we have only dealt with numerical attributes, but now let’s look at text attributes. In this dataset, there is just one: the ocean_proximity attribute. Let’s look at its value for the first 10 instances:
#
# Géron, Aurélien. Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow (pp. 65-66). O'Reilly Media. Kindle Edition.

# In[30]:

housing_cat = housing[["ocean_proximity"]]

# In[31]:

housing_cat.head(10)

# In[32]:

from sklearn.preprocessing import OrdinalEncoder
ordinal_encoder = OrdinalEncoder()
housing_cat_encoded = ordinal_encoder.fit_transform(housing_cat)
housing_cat_encoded[:10]

# In[33]:

# You can get the list of categories using the categories_ instance
# variable. It is a list containing a 1D array of categories for each
# categorical attribute (in this case, a list containing a single array
# since there is just one categorical attribute):

ordinal_encoder.categories_

# In[34]:

# one-hot attributes
from sklearn.preprocessing import OneHotEncoder
cat_encoder = OneHotEncoder()
housing_cat_1hot = cat_encoder.fit_transform( housing_cat)
housing_cat_1hot

# In[35]:

# Notice that the output is a SciPy sparse matrix, instead of a NumPy array.
# You can use it mostly like a normal 2D array, 21 but if you really want to convert it to a (dense) NumPy array, just call the toarray() method:

housing_cat_1hot.toarray()

# In[36]:

type(housing_cat_1hot)

# In[37]:

cat_encoder.categories_

# ## Custom Transformers
#
# Géron, Aurélien. Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow (p. 68). O'Reilly Media. Kindle Edition.

# In[38]:

from sklearn.base import BaseEstimator, TransformerMixin

rooms_ix, bedrooms_ix, population_ix, households_ix = 3, 4, 5, 6

class CombinedAttributesAdder( BaseEstimator, TransformerMixin):
    def __init__( self, add_bedrooms_per_room = True): # no *args or ** kargs
        self.add_bedrooms_per_room = add_bedrooms_per_room
    def fit( self, X, y = None):
        return self # nothing else to do
    def transform( self, X):
        rooms_per_household = X[:, rooms_ix] / X[:, households_ix]
        population_per_household = X[:, population_ix] / X[:, households_ix]

        if self.add_bedrooms_per_room:
            bedrooms_per_room = X[:, bedrooms_ix] / X[:, rooms_ix]
            return np.c_[ X, rooms_per_household, population_per_household, bedrooms_per_room]
        else:
            return np.c_[ X, rooms_per_household, population_per_household]

attr_adder = CombinedAttributesAdder( add_bedrooms_per_room = False)
housing_extra_attribs = attr_adder.transform( housing.values)

# In[39]:

attr_adder

# In[40]:

housing_extra_attribs

# ## Transformation Pipelines
#
# As you can see, there are many data transformation steps that need to be executed in the right order. Fortunately, Scikit-Learn provides the Pipeline class to help with such sequences of transformations. Here is a small pipeline for the numerical attributes:
#
# Géron, Aurélien. Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow (p. 70). O'Reilly Media. Kindle Edition.

# In[41]:

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

num_pipeline = Pipeline([ ('imputer', SimpleImputer(strategy = "median")),
                         ('attribs_adder', CombinedAttributesAdder()),
                         ('std_scaler', StandardScaler()), ])

housing_num_tr = num_pipeline.fit_transform(housing_num)

# In[42]:

#
from sklearn.compose import ColumnTransformer

num_attribs = list( housing_num)
cat_attribs = ["ocean_proximity"]
full_pipeline = ColumnTransformer([
    ("num", num_pipeline, num_attribs),
    ("cat", OneHotEncoder(), cat_attribs),
    ])

housing_prepared = full_pipeline.fit_transform( housing)

# In[43]:

housing_prepared

# In[ ]:


# To add a new cell, type '# %%'
# To add a new markdown cell, type '# %% [markdown]'
# %% [markdown]
# # Effect of Heuristic on Search Efficiency
#
# This project will show you how to solve planning problems -- Think logistics and scheduling -- using search and logic to find tractable solutions.  Classic planning problems can solve a problem where the rules are known and clear-cut.  In other words, they are _deterministic_.  Where to effects of an action are known to all, in AI, this is known as being _observable_.  In controlled environments, where change only occurs through the actions taken by a planning agent, these systems work well.
#
# Acting in these worlds can be modeled logically.  Actions can take place by first meeting all their preconditions.  And once completed, its effects known.  Imagine you are in charge of logistics at Fed Ex and need to find solutions for getting packages from place to place.  How could that be done?  To model the action of flying a package, we can use a [Planning Domain Definition Language](https://planning.wiki/) (PDDL).
#
# ```
# Action(Fly, (plane, from, to)
#     Precondition: At(plane, from) and Plane(from) and Airport(from) and Airport(to)
#     Effect: not At(plane, from) and At(plane, to)
# )
# ```
#
# Propositional logic describes each action briefly.   Just add actions to represent new problems or new business domains.  Knowing that we have a starting state and goal state, a set of actions and costs associated with each action, allows us to apply search algorithms to find solutions.
#

# %%
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from dataclasses import make_dataclass

sns.set_theme(style="whitegrid")

# %%
Record = make_dataclass(
    "Record",
    [
        ("Problem", int),
        ("Alg_Num", int),
        ("Alg_Name", str),
        ("Heuristic", str),
        ("Actions", int),
        ("Expansions", int),
        ("Goal_Tests", int),
        ("New_Nodes", int),
        ("Length", int),
        ("Time", float),
    ],
)

# %% [markdown]
# ## Experimental Results
#
# I ran pypy to generate my experiments results using my mac desktop computer.

# %%
dataset = [
    Record(1, 1, "BFS", "", 20, 43, 56, 178, 6, 0.0284),
    Record(1, 2, "DFS", "", 20, 21, 22, 84, 20, 0.0073),
    Record(2, 1, "BFS", "", 72, 3343, 4609, 30503, 9, 0.3459),
    Record(2, 2, "DFS", "", 72, 624, 625, 5602, 619, 0.4810),
    Record(4, 4, "Greedy", "h_unmet_goals", 104, 29, 31, 280, 18, 0.0612),
    Record(
        4, 11, "A*", "h_pg_setlevel", 104, 79863, 79865, 768641, 14, 30733.3346,
    ),
    Record(
        3, 11, "A*", "h_pg_setlevel", 88, 12872, 12874, 115220, 12, 3046.3672
    ),
    Record(1, 3, "UCS", "", 20, 60, 62, 240, 6, 0.0379),
    Record(1, 4, "Greedy", "h_unmet_goals", 20, 7, 9, 29, 6, 0.0032),
    Record(1, 5, "Greedy", "h_pg_levelsum", 20, 6, 8, 28, 6, 0.4590),
    Record(1, 6, "Greedy", "h_pg_maxlevel", 20, 6, 8, 24, 6, 0.1218),
    Record(1, 7, "Greedy", "h_pg_setlevel", 20, 13, 15, 53, 6, 1.2660),
    Record(1, 8, "A*", "h_unmet_goals", 20, 50, 52, 206, 6, 0.0216),
    Record(1, 9, "A*", "h_pg_levelsum", 20, 28, 30, 122, 6, 0.2154),
    Record(1, 10, "A*", "h_pg_maxlevel", 20, 43, 45, 180, 6, 0.1616),
    Record(1, 11, "A*", "h_pg_setlevel", 20, 46, 48, 192, 6, 0.7168),
    Record(2, 3, "UCS", "", 72, 5154, 5156, 46618, 9, 0.7450),
    Record(2, 4, "Greedy", "h_unmet_goals", 72, 17, 19, 170, 9, 0.0205),
    Record(2, 5, "Greedy", "h_pg_levelsum", 72, 9, 11, 86, 9, 0.4228),
    Record(2, 6, "Greedy", "h_pg_maxlevel", 72, 27, 29, 249, 9, 0.6970),
    Record(2, 7, "Greedy", "h_pg_setlevel", 72, 304, 306, 2846, 10, 54.8001),
    Record(2, 8, "A*", "h_unmet_goals", 72, 2467, 2469, 22522, 9, 0.7026),
    Record(2, 9, "A*", "h_pg_levelsum", 72, 357, 359, 3426, 9, 10.3350),
    Record(2, 10, "A*", "h_pg_maxlevel", 72, 2887, 2889, 26594, 9, 59.0657),
    Record(2, 11, "A*", "h_pg_setlevel", 72, 2879, 2881, 26622, 9, 577.1144),
    Record(3, 4, "Greedy", "h_unmet_goals", 88, 25, 27, 230, 15, 0.0519),
    Record(3, 6, "Greedy", "h_pg_maxlevel", 88, 21, 23, 195, 13, 1.8522),
    Record(4, 4, "Greedy", "h_unmet_goals", 104, 29, 31, 280, 18, 0.0615),
    Record(4, 6, "Greedy", "h_pg_maxlevel", 104, 56, 58, 580, 17, 3.6040),
    Record(3, 8, "A*", "h_unmet_goals", 88, 7388, 7390, 65711, 12, 1.2993),
    Record(3, 10, "A*", "h_pg_maxlevel", 88, 9580, 9582, 86312, 12, 331.0158),
    Record(4, 8, "A*", "h_unmet_goals", 104, 34330, 34332, 328509, 14, 4.0136),
    Record(
        4, 10, "A*", "h_pg_maxlevel", 104, 62077, 62079, 599376, 14, 3268.4160
    ),
    Record(3, 2, "DFS", "", 88, 408, 409, 3364, 392, 0.2780),
    Record(4, 2, "DFS", "", 104, 25174, 25175, 228849, 24132, 771.7955),
]
df = pd.DataFrame(dataset)

# %% [markdown]
# ---
#
# ## Supporting Functions

# %%
def create_catplot(df, y_var, title, is_log=True):
    """Creates a categorical plot using Seaborn's catplot.

    Parameters:
        df: Dataframe contianing data to be plotted
        y_var: Name of column in dataframe used for y-axis values
        title: Title for the plot
    """

    g = sns.catplot(
        data=df,
        kind="bar",
        x="Problem",
        y=y_var,
        hue="Alg_Title",
        height=7,
        aspect=1.0,
    )
    g.despine(left=True, bottom=True)
    if is_log:
        g.set(yscale="log")
    g.ax.xaxis.grid(True, linewidth=0.25)
    g.ax.yaxis.grid(True, "minor", linewidth=0.25)
    g.despine(left=True, bottom=False)
    plt.title(title)

# %%
def create_relplot(df, y_var, title):
    """Creates a relational plot using Seaborn's catplot.

    Parameters:
        df: Dataframe contianing data to be plotted
        y_var: Name of column in dataframe used for y-axis values
        title: Title for the plot
    """

    g = sns.relplot(
        data=df,
        x="Actions",
        y=y_var,
        hue="Alg_Title",
        size="Problem",
        height=7,
        aspect=1.0,
        sizes=(40, 120),
    )
    g.set(yscale="log")
    g.ax.xaxis.grid(True, linewidth=0.25)
    g.ax.yaxis.grid(True, "minor", linewidth=0.25)
    g.despine(left=True, bottom=False)
    plt.title(title)

# %%
df["Alg_Title"] = (
    df.Alg_Num.astype(str) + " " + df["Alg_Name"] + " " + df["Heuristic"]
)
df = df.sort_values(by=["Problem", "Alg_Num"])

# %% [markdown]
# ---
#
# ## Analysis: Nodes vs Actions

# %%
create_relplot(df, "New_Nodes", "New Nodes vs Actions")

# %%
create_catplot(df, "New_Nodes", "New Nodes by Problem")

# %%
df[["Problem", "Alg_Title", "New_Nodes", "Actions"]].set_index(
    "Problem"
).sort_values(by=["Problem", "New_Nodes"])

# %% [markdown]
# ### Discussion
#
# Solving more complex problems requires more actions.  To solve the problems here, the number of actions grows linearly.   Finding the sequence of actions needed to reach the goal requires exponentially more new nodes as problem complexity increases.  When plotted logarithmically, exponential growth appears linearly, just as we see here.  The linear pattern occurs as logarithms and exponents are inverse functions.  One notable exception is the performance of depth-first search on problem 3 is an order of magnitude better than expected.  Repeated runs are consistent, so I imagine it has to do with the structure of the problem that allows DFS to find a solution with minimal backtracking.
#
# For all problems and heuristics, greedy algorithms outperform A* by needing fewer nodes to arrive at a solution.  A* has exponential growth in space complexity and puts A* at a disadvantage.  This especially true when the number of actions grows with the increased complexity of the problem.  Breadth-first search algorithms performed the worst -- as expected -- since its frontier size grows exponentially.
# %% [markdown]
# ---
#
# ## Analysis: Time vs Actions
#

# %%
create_relplot(df, "Time", "Runtime vs Actions")

# %%
create_catplot(df, "Time", "Runtime vs Actions")

# %%
df[["Problem", "Alg_Title", "Time", "Actions"]].set_index(
    "Problem"
).sort_values(by=["Problem", "Time"])

# %%

# %% [markdown]
# ### Discussion
# Greedy search -- especially when using the h_unmet_goals heuristic -- finds solutions the fastest. The h_pg_setlevel heuristic is the slowest for all problems. For problem 1, which only requires 20 actions to solve, greedy search using the h_pg_setlevel is the slowest.  For all other problems, it is A* using this same heuristic. The h_pg_setlevel heuristic estimates:
#
# > The set level of a planning graph is the first level where all goals appear such that no pair of goal literals are mutex in the last layer of the planning graph.
#
# The h_pg_setlevel heuristic is known to perform poorly on complex problems, and here it lives up to its billing.
#
#
#
# %% [markdown]
# ---
#
# ## Analysis: Length of plan by algorithm
#
#

# %%
create_catplot(df, "Actions", "Actions by Algorithm", is_log=False)

# %%
df[["Problem", "Alg_Title", "Actions"]].set_index("Problem").sort_values(
    by=["Problem", "Actions"]
)

# %% [markdown]
# ### Discussion
#
# The length of the plan required to arrive at the goal is consistent for all algorithms for each problem. The work to derive the plan -- measured in time expended of new nodes created -- varies considerably across algorithms. Why is this? Conflicting actions have been eliminated by only allowing actions that are not mutually exclusive. Mutually exclusive or mutex actions happen in three different ways.
#
# 1. _Inconsistent_ actions negate the effects of one another. For example, `Load(C, p)` and `Unload(C, p)`.
#
# 1. _Interference_ s when the effects of one action are mutually exclusive of the precondition of another.  For example, the actions `Fly(a, b)` and `At(a)` are _mutex_ since once flown to airport b.  The plane is no longer at airport a.
#
# 1. _Competing needs_ occur when the preconditions of one action interfere with the preconditions of the other.  One plane can only fly to one airport, so the actions `Fly(p, a, b)` and `Fly(P, a, c)` compete for the having the plane `At(a)`.
#
# By eliminating mutex actions, the size of the resulting plan shrinks dramatically.
# %% [markdown]
# # Questions
#
# 1. Which algorithm or algorithms would be most appropriate for planning in a very restricted domain (i.e., one that has only a few actions) and needs to operate in real time?
#
# > Based on my analysis of time vs. actions, I would select Greedy h_unmet_goals or DFS because they performed the best for small problems (i.e., problem 1). Both completed their search in less than one-hundredth of a second, an order of magnitude faster than A* h_unmet_goals.
#
# 1. _Which algorithm or algorithms would be most appropriate for planning in very large domains (e.g., planning delivery routes for all UPS drivers in the U.S. on a given day)_
#
# > Greedy h_unmet_goals is by far the best choice for large problem domains (i.e., problem 4). Other choices are Greedy h_pg_maxlevel or A* h_unmet_goals, but they are three orders of magnitude slower.
#
#
# 1. _Which algorithm or algorithms would be most appropriate for planning problems where it is important to find only optimal plans?
#
# > A planning graph is a tree with its root at the initial state. Actions are the branches of the tree and whose effects result in a new state. A* is guaranteed to return an optimal plan when searching a tree using an admissible heuristic. Heuristics are estimates of the cost measured in the number of new states from the initial state. An admissible heuristic will always be less than or equal to the actual cost. Of the heuristics considered in this analysis, here are those that are admissible:
#
# * The h_pg_maxlevel. heuristic is the largest level-cost needed to achieve any one of the goal's conditions.
#
# * The h_pg_setlevel heuristic is the level where all the goal's conditions are achieved.
#
# > __So, to find optimal plans use either A*_h_pg_setlevel heuristic or A*_h_pg_maxlevel. In my results, A_h_pg_setlevel is about 10 faster than A_h_pg_maxlevel for all problems analyzed in this exercise. So, choose A*_h_pg_setlevel. It will return an optimal plan in the least amount of time.__
#
# BFS, too, will return an optimal plan but expands an exponentially expanding number of nodes in the process, so it consumes a lot of memory and time.__
#
# ### Cite
#
# Russell, S. J., & Norvig, P. (2010). Artificial intelligence: A modern approach (3rd ed.). Upper Saddle River: Prentice-Hall.  Chapter 3 - Solving Problems by Searching and Chapter 10 - Classical Planning.

# %%


import os
import torch
import numpy as np
import random

def set_seed(seed):
    os.environ['PYTHONHASHSEED'] = str(seed)
    torch.manual_seed(seed)  # cpu
    torch.cuda.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)  # gpu
    np.random.seed(seed)
    random.seed(seed)
    torch.backends.cudnn.deterministic = True

# set_seed(1)
# print('#randam:\t', torch.rand(5))


r"""
Support for persistent functions in .sage files

Persistent functions are functions whose values are stored on disk
so they do not have to be recomputed.

The inputs to the function must be hashable (so lists are not
allowed). Though a hash is used, in the incredibly unlikely event
that a hash collision occurs, your function will not return an
incorrect result because of this (though the cache might not be
used either).

This is meant to be used from ``.sage`` files, not from
library ``.py`` files.

To use this disk caching mechanism, just put
``@func_persist`` right before your function
definition. For example,

::

    @func_persist
    def bern(n):
        "Return the n-th Bernoulli number, caching the result to disk."
        return bernoulli(n)

You can then use the function ``bern`` as usual, except
it will almost instantly return values that have already been
computed, even if you quit and restart.

The disk cache files are stored by default in the subdirectory
``func_persist`` of the current working directory,
with one file for each evaluation of the function.
"""
########################################################################
#       Copyright (C) 2006 William Stein <wstein@gmail.com>
#
#  Distributed under the terms of the GNU General Public License (GPL)
#
#                  https://www.gnu.org/licenses/
########################################################################

import inspect
import os

from . import persist

class func_persist:
    r"""
    Put ``@func_persist`` right before your function
    definition to cache values it computes to disk.
    """
    def __init__(self, f, dir='func_persist'):
        from sage.misc.misc import sage_makedirs
        self.__func = f
        self.__dir = dir
        sage_makedirs(dir)
        self.__doc__ = '%s%s%s' % (
            f.__name__,
            inspect.formatargspec(*inspect.getargs(f.__code__)),
            f.__doc__)

    def __call__(self, *args, **kwds):
        key = (tuple(args), tuple(kwds.items()))
        h = hash(key)
        name = '%s/%s_%s.sobj' % (self.__dir, self.__func.__name__, h)

        if os.path.exists(name):
            key2, val = persist.load(name)
            if key == key2:
                # We save and test equality of keys to avoid
                # the (extremely remote) possibility of a hash
                # collision.  Correctness is crucial in mathematics.
                return val

        val = self.__func(*args, **kwds)
        persist.save((key, val), name)
        return val


# !/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Defines the unit tests for the :mod:`colour.appearance.rlab` module.
"""

import numpy as np
import unittest
from itertools import permutations

from colour.appearance import (
    D_FACTOR_RLAB,
    VIEWING_CONDITIONS_RLAB,
    XYZ_to_RLAB,
)
from colour.utilities import (
    as_float_array,
    domain_range_scale,
    ignore_numpy_errors,
)

__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013-2021 - Colour Developers'
__license__ = 'New BSD License - https://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-developers@colour-science.org'
__status__ = 'Production'

__all__ = [
    'TestXYZ_to_RLAB',
]

class TestXYZ_to_RLAB(unittest.TestCase):
    """
    Defines :func:`colour.appearance.rlab.XYZ_to_RLAB` definition unit
    tests methods.
    """

    def test_XYZ_to_RLAB(self):
        """
        Tests :func:`colour.appearance.rlab.XYZ_to_RLAB` definition.

        Notes
        -----
        -   The test values have been generated from data of the following file
            by *Fairchild (2013)*:
            http://rit-mcsl.org/fairchild//files/AppModEx.xls
        """

        XYZ = np.array([19.01, 20.00, 21.78])
        XYZ_n = np.array([95.05, 100.00, 108.88])
        Y_n = 318.31
        sigma = 0.4347
        np.testing.assert_allclose(
            XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma),
            np.array([49.67, 0.01, 270, 0, np.nan, 0, -0.01]),
            rtol=0.01,
            atol=0.01)

        XYZ = np.array([57.06, 43.06, 31.96])
        Y_n = 31.83
        np.testing.assert_allclose(
            XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma),
            np.array([69.33, 49.74, 21.3, 0.72, np.nan, 46.33, 18.09]),
            rtol=0.01,
            atol=0.01)

        XYZ = np.array([3.53, 6.56, 2.14])
        XYZ_n = np.array([109.85, 100.00, 35.58])
        Y_n = 318.31
        np.testing.assert_allclose(
            XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma),
            np.array([30.78, 41.02, 176.9, 1.33, np.nan, -40.96, 2.25]),
            rtol=0.01,
            atol=0.01)

        XYZ = np.array([19.01, 20.00, 21.78])
        Y_n = 31.83
        np.testing.assert_allclose(
            XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma),
            np.array([49.83, 54.87, 286.5, 1.1, np.nan, 15.57, -52.61]),
            rtol=0.01,
            atol=0.01)

    def test_n_dimensional_XYZ_to_RLAB(self):
        """
        Tests :func:`colour.appearance.rlab.XYZ_to_RLAB` definition
        n-dimensional support.
        """

        XYZ = np.array([19.01, 20.00, 21.78])
        XYZ_n = np.array([95.05, 100.00, 108.88])
        Y_n = 318.31
        sigma = 0.4347
        specification = XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma)

        XYZ = np.tile(XYZ, (6, 1))
        specification = np.tile(specification, (6, 1))
        np.testing.assert_almost_equal(
            XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma), specification, decimal=7)

        XYZ_n = np.tile(XYZ_n, (6, 1))
        np.testing.assert_almost_equal(
            XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma), specification, decimal=7)

        XYZ = np.reshape(XYZ, (2, 3, 3))
        XYZ_n = np.reshape(XYZ_n, (2, 3, 3))
        specification = np.reshape(specification, (2, 3, 7))
        np.testing.assert_almost_equal(
            XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma), specification, decimal=7)

    def test_domain_range_scale_XYZ_to_RLAB(self):
        """
        Tests :func:`colour.appearance.rlab.XYZ_to_RLAB` definition domain and
        range scale support.
        """

        XYZ = np.array([19.01, 20.00, 21.78])
        XYZ_n = np.array([109.85, 100, 35.58])
        Y_n = 31.83
        sigma = VIEWING_CONDITIONS_RLAB['Average']
        D = D_FACTOR_RLAB['Hard Copy Images']
        specification = XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma, D)

        d_r = (
            ('reference', 1, 1),
            (1, 0.01, np.array([1, 1, 1 / 360, 1, np.nan, 1, 1])),
            (100, 1, np.array([1, 1, 100 / 360, 1, np.nan, 1, 1])),
        )
        for scale, factor_a, factor_b in d_r:
            with domain_range_scale(scale):
                np.testing.assert_almost_equal(
                    XYZ_to_RLAB(XYZ * factor_a, XYZ_n * factor_a, Y_n, sigma,
                                D),
                    as_float_array(specification) * factor_b,
                    decimal=7)

    @ignore_numpy_errors
    def test_nan_XYZ_to_RLAB(self):
        """
        Tests :func:`colour.appearance.rlab.XYZ_to_RLAB` definition nan
        support.
        """

        cases = [-1.0, 0.0, 1.0, -np.inf, np.inf, np.nan]
        cases = set(permutations(cases * 3, r=3))
        for case in cases:
            XYZ = np.array(case)
            XYZ_n = np.array(case)
            Y_n = case[0]
            sigma = case[0]
            D = case[0]
            XYZ_to_RLAB(XYZ, XYZ_n, Y_n, sigma, D)


"""
PlotlyFig testing. Most tests are run by comparing generated Plotly figures
with pre-generated json files. Some are just ensuring Plotly does not throw
errors.
"""
from copy import deepcopy

from monty.json import MontyEncoder

__author__ = "Alex Dunn <ardunn@lbl.gov>"

import os
import unittest
import json
import numpy as np
import pandas as pd

from figrecipes.plot import PlotlyFig

a = [1.6, 2.1, 3]
b = [1, 4.2, 9]
c = [14, 15, 17]
ah = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
bh = [2, 4, 6, 8, 10, 2, 4, 6, 8, 10]
ch = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
np.random.seed(23)
xlabels = ["low", "med", "high"]
ylabels = ["worst", "mediocre", "best"]
pfkwargs = {
    "mode": "offline",
    "colorbar_title": "auto",
    "y_scale": "linear",
    "x_scale": "linear",
    "ticksize": 25,
    "fontscale": 0.9,
    "fontsize": 25,
    "fontfamily": "Courier",
    "bgcolor": "white",
    "colorscale": "Viridis",
    "margins": 120,
    "pad": 0,
    "filename": "offline_plot",
    "show_offline_plot": True,
    "hovermode": "closest",
    "hoverinfo": "x+y+text",
}

def refresh_json(open_plots=False):
    """
    For developer use. Refresh the json files and open plots to see if they
    look good. Use this function to set the current PlotlyFig build outputs
    as the true values of the tests.

    Args:
        open_plots (bool): If True, opens all plots generated. Useful if you
            want to check the current build outputs to make sure they look good.
            If False, just generates the json files and quits.
    """

    pf = PlotlyFig(**pfkwargs)
    xys = pf.xy([(a, b)], return_plot=True)
    xym = pf.xy([(a, b), (b, a)], return_plot=True)
    xy_colors = pf.xy(
        [(a, b), (a, c), (c, c)],
        modes=["markers", "markers+lines", "lines"],
        colors=[c, "red", "blue"],
        return_plot=True,
    )
    hmb = pf.heatmap_basic([a, b, c], xlabels, ylabels, return_plot=True)
    his = pf.histogram(a + b + c, n_bins=5, return_plot=True)
    bar = pf.bar(x=a, y=b, labels=xlabels, return_plot=True)
    pcp = pf.parallel_coordinates([a, b], cols=xlabels, return_plot=True)

    scm = pf.scatter_matrix([a, b, c], return_plot=True)
    vio = pf.violin([a, b, c, b, a, c, b], cols=xlabels, return_plot=True)

    df = pd.DataFrame(data=np.asarray([ah, bh, ch]).T, columns=["ah", "bh", "ch"])
    x_labels = ["low", "high"]
    y_labels = ["small", "large"]

    # TODO: this plot was not JSON serializable, use a different serialization
    #  method for all plots
    hmdf = pf.heatmap_df(df, x_labels=x_labels, y_labels=y_labels, return_plot=True)

    df = pd.DataFrame(np.random.rand(50, 3), columns=list("qwe"))
    triangle = pf.triangle(df[["q", "w", "e"]], return_plot=True)

    fnamedict = {
        "xys": xys,
        "xym": xym,
        "xy_colors": xy_colors,
        "hmb": hmb,
        "his": his,
        "bar": bar,
        "pcp": pcp,
        "vio": vio,
        "scm": scm,
        "triangle": triangle,
        "hmdf": hmdf,
    }

    for fname, orig_obj in fnamedict.items():
        obj = deepcopy(orig_obj)
        if fname in ["vio", "scm"]:
            # Layout is compared for the plots which always convert to
            # dataframes, as dataframes are not easily encoded by json.dump
            obj = obj["layout"].to_plotly_json()
        elif fname in ["triangle"]:
            pass
        else:
            # plotly figures need to be converted jsonable data
            obj["data"] = [p.to_plotly_json() for p in obj["data"]]

        with open("template_{}.json".format(fname), "w") as f:
            json.dump(obj, f, cls=MontyEncoder)

    if open_plots:
        for obj in fnamedict.values():
            pf.create_plot(obj, return_plot=False)

class PlotlyFigTest(unittest.TestCase):
    def setUp(self):
        self.pf = PlotlyFig(**pfkwargs)
        self.base_dir = os.path.dirname(os.path.realpath(__file__))

    def fopen(self, fname):
        fname = self.base_dir + "/" + fname
        with open(fname, "r") as f:
            return json.load(f)

    def test_xy(self):
        # Single trace
        xys_test = self.pf.xy([(a, b)], return_plot=True)
        xys_test["data"] = [p.to_plotly_json() for p in xys_test["data"]]
        xys_true = self.fopen("template_xys.json")
        self.assertTrue(xys_test == xys_true)

        # Multi trace
        xym_test = self.pf.xy([(a, b), (b, a)], return_plot=True)
        xym_test["data"] = [p.to_plotly_json() for p in xym_test["data"]]
        xym_true = self.fopen("template_xym.json")
        self.assertTrue(xym_test == xym_true)

        xy_colors_test = self.pf.xy(
            [(a, b), (a, c), (c, c)],
            modes=["markers", "markers+lines", "lines"],
            colors=[c, "red", "blue"],
            return_plot=True,
        )
        xy_colors_test["data"] = [p.to_plotly_json() for p in xy_colors_test["data"]]
        xy_colors_true = self.fopen("template_xy_colors.json")
        self.assertTrue(xy_colors_test == xy_colors_true)

    def test_heatmap_basic(self):
        hmb_test = self.pf.heatmap_basic([a, b, c], xlabels, ylabels, return_plot=True)
        hmb_test["data"] = [p.to_plotly_json() for p in hmb_test["data"]]
        hmb_true = self.fopen("template_hmb.json")
        self.assertEqual(hmb_test, hmb_true)

    def test_histogram(self):
        his_test = self.pf.histogram(a + b + c, n_bins=5, return_plot=True)
        his_test["data"] = [p.to_plotly_json() for p in his_test["data"]]
        his_true = self.fopen("template_his.json")
        self.assertTrue(his_test == his_true)

    def test_bar(self):
        bar_test = self.pf.bar(x=a, y=b, labels=xlabels, return_plot=True)
        bar_test["data"] = [p.to_plotly_json() for p in bar_test["data"]]
        bar_true = self.fopen("template_bar.json")
        self.assertTrue(bar_test == bar_true)

    def test_parallel_coordinates(self):
        pcp_test = self.pf.parallel_coordinates([a, b], cols=xlabels, return_plot=True)
        pcp_test["data"] = [p.to_plotly_json() for p in pcp_test["data"]]
        pcp_true = self.fopen("template_pcp.json")
        self.assertTrue(pcp_test == pcp_true)

    def test_violin(self):
        vio_test = self.pf.violin([a, b, c, b, a, c, b], cols=xlabels, return_plot=True)["layout"]
        vio_test = vio_test.to_plotly_json()
        vio_true = self.fopen("template_vio.json")

        # Avoid errors from CircleCI's different plotly config
        for vio in [vio_test, vio_true]:
            vio["xaxis"]["range"] = [-0.167009, 0.167009]
        self.assertDictEqual(vio_test, vio_true)

    def test_scatter_matrix(self):
        scm_test = self.pf.scatter_matrix([a, b, c], return_plot=True)["layout"]
        scm_test = scm_test.to_plotly_json()
        scm_true = self.fopen("template_scm.json")
        self.assertTrue(scm_test == scm_true)

    def test_heatmap_df(self):

        df = pd.DataFrame(data=np.asarray([ah, bh, ch]).T, columns=["ah", "bh", "ch"])
        x_labels = ["low", "high"]
        y_labels = ["small", "large"]
        with self.assertWarns(UserWarning):
            hmdf_test = self.pf.heatmap_df(df, x_labels=x_labels, y_labels=y_labels, return_plot=True)
        hmdf_true = self.fopen("template_hmdf.json")
        self.assertTrue(hmdf_test, hmdf_true)

    def test_triangle(self):
        df = pd.DataFrame(np.random.rand(50, 3), columns=list("qwe"))
        triangle_test = self.pf.triangle(df[["q", "w", "e"]], return_plot=True)
        triangle_true = self.fopen("template_triangle.json")
        self.assertTrue(triangle_test, triangle_true)

if __name__ == "__main__":
    # refresh_json(open_plots=True)
    unittest.main()


import numpy as np

class Normalization:
    def normalize_x(self, x_vector: np.ndarray):
        pass

    def denormalize_x(self, x_vector: np.ndarray):
        pass

    def normalize_y(self, y_vector: np.ndarray):
        pass

    def denormalize_y(self, y_vector: np.ndarray):
        pass


import numpy as np

name = ['Cat', 'mouse', 'Fluffy', 'Whiskers']
age = [5, 4, 11, 34]
weight = [12.0, 5.5, 23.0, 45.5]

dateformat = {'names':('name','age','weight'),'formats':('U10','i4','f8')}

x = np.zeros(4,dtype=dateformat)
#print(x)
#print(x.dtype)

x['name'] = name
x['age'] = age
x['weight'] = weight

print(x)

print(x['name'])

print(x[x['weight']>15]['name'])