```python
from bs4 import BeautifulSoup
from tqdm import tqdm
import requests
from functions import *
import os
import re
from datetime import datetime
import csv
import pandas as pd
import json
pd.set_option("display.max_rows", None, "display.max_columns", None, "max_colwidth", None)
```

# 1. Data ollection

At first, we needed to get the links of the anime in the first 400 pages in here [top animes ever list](https://myanimelist.net/topanime.php). Then, after collecting all the urls, we needed to download their html pages, in order to store them in a local folder and to use them without any kind of problem. After the crawling, the last thing to do was to parse the downloaded pages, i.e. taking only the requested information, and to put each anime in a tsv file.

At the end we could start creating the Search Engine!

## 1.1 Get the list of animes

**THE CELL BELOW MUST BE RUNNED JUST ONE TIME, COMMENT IT**

```python
# get_link('https://myanimelist.net/topanime.php?limit=', 'anime_links.txt')
```

## 1.2 Crawl animes

```python
# how many urls has the text file
with open('anime_links.txt', 'r', encoding='utf-8') as file:
    urls = list(file.read().splitlines())
    len_list = len(urls)
    print("Numbers of anime founded:", len_list)

print("Numbers of pages founded:", len_list//50+1)
```

    Numbers of anime founded: 19130
    Numbers of pages founded: 383

As we can see in the cell below, there were not 400 pages, but only 383, and also there were only 19130 and not 20000, but this is not the only "problem" that we found...

**THE CELL BELOW MUST BE RUNNED JUST ONE TIME, COMMENT IT**

We used this cell to create the folders to store the html pages.

```python
# os.mkdir('pages')
# for i in range(len_list//50 + 1):
#     os.mkdir(f'pages/page_{i + 1}')
```

**THE CELL BELOW MUST BE RUNNED JUST ONE TIME, COMMENT IT**

This cell was used to crawl each anime.

```python
# change the start_ind value if the process stops
# and the crawling will continue from that index
# change stop_ind value to recover only the pages
# from start_ind to stop_ind

# start_ind = 0
# stop_ind = len_list
# crawl_html(start_ind)
```

## 1.3 Parse downloaded pages

After all the crawling, we found out that this anime, `article_7242.html` and `article_15009.html`, were missing, because the anime urls do not exist:

https://myanimelist.net/anime/2644/Doraemon__Treasure_of_the_Shinugumi_Mountain

https://myanimelist.net/anime/43247/Bing_Di_Lian

**THE CELL BELOW MUST BE RUNNED JUST ONE TIME, COMMENT IT**

We used this cell to create the pages to store the tsv file for each anime.

```python
# os.mkdir('pages_tsv')
# for i in range(len_list//50 + 1):
#     os.mkdir(f'pages_tsv/page_{i + 1}')
```

In the cell below we store the information requested for each anime in their respective tsv file, by storing in the first row the columns' names, and in the second row the information. We already executed the cell, so there is no need to do it again and we can comment it!

```python
# for i in tqdm(range(len_list)):
#     # the files with the indexes below do not exist
#     if i == 7241 or i == 15008:
#         continue
#     page = i//50 + 1
#     with open(f'pages/page_{page}/article_{i+1}.html', 'r', encoding='utf-8') as file:
#         soup = BeautifulSoup(file, 'html.parser')
#         with open(f'pages_tsv/page_{page}/anime_{i+1}.tsv', 'w', encoding='utf-8') as tsv_file:
#             tsv = csv.writer(tsv_file, delimiter='\t')
#             tsv.writerow(['animeTitle', 'animeType', 'animeNumEpisode', 'releaseDate', 'endDate', 'animeNumMembers', 'animeScore', 'animeUsers', 'animeRank', 'animePopularity', 'animeDescription', 'animeRelated', 'animeCharacters', 'animeVoices', 'animeStaff'])
#             tsv.writerow([get_title(soup), get_type(soup), get_num_ep(soup), get_dates(soup)[0], get_dates(soup)[1], get_memb(soup), get_score(soup), get_users(soup), get_rank(soup), get_pop(soup), get_descr(soup), get_rel_an(soup), get_char(soup), get_voices(soup), get_staff(soup)])

```

# 2. Search Engine

## 2.1. Conjunctive query

```python
# download all the nltk files needed
download()

# creates the vocabulary of every word in the Synopsis of every anime
# and stores it in a json file
create_vocab()
```

    [nltk_data] Downloading package punkt to
    [nltk_data]     C:\Users\clara\AppData\Roaming\nltk_data...
    [nltk_data]   Unzipping tokenizers\punkt.zip.
    [nltk_data] Downloading package stopwords to
    [nltk_data]     C:\Users\clara\AppData\Roaming\nltk_data...
    [nltk_data]   Unzipping corpora\stopwords.zip.
    100%|██████████| 383/383 [01:22<00:00,  4.62it/s]

### 2.1.1) Create your index!

```python
# creates the inverted index dictionary and stores it in a json file
invertedIndex()
```

    100%|██████████| 383/383 [00:26<00:00, 14.42it/s]

### 2.1.2) Execute the query

The cell below is used to open the vocabulary's terms, the inverted_index and the anime_links and to store them in global variables to use in other functions.

```python
# opening the vocabulary
voc_json = open('vocabulary.json', 'r', encoding='utf-8')
vocabulary = json.load(voc_json)
voc_json.close()

# opening the inverted_index
inv_ind_json = open('inverted_index.json', 'r', encoding='utf-8')
inverted_index = json.load(inv_ind_json)
inv_ind_json.close()

# opening the anime_links
list_url_txt = open('anime_links.txt', 'r', encoding='utf-8')
list_url = list_url_txt.read().splitlines()
list_url_txt.close()
```

```python
# taking in input the query
query = input("Insert the query for the inverted index: ")
assert len(query) > 0, "The query is empty!!"
# stemming the query
query_stemmed = text_mining(query)
print("Query:", query)
```

    Query: saiyan race

```python
# creating index query dictionary
query_dict = dict()
for word in query_stemmed:
    if word in vocabulary.keys():
        query_dict[vocabulary[word]] = inverted_index[str(vocabulary[word])]
```

```python
# saving the inverted_index of the query
query_index = list(query_dict.keys())

# searching for the documents requested from the query
doc_list = set(query_dict[query_index[0]])
for query_word in query_index[1:]:
    doc_list.intersection_update(query_dict[query_word])

# creating a pandas dataframe for the final result
doc_df = pd.DataFrame(columns=["animeTitle", "animeDescription", "Url"])
for doc in doc_list:
    i = int(''.join(re.findall(r'\d+', doc)))
    doc_page = (i-1)//50 + 1
    path = f'pages_tsv/page_{doc_page}/anime_{i}.tsv'
    tsv_file = open(path, 'r', encoding='utf-8')
    anime_tsv = csv.DictReader(tsv_file, delimiter='\t')
    anime = anime_tsv.__next__()
    doc_df.loc[doc, ["animeTitle", "animeDescription", "Url"]] = [anime["animeTitle"], anime["animeDescription"], list_url[i-1]]
```

```python
doc_d = dict(selector="th",
             props=[('text-align', 'center')])
def make_clickable(val):
    return '<a href="{}">{}</a>'.format(val, val)

doc_df.style.set_properties(**{'text-align':'center'}).set_table_styles([doc_d]).format({'Url': make_clickable})
```

        	        animeTitle	        animeDescription	        Url

                        document_365
                        Dragon Ball Z
                        Five years after winning the World Martial Arts tournament, Gokuu is now living a peaceful life with his wife and son. This changes, however, with the arrival of a mysterious enemy named Raditz who presents himself as Gokuu's long-lost brother. He reveals that Gokuu is a warrior from the once powerful but now virtually extinct Saiyan race, whose homeworld was completely annihilated. When he was sent to Earth as a baby, Gokuu's sole purpose was to conquer and destroy the planet; but after suffering amnesia from a head injury, his violent and savage nature changed, and instead was raised as a kind and well-mannered boy, now fighting to protect others.

With his failed attempt at forcibly recruiting Gokuu as an ally, Raditz warns Gokuu's friends of a new threat that's rapidly approaching Earth—one that could plunge Earth into an intergalactic conflict and cause the heavens themselves to shake. A war will be fought over the seven mystical dragon balls, and only the strongest will survive in Dragon Ball Z.

[Written by MAL Rewrite]
                        https://myanimelist.net/anime/813/Dragon_Ball_Z

                        document_1035
                        Dragon Ball Kai
                        Five years after the events of Dragon Ball, martial arts expert Gokuu is now a grown man married to his wife Chi-Chi, with a four-year old son named Gohan. While attending a reunion on Turtle Island with his old friends Master Roshi, Krillin, Bulma and others, the festivities are interrupted when a humanoid alien named Raditz not only reveals the truth behind Gokuu's past, but kidnaps Gohan as well.

With Raditz displaying power beyond anything Gokuu has seen before, he is forced to team up with his old nemesis, Piccolo, in order to rescue his son. But when Gokuu and Piccolo reveal the secret of the seven mystical wish-granting Dragon Balls to Raditz, he informs the duo that there is more of his race, the Saiyans, and they won’t pass up an opportunity to seize the power of the Dragon Balls for themselves.

These events begin the saga of Dragon Ball Kai, a story that finds Gokuu and his friends and family constantly defending the galaxy from increasingly more powerful threats. Bizarre, comical, heartwarming and threatening characters come together in a series of battles that push the powers and abilities of Gokuu and his friends beyond anything they have ever experienced.
                        https://myanimelist.net/anime/6033/Dragon_Ball_Kai

                        document_401
                        Dragon Ball Super: Broly
                        Forty-one years ago on Planet Vegeta, home of the infamous Saiyan warrior race, King Vegeta noticed a baby named Broly whose latent power exceeded that of his own son. Believing that Broly's power would one day surpass that of his child, Vegeta, the king sends Broly to the desolate planet Vampa. Broly's father Paragus follows after him, intent on rescuing his son. However, his ship gets damaged, causing the two to spend years trapped on the barren world, unaware of the salvation that would one day come from an unlikely ally.

Years later on Earth, Gokuu Son and Prince Vegeta—believed to be the last survivors of the Saiyan race—are busy training on a remote island. But their sparring is interrupted when the appearance of their old enemy Frieza drives them to search for the last of the wish-granting Dragon Balls on a frozen continent. Once there, Frieza shows off his new allies: Paragus and the now extremely powerful Broly. A legendary battle that shakes the foundation of the world ensues as Gokuu and Vegeta face off against Broly, a warrior without equal whose rage is just waiting to be unleashed.

[Written by MAL Rewrite]
                        https://myanimelist.net/anime/36946/Dragon_Ball_Super__Broly

                        document_1469
                        Dragon Ball Z Special 1: Tatta Hitori no Saishuu Kessen
                        Bardock, Son Goku's father, is a low-ranking Saiyan soldier who was given the power to see into the future by the last remaining alien on a planet he just destroyed. He witnesses the destruction of his race and must now do his best to stop Frieza's impending massacre.

(Source: ANN)
                        https://myanimelist.net/anime/986/Dragon_Ball_Z_Special_1__Tatta_Hitori_no_Saishuu_Kessen

## 2.2) Conjunctive query & Ranking score

### 2.2.1) Inverted index

We create two dictionaries:
- inverted_term such that for each word we have the list of documents in which it is contained in, and the relative tfIdf score.
- inverte_doc such that for each document we have the sum of the squares of the tfidf, we will use this dictonary in the execute query.

We compute the tf-idf as $tf*idf$:
- $tf=\frac{n_i}{|d|}$, n is the number of occurences of the i-th word in the document and |d| is the number of words in the document
- $idf=log_{10}\left(\frac{N}{n_d}\right)$, N is the total number of documents and $n_d$ is the number of documents contaning the word

```python
# creates the inverted_index_tfidf and inverted_doc dictionaries and stores them in a json file
invertedIndex_tfidf(vocabulary, inverted_index)
```

    100%|██████████| 383/383 [00:27<00:00, 13.89it/s]

### 2.2.2) Execute the query

The cell below is used to open the inverted_index_tfidf and the inverted_doc and to store them in global variables to use in other functions.

```python
# opening the inverted_index
inv_ind_tfidf_json = open('inverted_index_tfidf.json', 'r', encoding='utf-8')
inverted_index_tfidf = json.load(inv_ind_tfidf_json)
inv_ind_tfidf_json.close()

# opening the anime_links
inv_doc_json = open('inverted_doc.json', 'r', encoding='utf-8')
inverted_doc = json.load(inv_doc_json)
inv_doc_json.close()
```

Given a query we get the set of documents containing all the words in the query and sort them according to their similarity to the query

- First we consider only the documents that contain all the words in the query.

- We create a dictionary called numerator such that for each document we have the tf-idf sum of the words in the query in reference to the document.

- We compute the cosine similarity for each document as $ cos(\theta) = \frac{(\vec{q} \cdot \vec{d})}{(|{\vec{q}}| \cdot |{\vec{d}}|)}$ where:

 $(\vec{q} \cdot \vec{d})$ is the intersection of the document and the query vectors: we used the numerator dictonary argument for each document as intersection;

 $|{\vec{q}}| \cdot |{\vec{d}}|$  are the norms of the document and query vectors . We compute $|{\vec{q}}|$ as the square root of the length of the query (because the query vector has only components equal to 1 corresponding to the query words). We compute $|{\vec{d}}|$ as the square root of the sum of the squares of the tf-idf of all words in the document (so we use the inverted_doc dictonary to compute the document norm)

- Then we create the result dictonary to store for each document the corresponding cosine similarity to the query.

```python
# taking in input the query
query_2 = input("Insert the query for the inverted index tfidf: ")
k = int(input("Insert k: "))
assert len(query_2) > 0, "The query is empty!!"
assert isinstance(k, int), "k should be an integer!!"
# stemming the query
query_stemmed_2 = text_mining(query_2)
print("Query:", query_2)
print('k:', k)
```

    Query: saiyan race
    k: 3

```python
# creating a pandas dataframe for the final result
doc_df_2 = pd.DataFrame(columns=["animeTitle", "animeDescription", "Url", "Similarity"])
final_doc, result = top_k_documents(k, query=query_stemmed_2, inverted_index=inverted_index, inverted_index_tfidf=inverted_index_tfidf, inverted_doc=inverted_doc, vocabulary=vocabulary)
for doc in final_doc:
    i = int(''.join(re.findall(r'\d+', doc)))
    doc_page = (i-1)//50 + 1
    path = f'pages_tsv/page_{doc_page}/anime_{i}.tsv'
    tsv_file = open(path, 'r', encoding='utf-8')
    anime_tsv = csv.DictReader(tsv_file, delimiter='\t')
    anime = anime_tsv.__next__()
    doc_df_2.loc[doc, ["animeTitle", "animeDescription", "Url", "Similarity"]] = [anime["animeTitle"], anime["animeDescription"], list_url[i-1], result[doc]]
```

```python
doc_dict = dict(selector="th",
                props=[('text-align', 'center')])
def make_clickable(val):
    return '<a href="{}">{}</a>'.format(val, val)

doc_df_2.style.set_properties(**{'text-align':'center'}).set_table_styles([doc_dict]).format({'Url': make_clickable})
```

        	        animeTitle	        animeDescription	        Url	        Similarity

                        document_1469
                        Dragon Ball Z Special 1: Tatta Hitori no Saishuu Kessen
                        Bardock, Son Goku's father, is a low-ranking Saiyan soldier who was given the power to see into the future by the last remaining alien on a planet he just destroyed. He witnesses the destruction of his race and must now do his best to stop Frieza's impending massacre.

(Source: ANN)
                        https://myanimelist.net/anime/986/Dragon_Ball_Z_Special_1__Tatta_Hitori_no_Saishuu_Kessen
                        0.320894

                        document_401
                        Dragon Ball Super: Broly
                        Forty-one years ago on Planet Vegeta, home of the infamous Saiyan warrior race, King Vegeta noticed a baby named Broly whose latent power exceeded that of his own son. Believing that Broly's power would one day surpass that of his child, Vegeta, the king sends Broly to the desolate planet Vampa. Broly's father Paragus follows after him, intent on rescuing his son. However, his ship gets damaged, causing the two to spend years trapped on the barren world, unaware of the salvation that would one day come from an unlikely ally.

Years later on Earth, Gokuu Son and Prince Vegeta—believed to be the last survivors of the Saiyan race—are busy training on a remote island. But their sparring is interrupted when the appearance of their old enemy Frieza drives them to search for the last of the wish-granting Dragon Balls on a frozen continent. Once there, Frieza shows off his new allies: Paragus and the now extremely powerful Broly. A legendary battle that shakes the foundation of the world ensues as Gokuu and Vegeta face off against Broly, a warrior without equal whose rage is just waiting to be unleashed.

[Written by MAL Rewrite]
                        https://myanimelist.net/anime/36946/Dragon_Ball_Super__Broly
                        0.085567

                        document_365
                        Dragon Ball Z
                        Five years after winning the World Martial Arts tournament, Gokuu is now living a peaceful life with his wife and son. This changes, however, with the arrival of a mysterious enemy named Raditz who presents himself as Gokuu's long-lost brother. He reveals that Gokuu is a warrior from the once powerful but now virtually extinct Saiyan race, whose homeworld was completely annihilated. When he was sent to Earth as a baby, Gokuu's sole purpose was to conquer and destroy the planet; but after suffering amnesia from a head injury, his violent and savage nature changed, and instead was raised as a kind and well-mannered boy, now fighting to protect others.

With his failed attempt at forcibly recruiting Gokuu as an ally, Raditz warns Gokuu's friends of a new threat that's rapidly approaching Earth—one that could plunge Earth into an intergalactic conflict and cause the heavens themselves to shake. A war will be fought over the seven mystical dragon balls, and only the strongest will survive in Dragon Ball Z.

[Written by MAL Rewrite]
                        https://myanimelist.net/anime/813/Dragon_Ball_Z
                        0.067677

To check that we were computing the cosine similarity in the right way, we did some tests considering the description of some documents as a query.

In the first test we used as a query the description of document 401 and the cosine similarity is 0.65, it is a high similarity value, but the value should have been around 1 (having used the document description itself). This does not happen because the description of the 401 document is very long (the number of words is high) and therefore the cosine similarity is less reliable.

In the second test instead we used the description of document 203 which has only 7 words and in this case the cosine similarity is 0.96.

#### FIRST TEST
Using all anime description of document_401

```python
# taking in input the query
query_2 = input("Insert the query for the first test: ")
k = int(input("Insert k: "))
assert len(query_2) > 0, "The query is empty!!"
assert isinstance(k, int), "k should be an integer!!"
# stemming the query
query_stemmed_2 = text_mining(query_2)
print("Query:", query_2)
print('k:', k)
```

    Query: Forty-one years ago on Planet Vegeta, home of the infamous Saiyan warrior race, King Vegeta noticed a baby named Broly whose latent power exceeded that of his own son. Believing that Broly's power would one day surpass that of his child, Vegeta, the king sends Broly to the desolate planet Vampa. Broly's father Paragus follows after him, intent on rescuing his son. However, his ship gets damaged, causing the two to spend years trapped on the barren world, unaware of the salvation that would one day come from an unlikely ally. Years later on Earth, Gokuu Son and Prince Vegeta—believed to be the last survivors of the Saiyan race—are busy training on a remote island. But their sparring is interrupted when the appearance of their old enemy Frieza drives them to search for the last of the wish-granting Dragon Balls on a frozen continent. Once there, Frieza shows off his new allies: Paragus and the now extremely powerful Broly. A legendary battle that shakes the foundation of the world ensues as Gokuu and Vegeta face off against Broly, a warrior without equal whose rage is just waiting to be unleashed. [Written by MAL Rewrite]
    k: 1

```python
# creating a pandas dataframe for the final result
doc_df_2 = pd.DataFrame(columns=["animeTitle", "animeDescription", "Url", "Similarity"])
final_doc, result = top_k_documents(k, query=query_stemmed_2, inverted_index=inverted_index, inverted_index_tfidf=inverted_index_tfidf, inverted_doc=inverted_doc, vocabulary=vocabulary)
for doc in final_doc:
    i = int(''.join(re.findall(r'\d+', doc)))
    doc_page = (i-1)//50 + 1
    path = f'pages_tsv/page_{doc_page}/anime_{i}.tsv'
    tsv_file = open(path, 'r', encoding='utf-8')
    anime_tsv = csv.DictReader(tsv_file, delimiter='\t')
    anime = anime_tsv.__next__()
    doc_df_2.loc[doc, ["animeTitle", "animeDescription", "Url", "Similarity"]] = [anime["animeTitle"], anime["animeDescription"], list_url[i-1], result[doc]]
```

```python
doc_dict = dict(selector="th",
                props=[('text-align', 'center')])
def make_clickable(val):
    return '<a href="{}">{}</a>'.format(val, val)

doc_df_2.style.set_properties(**{'text-align':'center'}).set_table_styles([doc_dict]).format({'Url': make_clickable})
```

        	        animeTitle	        animeDescription	        Url	        Similarity

                        document_401
                        Dragon Ball Super: Broly
                        Forty-one years ago on Planet Vegeta, home of the infamous Saiyan warrior race, King Vegeta noticed a baby named Broly whose latent power exceeded that of his own son. Believing that Broly's power would one day surpass that of his child, Vegeta, the king sends Broly to the desolate planet Vampa. Broly's father Paragus follows after him, intent on rescuing his son. However, his ship gets damaged, causing the two to spend years trapped on the barren world, unaware of the salvation that would one day come from an unlikely ally.

Years later on Earth, Gokuu Son and Prince Vegeta—believed to be the last survivors of the Saiyan race—are busy training on a remote island. But their sparring is interrupted when the appearance of their old enemy Frieza drives them to search for the last of the wish-granting Dragon Balls on a frozen continent. Once there, Frieza shows off his new allies: Paragus and the now extremely powerful Broly. A legendary battle that shakes the foundation of the world ensues as Gokuu and Vegeta face off against Broly, a warrior without equal whose rage is just waiting to be unleashed.

[Written by MAL Rewrite]
                        https://myanimelist.net/anime/36946/Dragon_Ball_Super__Broly
                        0.655309

#### SECOND TEST
Using the description of document_203

```python
# taking in input the query
query_2 = input("Insert the query for the second test: ")
k = int(input("Insert k: "))
assert len(query_2) > 0, "The query is empty!!"
assert isinstance(k, int), "k should be an integer!!"
# stemming the query
query_stemmed_2 = text_mining(query_2)
print("Query:", query_2)
print('k:', k)
```

    Query: Two special episodes bundled in the fourth and fifth volume of the Blu-ray/DVD.
    k: 1

```python
# creating a pandas dataframe for the final result
doc_df_2 = pd.DataFrame(columns=["animeTitle", "animeDescription", "Url", "Similarity"])
final_doc, result = top_k_documents(k, query=query_stemmed_2, inverted_index=inverted_index, inverted_index_tfidf=inverted_index_tfidf, inverted_doc=inverted_doc, vocabulary=vocabulary)
for doc in final_doc:
    i = int(''.join(re.findall(r'\d+', doc)))
    doc_page = (i-1)//50 + 1
    path = f'pages_tsv/page_{doc_page}/anime_{i}.tsv'
    tsv_file = open(path, 'r', encoding='utf-8')
    anime_tsv = csv.DictReader(tsv_file, delimiter='\t')
    anime = anime_tsv.__next__()
    doc_df_2.loc[doc, ["animeTitle", "animeDescription", "Url", "Similarity"]] = [anime["animeTitle"], anime["animeDescription"], list_url[i-1], result[doc]]
```

```python
doc_dict = dict(selector="th",
                props=[('text-align', 'center')])
def make_clickable(val):
    return '<a href="{}">{}</a>'.format(val, val)

doc_df_2.style.set_properties(**{'text-align':'center'}).set_table_styles([doc_dict]).format({'Url': make_clickable})
```

        	        animeTitle	        animeDescription	        Url	        Similarity

                        document_203
                        Natsume Yuujinchou Go Specials
                        Two special episodes bundled in the fourth and fifth volume of the Blu-ray/DVD.
                        https://myanimelist.net/anime/34534/Natsume_Yuujinchou_Go_Specials
                        0.961133

# 3. Define a new Score!

We define the new score as:
\begin{equation}
score={\sum_{j∈columns}(score_j)}
\end{equation}
where $columns={animeTitle, animeType, ..., animeStaff}$

We compute the scores in this way:
- the title score: $score_t={\sum_{i∈query}\frac{2*n_i}{len(title)+len(query)}}$, where $n_i$ is the number of occurrences of $word_i$ of the query in the title. We are considering how many times the query word appears in the title, normalizing by the sum of query length and the title length. We decided to multiply the numerator by two in order to try to give greater importance of similarity to the queries with a word that appeared in the title.
- the type score: $score_p={\sum_{i∈query}\frac{n_i}{2}}$, where $n_i$ is different from zero, if in the query is specified the anime type, in that case we consider as score: $n_i$ divided by two to not give much importance to the score type.
- score_episode, score_members, score_animeScore, score_users, score_rank, score_popularity are computed such that their sum is equal to one; we give much importance if the query match with score_animeScore and with score_rank, score_episode. The others are less significant in the similarity in our opinion, so we did not give them much weight.
- the description score: $score_d={\sum_{i∈query}\frac{2*d_i}{len(description)+len(query)}}$, where $d_i$ is the occurrences of the $word_i$ of the query in the description. We compute this score with the same rules of the title_score.
- the releaseDate_score and endDate_score: we consider these scores equal to 0.5 if in the query appears an year that coincides with the releasedate or enddate year. We compute these scores taking into account only the year because we think the user is more likely to do a search by remembering only the releasedate or enddate year rather than the exact date with day and month.
- the score_staff, score_characters, score_voice: we compute these scores adding 0.5 every time a name in the query appears in the list of strings. We decided to add 0.5 because in this way, if the users only enter the name or the surname of the character/staff/voice the relative score will be 0.5, instead if the users enters both the name and the surname the relative score is equal to 1 (as the similarity is more precise).

```python
query = input("insert the query for the new score: ")
k = int(input("insert k: "))
assert len(query) > 0, "The query is empty!!"
assert isinstance(k, int), "k should be an integer!!"
# stemming the query
print("Query:", query)
print('k:', k)
```

    Query: gintama enchousen Youichi 2018 64
    k: 5

```python
new_score(query)
```

    100%|██████████| 383/383 [14:39<00:00,  2.30s/it]

```python
# opening the score_dict
score_dict_json = open('score_dict.json', 'r', encoding='utf-8')
score_dict = json.load(score_dict_json)
score_dict_json.close()

# opening the heap list
heap_json = open('heap.json', 'r', encoding='utf-8')
heap = json.load(heap_json)
heap_json.close()
```

```python
# creating a pandas dataframe for the final result
doc_df = pd.DataFrame(columns=["animeTitle", "animeDescription", "Url", "Similarity"])
final_doc, final_score_dict = top_k_documents(k, heap=heap, score_dict=score_dict)
for doc in final_doc:
    i = int(''.join(re.findall(r'\d+', doc)))
    doc_page = (i-1)//50 + 1
    path = f'pages_tsv/page_{doc_page}/anime_{i}.tsv'
    tsv_file = open(path, 'r', encoding='utf-8')
    anime_tsv = csv.DictReader(tsv_file, delimiter='\t')
    anime = anime_tsv.__next__()
    doc_df.loc[doc, ["animeTitle", "animeDescription", "Url", "Similarity"]] = [anime["animeTitle"], anime["animeDescription"], list_url[i-1], final_score_dict[doc]]
```

```python
doc_dict = dict(selector="th",
                props=[('text-align', 'center')])
def make_clickable(val):
    return '<a href="{}">{}</a>'.format(val, val)

doc_df.style.set_properties(**{'text-align':'center'}).set_table_styles([doc_dict]).format({'Url': make_clickable})
```

        	        animeTitle	        animeDescription	        Url	        Similarity

                        document_9
                        Gintama': Enchousen
                        While Gintoki Sakata was away, the Yorozuya found themselves a new leader: Kintoki, Gintoki's golden-haired doppelganger. In order to regain his former position, Gintoki will need the help of those around him, a troubling feat when no one can remember him! Between Kintoki and Gintoki, who will claim the throne as the main character?

In addition, Yorozuya make a trip back down to red-light district of Yoshiwara to aid an elderly courtesan in her search for her long-lost lover. Although the district is no longer in chains beneath the earth's surface, the trio soon learn of the tragic backstories of Yoshiwara's inhabitants that still haunt them. With flashback after flashback, this quest has Yorozuya witnessing everlasting love and protecting it as best they can with their hearts and souls.

Gintama': Enchousen includes moments of action-packed intensity along with their usual lighthearted, slapstick humor for Gintoki and his friends.

[Written by MAL Rewrite]
                        https://myanimelist.net/anime/15417/Gintama__Enchousen
                        0.979227

                        document_10
                        Gintama: The Final
                        New Gintama movie.
                        https://myanimelist.net/anime/39486/Gintama__The_Final
                        0.972222

                        document_22
                        Gintama.: Shirogane no Tamashii-hen - Kouhan-sen
                        Second Season of the final arc of Gintama.
                        https://myanimelist.net/anime/37491/Gintama__Shirogane_no_Tamashii-hen_-_Kouhan-sen
                        0.848485

                        document_15
                        Gintama
                        The Amanto, aliens from outer space, have invaded Earth and taken over feudal Japan. As a result, a prohibition on swords has been established, and the samurai of Japan are treated with disregard as a consequence.

However one man, Gintoki Sakata, still possesses the heart of the samurai, although from his love of sweets and work as a yorozuya, one might not expect it. Accompanying him in his jack-of-all-trades line of work are Shinpachi Shimura, a boy with glasses and a strong heart, Kagura with her umbrella and seemingly bottomless stomach, as well as Sadaharu, their oversized pet dog. Of course, these odd jobs are not always simple, as they frequently have run-ins with the police, ragtag rebels, and assassins, oftentimes leading to humorous but unfortunate consequences.

Who said life as an errand boy was easy?

[Written by MAL Rewrite]
                        https://myanimelist.net/anime/918/Gintama
                        0.833333

                        document_6
                        Gintama'
                        After a one-year hiatus, Shinpachi Shimura returns to Edo, only to stumble upon a shocking surprise: Gintoki and Kagura, his fellow Yorozuya members, have become completely different characters! Fleeing from the Yorozuya headquarters in confusion, Shinpachi finds that all the denizens of Edo have undergone impossibly extreme changes, in both appearance and personality. Most unbelievably, his sister Otae has married the Shinsengumi chief and shameless stalker Isao Kondou and is pregnant with their first child.

Bewildered, Shinpachi agrees to join the Shinsengumi at Otae and Kondou's request and finds even more startling transformations afoot both in and out of the ranks of the the organization. However, discovering that Vice Chief Toushirou Hijikata has remained unchanged, Shinpachi and his unlikely Shinsengumi ally set out to return the city of Edo to how they remember it.

With even more dirty jokes, tongue-in-cheek parodies, and shameless references, Gintama' follows the Yorozuya team through more of their misadventures in the vibrant, alien-filled world of Edo.

[Written by MAL Rewrite]

                        https://myanimelist.net/anime/9969/Gintama
                        0.801462

# 5. Algorithmic question

## Write an algorithm that computes the acceptable solution with the longest possible duration.

**MyAlg**

```
seq = the sequence of request
query = the list of requests
query_index = empty list

for time in query:
    query_index.append(seq.index(time))

new_query=[0 for i in seq]

for i in query_index:
    new_query[i] = seq[i]

max_sum_nonadjacent_element(new_query)
```

- In the first for we write the position of each element of the query in the sequence.
- Then, we define a new vector with length equal to the length of the sequence, where the elements of the query take position according to query_index, and the other values are 0
- we call the function "max_sum_nonadjacent_elements(new_query)" while keeping track of the elements we are summing

**Max sum non-adjacent element(A)**

```
incl_sum = A[0]
excl_sum = 0

for i = 1 to length(A):
    max_sum = max(incl_sum, excl_sum)
    incl_sum = excl_sum + A[i]
    excl_sum = max_sum

max_sum = max(incl_sum, excl_sum)

return max_sum
```

- We take two variables incl_sum and excl_sum and we itialize incl_sum equal to the first element and excl_sum as zero;
- For each element find the maximum of incl_sum and excl_sum;
- Incl_sum will be equal to excl_sum at the previous step plus the current element;
- excl_sum will be the maximum between excl_sum and incl_sum at the previous step;
- After all the steps, we will take the maximum between incl_sum and excl_sum as result.

## Implement a program that given in input an instance in the form given above, gives the optimal solution.

```python
def MyAlg(seq,query):

    query_index=[]
    seq1=seq.copy()

    for time in query:
        try:
            query_index.append(seq.index(time))
            seq[seq.index(time)]=0
        except:
            print('not valid query')
            return

    new_query=[0 for i in seq]

    # creating new_query as explained before
    for i in query_index:
        new_query[i] = seq1[i]

    # using max_sum_nonadjacent_elements(new_query)
    incl_sum = new_query[0]
    excl_sum = 0
    ind1 = [0]             # indeces of the inclusive sum
    ind2 = []              # indeces of the exclusive sum
    indm = ind1[:]         # indeces of max sum
    max_sum = incl_sum

    for i in range(1, len(new_query)):
        incl_sum = excl_sum + new_query[i]
        ind2.append(i)
        excl_sum = max_sum
        ind1 = indm[:]
        if incl_sum > excl_sum:
            max_sum = incl_sum
            indm = ind2[:]
            ind2 = ind1 [:]
        else:
            max_sum = excl_sum
            indm = ind1[:]
            ind2 = ind1[:]

    query_opt = []

    for i in indm:
        query_opt.append(new_query[i])

    print(f"The optimal solution is: {query_opt}")
    print(f"The duration is: {max_sum}")
```

```python
seq=[30, 40, 25, 50, 30, 20]
query=[30, 40, 25, 30]
MyAlg(seq, query)
```

    The optimal solution is: [30, 25, 30]
    The duration is: 85

We added a try/except to avoid an overlap of appointments as shown in the following example:

```python
seq = [30, 40, 25, 50, 30, 20]
query = [30, 40, 25, 50, 30, 20, 20]
MyAlg(seq,query)
```

    not valid query


```python
import matplotlib.pylab as pyl
import numpy as np
import pandas as pd
import sympy as sp
```

```python
SRC_BACKUP_EXCEL_DIR = 'E:/Diablo II Resurrected/backup/excel'
DST_EXCEL_DIR = 'E:/Diablo II Resurrected/mods/kruno/kruno.mpq/data/global/excel'
```

```python
def restore_all():
    import shutil
    shutil.rmtree(DST_EXCEL_DIR)
    shutil.copytree(SRC_BACKUP_EXCEL_DIR, DST_EXCEL_DIR)

restore_all()
```

```python
class Database:
    data: pd.DataFrame

    def __init__(self, name_of_file: str):
        self.path = f'{DST_EXCEL_DIR}/{name_of_file}.txt'
        self.data = pd.read_csv(self.path, sep="\t", dtype=object)

    def save(self):
        self.data.to_csv(self.path, sep='\t', mode='w', index=False)

    def get_property(self, row: int, label: str):
        return self.data.loc[row, label]

    def set_property(self, row: int, label: str, value):
        self.data.loc[row, label] = value

    def mutate_property(self, row: int, label: str, fn):
        x = self.get_property(row, label)
        self.set_property(row, label, fn(x))
```

```python
charstats = Database('charstats')
hirelings = Database('hireling')
weapons = Database('weapons')
misc = Database('misc')
skills = Database('skills')
skilldesc = Database('skilldesc')
monstats = Database('monstats')
monstats2 = Database('monstats2')
missiles = Database('missiles')
levels = Database('levels')
```

Character changes

```python
for i in range(8):
    if i == 5:
        continue
    charstats.set_property(i, 'WalkVelocity', 14)
    charstats.set_property(i, 'RunVelocity', 16)
    charstats.set_property(i, 'ManaRegen', 20) # 20 seconds to reach full mana
    charstats.set_property(i, 'RunDrain', 1)
    charstats.set_property(i, 'stamina', 200)

charstats.set_property(1, 'ManaRegen', 25) # Sorc has warmth for a big boost, and generally the most mana.
charstats.set_property(0, 'ManaRegen', 15)
```

Hireling Changes

```python
for i in range(120):
    new_hp = int(hirelings.get_property(i, 'HP')) * 10
    hirelings.set_property(i, 'HP', new_hp)

mask = monstats.data['AI'].str.contains('Hireable', na=False)
for i in monstats.data.loc[mask].index:
    idx = int(i)

    print(f"{idx}: {monstats.get_property(idx, 'Id')}, {monstats.get_property(idx, '*hcIdx')}")

    monstats.set_property(idx, 'Velocity', 16)
    monstats.set_property(idx, 'Run', 20)

    monstats.set_property(idx, 'threat', 12)

    monstats.set_property(idx, 'aidel', 1)
    monstats.set_property(idx, 'aidel(N)', 1)
    monstats.set_property(idx, 'aidel(H)', 1)

    monstats.set_property(idx, 'aidist', 50)
    monstats.set_property(idx, 'aidist(N)', 50)
    monstats.set_property(idx, 'aidist(H)', 50)

    monstats.set_property(idx, 'coldeffect', -50)
    monstats.set_property(idx, 'coldeffect(N)', -33)
    monstats.set_property(idx, 'coldeffect(H)', -25)

    monstats.mutate_property(idx, 'DamageRegen', lambda x: int(x) + 2)
```

    271: roguehire, 271
    338: act2hire, 338
    359: act3hire, 359
    561: act5hire1, 560
    562: act5hire2, 561

Pet buffs

```python
# Increase Pet defences
mask = monstats.data['AI'].str.contains('NecroPet', na=False)

# Template for values
data = {
    'Id': '',

    'aidel': 1,
    'aidel(N)': 1,
    'aidel(H)': 1,

    'Velocity': 16,
    'Run': 25,

    'Drain': 100,
    'Drain(N)': 100,
    'Drain(H)': 100,
    'DamageRegen': 10,

    'coldeffect': -50,
    'coldeffect(N)': -33,
    'coldeffect(H)': -15,

    'ResDm': 75,
    'ResMa': 75,
    'ResFi': 75,
    'ResLi': 75,
    'ResCo': 75,
    'ResPo': 75,
    'ResDm(N)': 75,
    'ResMa(N)': 75,
    'ResFi(N)': 75,
    'ResLi(N)': 75,
    'ResCo(N)': 75,
    'ResPo(N)': 75,
    'ResDm(H)': 75,
    'ResMa(H)': 75,
    'ResFi(H)': 75,
    'ResLi(H)': 75,
    'ResCo(H)': 75,
    'ResPo(H)': 75,
}

# expand data to match all rows
num_rows = monstats.data[mask].count()[0]
data = {k: [v]*num_rows for k, v in data.items()}

# Fill in the ids
data['Id'] = monstats.data.loc[mask, 'Id']

# add new data to main data, but masked
data = pd.DataFrame.from_dict(data)
monstats.data.loc[mask, data.keys()] = data

monstats.data.loc[mask, list(data.keys())]
```

      Id
      aidel
      aidel(N)
      aidel(H)
      Velocity
      Run
      Drain
      Drain(N)
      Drain(H)
      DamageRegen
      ...
      ResFi(N)
      ResLi(N)
      ResCo(N)
      ResPo(N)
      ResDm(H)
      ResMa(H)
      ResFi(H)
      ResLi(H)
      ResCo(H)
      ResPo(H)

      289
      claygolem
      1
      1
      1
      16
      25
      100
      100
      100
      10
      ...
      75
      75
      75
      75
      75
      75
      75
      75
      75
      75

      290
      bloodgolem
      1
      1
      1
      16
      25
      100
      100
      100
      10
      ...
      75
      75
      75
      75
      75
      75
      75
      75
      75
      75

      291
      irongolem
      1
      1
      1
      16
      25
      100
      100
      100
      10
      ...
      75
      75
      75
      75
      75
      75
      75
      75
      75
      75

      292
      firegolem
      1
      1
      1
      16
      25
      100
      100
      100
      10
      ...
      75
      75
      75
      75
      75
      75
      75
      75
      75
      75

      357
      valkyrie
      1
      1
      1
      16
      25
      100
      100
      100
      10
      ...
      75
      75
      75
      75
      75
      75
      75
      75
      75
      75

      363
      necroskeleton
      1
      1
      1
      16
      25
      100
      100
      100
      10
      ...
      75
      75
      75
      75
      75
      75
      75
      75
      75
      75

      364
      necromage
      1
      1
      1
      16
      25
      100
      100
      100
      10
      ...
      75
      75
      75
      75
      75
      75
      75
      75
      75
      75

      431
      wolf
      1
      1
      1
      16
      25
      100
      100
      100
      10
      ...
      75
      75
      75
      75
      75
      75
      75
      75
      75
      75

      432
      bear
      1
      1
      1
      16
      25
      100
      100
      100
      10
      ...
      75
      75
      75
      75
      75
      75
      75
      75
      75
      75

9 rows × 31 columns

```python
# increase HP of skeles

fields = [
    'minHP',
    'maxHP',
    'MinHP(N)',
    'MaxHP(N)',
    'MinHP(H)',
    'MaxHP(H)',
]

# Before
display(monstats.data.loc[mask, fields])

# Convert and manipulate
monstats.data.loc[mask, fields] = monstats.data.loc[mask, fields].astype('int')
monstats.data.loc[mask, fields] *= 5

# After
monstats.data.loc[mask, fields]
```

      minHP
      maxHP
      MinHP(N)
      MaxHP(N)
      MinHP(H)
      MaxHP(H)

      289
      100
      100
      175
      175
      275
      275

      290
      201
      201
      388
      388
      637
      637

      291
      306
      306
      595
      595
      980
      980

      292
      313
      313
      613
      613
      1013
      1013

      357
      400
      480
      400
      480
      400
      480

      363
      21
      21
      30
      30
      42
      42

      364
      61
      61
      88
      88
      123
      123

      431
      16
      25
      46
      74
      79
      127

      432
      12
      19
      34
      54
      57
      92

      minHP
      maxHP
      MinHP(N)
      MaxHP(N)
      MinHP(H)
      MaxHP(H)

      289
      500
      500
      875
      875
      1375
      1375

      290
      1005
      1005
      1940
      1940
      3185
      3185

      291
      1530
      1530
      2975
      2975
      4900
      4900

      292
      1565
      1565
      3065
      3065
      5065
      5065

      357
      2000
      2400
      2000
      2400
      2000
      2400

      363
      105
      105
      150
      150
      210
      210

      364
      305
      305
      440
      440
      615
      615

      431
      80
      125
      230
      370
      395
      635

      432
      60
      95
      170
      270
      285
      460

```python
# raise skeleton without corpse and enable town summon
data_fields = [
    'srvstfunc', 'srvdofunc', 'cltstfunc', 'cltdofunc',
    'InTown', 'threat', 'TargetCorpse'
]

skills.data.loc[70, data_fields] = ['', 56, '', '', 1, 12, '']
skills.data.loc[80, data_fields] = [15, 31, 20, 31, 1, 9, 1]

monstats.data.loc[80, ['killable']] = 0
```

```python
# [70, 80] = raise skele, raise mage

# Damage % per level
lvl = sp.Symbol('lvl')
e = lvl * 10

display(e)

eqtn = sp.horner(sp.series(e, x0=10, n=2).removeO())
display(eqtn.evalf())

skills.data.loc[70, 'aurastatcalc1'] = str(eqtn)
```

$\displaystyle 10 lvl$

$\displaystyle 10.0 lvl$

```python
# frozen orb
# skills.set_property(64, 'localdelay', 10)
# skills.set_property(64, 'globaldelay', 0)

# Skill synergy; par8 = 2 aka 2%
skills.set_property(64, 'EDmgSymPerCalc', "(skill('Ice Bolt'.blvl) + skill('Cold Mastery'.blvl))*par8")

# skills.set_property(64, 'mana', 65)

skills.set_property(64, 'calc1', "ln12 * (100 + skill('Ice Bolt'.blvl) * 10 + skill('Cold Mastery'.blvl)) / 100")
skills.set_property(64, 'calc2', "ln34 * (100 + skill('Ice Bolt'.blvl) * 10 + skill('Cold Mastery'.blvl)) / 100")
```

```python
# TP, ID books, and keys
misc.set_property(10, 'maxstack', 100)
misc.set_property(11, 'maxstack', 100)
misc.set_property(35, 'maxstack', 100)
```

```python
# Arrows + Bolts
misc.set_property(18, 'minstack', 512)
misc.set_property(18, 'maxstack', 512)

misc.set_property(20, 'minstack', 512)
misc.set_property(20, 'maxstack', 512)
```

```python
# Javs
weapons.set_property(47, 'minstack', 512)
weapons.set_property(47, 'maxstack', 512)
```

```python
# infinite ammo Jav
skills.set_property(2, 'srvstfunc', '')
skills.set_property(2, 'srvdofunc', '')

skills.set_property(2, 'cltstfunc', '')
skills.set_property(2, 'cltdofunc', '')

skills.set_property(2, 'srvmissile', 'javelin')
skills.set_property(2, 'cltmissile', 'javelin')

skills.set_property(2, 'noammo', 1)

# poison jav
for idx in [15, 25, 20, 35, 24, 14]:
    if skills.get_property(idx, 'srvstfunc') == '4':
        print(skills.get_property(idx, 'skill'))
        skills.set_property(idx, 'srvstfunc', '')
        skills.set_property(idx, 'srvdofunc', '')

        skills.set_property(idx, 'cltstfunc', '')
        skills.set_property(idx, 'cltdofunc', '')

    skills.set_property(idx, 'noammo', 1)
    skills.set_property(idx, 'decquant', '')
```

    Poison Javelin
    Plague Javelin
    Lightning Bolt
    Lightning Fury

Assassin Skill Changes

```python
# Assassin's Quickness skill
# Param5 and Param6 = (base duration and duration/lvl)

skills.set_property(258, 'Param5', int(16.666667 * 300 * 5))
skills.set_property(258, 'Param6', 1000)
```

```python
# Wake of Inferno buff [add physical damage and scale it off weapon by 50%]
skills.set_property(272, 'HitShift', 8) # this scales tick damage from [1 -> 8]/8

# Death Sentry Buff
skills.set_property(272, 'EDmgSymPerCalc', "(skill('Lightning Sentry'.lvl))*par8")

# increase shots
mask = skills.data['skilldesc'].str.contains('sentry', na=False)
for i in skills.data[mask]['*Id']:
    idx = int(i)
    skills.set_property(idx, 'Param1', 999)

# secondary skills that have to match the above
mask = skills.data['*Param8 Description'].str.contains('# of shots', na=False)
for i in skills.data[mask]['*Id']:
    idx = int(i)
    skills.set_property(idx, 'Param8', 999)
```

Make Paladin's Prayer good.

```python
data = skills.data
filter = data['skill'].str.contains('Prayer')
data.loc[filter, 'lvlmana'] = 0
# data.loc[filter, 'usemanaondo'] = 1
# hp
fields = ['EMin','EMinLev1','EMinLev2','EMinLev3','EMinLev4','EMinLev5']
display(data.loc[filter, fields])
# The level thresholds are levels 2-8, 9-16, 17-22, 23-28, 29 and beyond
data.loc[filter, fields] = [4, 2, 3, 4, 5, 6]
data.loc[filter, fields]
```

      EMin
      EMinLev1
      EMinLev2
      EMinLev3
      EMinLev4
      EMinLev5

      99
      2
      1
      1
      2
      2
      3

      EMin
      EMinLev1
      EMinLev2
      EMinLev3
      EMinLev4
      EMinLev5

      99
      4
      2
      3
      4
      5
      6

Add a move only skill

```python
# add a move only skill
headers = {k: '' for k in skills.data.columns.values}
data = headers | {
        'skill': 'DoNothing',
        '*Id': 365,
        'skilldesc': 'donothing',
        'leftskill': 1,
        'rightskill': 1,
        'cost add': 0,
        '*eol': 0
    }

skills.data = skills.data.append(data, ignore_index=True)

headers = {k: '' for k in skilldesc.data.columns.values}
data = headers | {
        'skilldesc': 'donothing',
        'SkillPage': 0,
        'SkillRow': 0,
        'SkillColumn': 0,
        'ListRow': 0,
        'IconCel': 0,
        'str name': 'SkillNameDoNothing',
        'str short': 'SkillDescDoNothing',
        'str long': 'SkillDescDoNothing',
        'str alt': 'SkillNameDoNothing',
        '*eol': 0
    }

skilldesc.data = skilldesc.data.append(data, ignore_index=True)

for i in range(8):
    if i == 5:
        continue
    charstats.set_property(i+1, 'Skill 10', 'DoNothing')
```

```python
data = levels.data
density_data = {'MonDen': 4, 'MonDen(N)': 3, 'MonDen(H)': 2}

for name, multiplier in density_data.items():
    data.loc[data[name].notnull(), name] = data[data[name].notnull()][name].astype('int')
    data.loc[data[name].notnull(), name] *= 4

# data[['Name','MonDen']]
```

add a missile that will do the base source damage as splash

```python

data_to_copy = {k: v for k, v in
    zip(missiles.data.columns.values,
    missiles.data[missiles.data['Missile'].str.fullmatch('fireball')].values[0])
}

data = data_to_copy | {
        'Missile': 'meleesplash',
        '*ID': missiles.data.tail(1).index.item() + 1,

        'pCltDoFunc': 1,
        'pCltHitFunc': 1,
        'pSrvDoFunc': 1,
        'pSrvHitFunc': 1,
        'pSrvDmgFunc': '',

        'sHitPar1': 3,
        'cHitPar1': 3,
        'cHitPar2': 1,

        'EType': '',

        'MissileSkill': 1,
        'InitSteps': 0,
        'Skill': 'Attack',
        'Range': 25,

        # 'ExplosionMissile': '',
        # 'CltHitSubMissile1': '',

        'TravelSound': '',
        'HitSound': '',
    }

missiles.data = missiles.data.append(data, ignore_index=True)

# add the last missile to attack
filter = (skills.data['itypea1'].notna() &
                                        (skills.data['itypea1'].str.match('mele') |         skills.data['itypea1'].str.match('spea') |
                                        skills.data['itypea1'].str.match('h2h') |
                                        skills.data['itypea1'].str.match('weap')))
skills.data.loc[filter, ['srvmissile', 'cltmissiled']] = 'meleesplash'
```

```python
# Add a skill to Rogue
# 'Skill3','Mode3','Chance3','ChancePerLvl3','Level3','LvlPerLvl3'

headers = ['Skill3','Mode3','Chance3','ChancePerLvl3','Level3','LvlPerLvl3']
data = ['Decrepify', 4, 50, 8, 1, 1]

scouts = hirelings.data['Hireling'].str.fullmatch('Rogue Scout')
hirelings.data.loc[scouts, headers] = data
```

Increase Monster Density

```python
data = levels.data
density_data = {'MonDen': 1.1, 'MonDen(N)': 1.2, 'MonDen(H)': 1.22}

for name, multiplier in density_data.items():
    data.loc[data[name].notnull(), name] = data[data[name].notnull()][name].astype('int')
    density = data.loc[data[name].notnull(), name].astype('int')
    density = (density * multiplier).round().astype('int').map(lambda x: 100_000 if x > 100_000 else x)
    data.loc[data[name].notnull(), name] = density

data[['Name','MonDen']]
```

      Name
      MonDen

      0
      Null
      0

      1
      Act 1 - Town
      0

      2
      Act 1 - Wilderness 1
      2288

      3
      Act 1 - Wilderness 2
      2288

      4
      Act 1 - Wilderness 3
      2288

      ...
      ...
      ...

      133
      Act 5 - World Stone
      0

      134
      Act 5 - Pandemonium 1
      4646

      135
      Act 5 - Pandemonium 2
      3872

      136
      Act 5 - Pandemonium 3
      3520

      137
      Act 5 - Pandemonium Finale
      0

138 rows × 2 columns

```python
charstats.save()
hirelings.save()
weapons.save()
misc.save()
skills.save()
skilldesc.save()
monstats.save()
missiles.save()
monstats2.save()
levels.save()
```


```python
!pip install datashader
```

    Requirement already satisfied: datashader in /opt/conda/lib/python3.6/site-packages
    Requirement already satisfied: param>=1.6.0 in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: colorcet>=0.9.0 in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: pillow>=3.1.1 in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: pyct[cmd] in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: datashape>=0.5.1 in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: dask[complete]>=0.15.4 in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: scipy in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: numpy>=1.7 in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: xarray>=0.9.6 in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: bokeh in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: toolz>=0.7.4 in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: numba>=0.37.0 in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: scikit-image in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: pandas>=0.20.3 in /opt/conda/lib/python3.6/site-packages (from datashader)
    Requirement already satisfied: olefile in /opt/conda/lib/python3.6/site-packages (from pillow>=3.1.1->datashader)
    Requirement already satisfied: requests; extra == "cmd" in /opt/conda/lib/python3.6/site-packages (from pyct[cmd]->datashader)
    Requirement already satisfied: pyyaml; extra == "cmd" in /opt/conda/lib/python3.6/site-packages (from pyct[cmd]->datashader)
    Requirement already satisfied: multipledispatch>=0.4.7 in /opt/conda/lib/python3.6/site-packages (from datashape>=0.5.1->datashader)
    Requirement already satisfied: python-dateutil in /opt/conda/lib/python3.6/site-packages (from datashape>=0.5.1->datashader)
    Requirement already satisfied: cloudpickle>=0.2.1 in /opt/conda/lib/python3.6/site-packages (from dask[complete]>=0.15.4->datashader)
    Requirement already satisfied: distributed>=1.20 in /opt/conda/lib/python3.6/site-packages (from dask[complete]>=0.15.4->datashader)
    Requirement already satisfied: partd>=0.3.8 in /opt/conda/lib/python3.6/site-packages (from dask[complete]>=0.15.4->datashader)
    Requirement already satisfied: tornado>=4.3 in /opt/conda/lib/python3.6/site-packages (from bokeh->datashader)
    Requirement already satisfied: six>=1.5.2 in /opt/conda/lib/python3.6/site-packages (from bokeh->datashader)
    Requirement already satisfied: packaging>=16.8 in /opt/conda/lib/python3.6/site-packages (from bokeh->datashader)
    Requirement already satisfied: Jinja2>=2.7 in /opt/conda/lib/python3.6/site-packages (from bokeh->datashader)
    Requirement already satisfied: llvmlite>=0.26.0dev0 in /opt/conda/lib/python3.6/site-packages (from numba>=0.37.0->datashader)
    Requirement already satisfied: matplotlib>=1.3.1 in /opt/conda/lib/python3.6/site-packages (from scikit-image->datashader)
    Requirement already satisfied: networkx>=1.8 in /opt/conda/lib/python3.6/site-packages (from scikit-image->datashader)
    Requirement already satisfied: pytz>=2011k in /opt/conda/lib/python3.6/site-packages (from pandas>=0.20.3->datashader)
    Requirement already satisfied: chardet<3.1.0,>=3.0.2 in /opt/conda/lib/python3.6/site-packages (from requests; extra == "cmd"->pyct[cmd]->datashader)
    Requirement already satisfied: idna<2.7,>=2.5 in /opt/conda/lib/python3.6/site-packages (from requests; extra == "cmd"->pyct[cmd]->datashader)
    Requirement already satisfied: urllib3<1.23,>=1.21.1 in /opt/conda/lib/python3.6/site-packages (from requests; extra == "cmd"->pyct[cmd]->datashader)
    Requirement already satisfied: certifi>=2017.4.17 in /opt/conda/lib/python3.6/site-packages (from requests; extra == "cmd"->pyct[cmd]->datashader)
    Requirement already satisfied: click>=6.6 in /opt/conda/lib/python3.6/site-packages (from distributed>=1.20->dask[complete]>=0.15.4->datashader)
    Requirement already satisfied: msgpack-python in /opt/conda/lib/python3.6/site-packages (from distributed>=1.20->dask[complete]>=0.15.4->datashader)
    Requirement already satisfied: psutil in /opt/conda/lib/python3.6/site-packages (from distributed>=1.20->dask[complete]>=0.15.4->datashader)
    Requirement already satisfied: sortedcontainers in /opt/conda/lib/python3.6/site-packages (from distributed>=1.20->dask[complete]>=0.15.4->datashader)
    Requirement already satisfied: tblib in /opt/conda/lib/python3.6/site-packages (from distributed>=1.20->dask[complete]>=0.15.4->datashader)
    Requirement already satisfied: zict>=0.1.3 in /opt/conda/lib/python3.6/site-packages (from distributed>=1.20->dask[complete]>=0.15.4->datashader)
    Requirement already satisfied: locket in /opt/conda/lib/python3.6/site-packages (from partd>=0.3.8->dask[complete]>=0.15.4->datashader)
    Requirement already satisfied: pyparsing>=2.0.2 in /opt/conda/lib/python3.6/site-packages (from packaging>=16.8->bokeh->datashader)
    Requirement already satisfied: MarkupSafe>=0.23 in /opt/conda/lib/python3.6/site-packages (from Jinja2>=2.7->bokeh->datashader)
    Requirement already satisfied: cycler>=0.10 in /opt/conda/lib/python3.6/site-packages/cycler-0.10.0-py3.6.egg (from matplotlib>=1.3.1->scikit-image->datashader)
    Requirement already satisfied: decorator>=4.1.0 in /opt/conda/lib/python3.6/site-packages (from networkx>=1.8->scikit-image->datashader)
    Requirement already satisfied: heapdict in /opt/conda/lib/python3.6/site-packages (from zict>=0.1.3->distributed>=1.20->dask[complete]>=0.15.4->datashader)
    You are using pip version 9.0.1, however version 18.1 is available.
    You should consider upgrading via the 'pip install --upgrade pip' command.

```python
!pip install xarray
```

    Collecting xarray
      Downloading https://files.pythonhosted.org/packages/ab/f3/c3b45f15a82d7c89cbb6f0726d047d5d2b0ec8d1933731c125ac45a38b3d/xarray-0.11.0-py2.py3-none-any.whl (490kB)
        100% |████████████████████████████████| 491kB 704kB/s eta 0:00:01
    Requirement already satisfied: numpy>=1.12 in /opt/conda/lib/python3.6/site-packages (from xarray)
    Requirement already satisfied: pandas>=0.19.2 in /opt/conda/lib/python3.6/site-packages (from xarray)
    Requirement already satisfied: python-dateutil>=2 in /opt/conda/lib/python3.6/site-packages (from pandas>=0.19.2->xarray)
    Requirement already satisfied: pytz>=2011k in /opt/conda/lib/python3.6/site-packages (from pandas>=0.19.2->xarray)
    Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.6/site-packages (from python-dateutil>=2->pandas>=0.19.2->xarray)
    Installing collected packages: xarray
    Successfully installed xarray-0.11.0
    You are using pip version 9.0.1, however version 18.1 is available.
    You should consider upgrading via the 'pip install --upgrade pip' command.

```python
!pip install colorcet
```

    Collecting colorcet
      Downloading https://files.pythonhosted.org/packages/1f/eb/428731f7d86ca2a3cc9d100155732b42854344db58e9c54b9f44419a2565/colorcet-0.9.1.tar.gz (115kB)
        100% |████████████████████████████████| 122kB 2.3MB/s ta 0:00:01
    Building wheels for collected packages: colorcet
      Running setup.py bdist_wheel for colorcet ... done
      Stored in directory: /home/jovyan/.cache/pip/wheels/66/40/ff/e376873956973b2272325f670e4ead84820170de9770cb89f4
    Successfully built colorcet
    Installing collected packages: colorcet
    Successfully installed colorcet-0.9.1
    You are using pip version 9.0.1, however version 18.1 is available.
    You should consider upgrading via the 'pip install --upgrade pip' command.

```python
!pip install hvplot
```

    Requirement already satisfied: hvplot in /opt/conda/lib/python3.6/site-packages
    Requirement already satisfied: pandas in /opt/conda/lib/python3.6/site-packages (from hvplot)
    Requirement already satisfied: holoviews>=1.10.5 in /opt/conda/lib/python3.6/site-packages (from hvplot)
    Requirement already satisfied: bokeh>=0.12.15 in /opt/conda/lib/python3.6/site-packages (from hvplot)
    Requirement already satisfied: pytz>=2011k in /opt/conda/lib/python3.6/site-packages (from pandas->hvplot)
    Requirement already satisfied: numpy>=1.9.0 in /opt/conda/lib/python3.6/site-packages (from pandas->hvplot)
    Requirement already satisfied: python-dateutil>=2.5.0 in /opt/conda/lib/python3.6/site-packages (from pandas->hvplot)
    Requirement already satisfied: param<2.0,>=1.6.1 in /opt/conda/lib/python3.6/site-packages (from holoviews>=1.10.5->hvplot)
    Requirement already satisfied: pyviz-comms>=0.6.0 in /opt/conda/lib/python3.6/site-packages (from holoviews>=1.10.5->hvplot)
    Requirement already satisfied: Jinja2>=2.7 in /opt/conda/lib/python3.6/site-packages (from bokeh>=0.12.15->hvplot)
    Requirement already satisfied: PyYAML>=3.10 in /opt/conda/lib/python3.6/site-packages (from bokeh>=0.12.15->hvplot)
    Requirement already satisfied: tornado>=4.3 in /opt/conda/lib/python3.6/site-packages (from bokeh>=0.12.15->hvplot)
    Requirement already satisfied: pillow>=4.0 in /opt/conda/lib/python3.6/site-packages (from bokeh>=0.12.15->hvplot)
    Requirement already satisfied: six>=1.5.2 in /opt/conda/lib/python3.6/site-packages (from bokeh>=0.12.15->hvplot)
    Requirement already satisfied: packaging>=16.8 in /opt/conda/lib/python3.6/site-packages (from bokeh>=0.12.15->hvplot)
    Requirement already satisfied: MarkupSafe>=0.23 in /opt/conda/lib/python3.6/site-packages (from Jinja2>=2.7->bokeh>=0.12.15->hvplot)
    Requirement already satisfied: olefile in /opt/conda/lib/python3.6/site-packages (from pillow>=4.0->bokeh>=0.12.15->hvplot)
    Requirement already satisfied: pyparsing>=2.0.2 in /opt/conda/lib/python3.6/site-packages (from packaging>=16.8->bokeh>=0.12.15->hvplot)
    You are using pip version 9.0.1, however version 18.1 is available.
    You should consider upgrading via the 'pip install --upgrade pip' command.

This solution uses [Google OR-Tools](https://developers.google.com/optimization/) to optimize the routing for Santa. OR-Tools include a "vehicle routing library" for solving TSPs and a convenient Python interface. Lucky for us, it's all included in Kaggle kernels!

The start point for the route comes from model-based clustering. You can see from the city plot how the density of points varies quite a bit across the grid. The right clustering approach may work better than starting with a grid or k-means clustering. Also, breaking the cities into clusters can scale to millions of points on a typical machine if the need arises.

Here is the overall approach:

* Divide cities into clusters
* Get centers for each cluster
* Find an optimal path across centers
* Find start and stop points for each cluster
* Find an optimal segment for each cluster and assemble

```python
from tqdm import tqdm
import numpy as np
import pandas as pd
from sympy import sieve
import hvplot.pandas #custom
import colorcet as cc
```

## Clustering

#### First Look
Hexagonal binning is often a good way to quickly see the density of overlapping points. Below is a comparison of all points with a bin plot of prime cities. Prime and non-prime cities appear to be be similarly distributed with a few dark, dense areas.

```python
cities = pd.read_csv("../data/raw/cities.csv", index_col=['CityId'])
pnums = list(sieve.primerange(0, cities.shape[0]))
cities['isprime'] = cities.index.isin(pnums)
display(cities.head())

# show all points and density of primes
allpoints = cities.hvplot.scatter('X', 'Y',  width=380, height=350, datashade=True,
                title='All Cities')
colors = list(reversed(cc.kbc))
primedensity = cities[cities.isprime].hvplot.hexbin(x='X', y='Y', width=420, height=350,
                cmap=colors, title='Density of Prime Cities').options(size_index='Count',
                min_scale=0.8, max_scale=0.95)
allpoints + primedensity
```

#### Model-based Clustering

I first tried density-based clustering, but it left too many points that didn't belong to any cluster. In some cases it's ideal to separate out the sparse points, but for TSPs it's not so good. Model-based clustering seems to be a better choice. The number of clusters is set manually. Here I'll choose a relatively high number to ease the burden on the router.

```python
%%time
from sklearn.mixture import GaussianMixture

mclusterer = GaussianMixture(n_components=250, tol=0.01, random_state=66, verbose=1)
cities['mclust'] = mclusterer.fit_predict(cities[['X', 'Y']].values)
nmax = cities.mclust.max()
print("{} clusters".format(nmax+1))
```

Clusters can be of different sizes with model-based clustering. Here's the distribution of cities per cluster and the scatterplot.

```python
histo = cities.hvplot.hist('mclust', ylim=(0,14000), color='tan')

custcolor = cc.rainbow + cc.rainbow
gausses = cities.hvplot.scatter('X', 'Y',  by='mclust', size=5, width=500, height=450,
                datashade=True, dynspread=True, cmap=custcolor)
display(histo, gausses)
```

## Cluster Centers

Here are the geometric centers of all the clusters. The big red dot is the North Pole.

```python
centers = cities.groupby('mclust')['X', 'Y'].agg('mean').reset_index()
def plot_it(df, dotsize, dotcolor, dotalpha):
    p = df.hvplot.scatter('X', 'Y', size=dotsize, xlim=(0,5100), ylim=(0,3400), width=500,
            height=450, hover_cols=['mclust'], color=dotcolor, alpha=dotalpha)
    return p

cents = plot_it(centers, 30, 'darkblue', 0.5)
npole = plot_it(cities.loc[[0]], 100, 'red', 1)
cents*npole
```

## Optimal Path across Centers

OR-Tools can find an optimal path across the clusters. Here is a modified version of the code documented on [Google's TSP page](https://developers.google.com/optimization/routing/tsp).

```python
#%% imports
from scipy.spatial.distance import pdist, squareform
from ortools.constraint_solver import pywrapcp
from ortools.constraint_solver import routing_enums_pb2

#%% functions
def create_mat(df):
    print("building matrix")
    mat = pdist(locations)
    return squareform(mat)

def create_distance_callback(dist_matrix):
    def distance_callback(from_node, to_node):
      return int(dist_matrix[from_node][to_node])
    return distance_callback

status_dict = {0: 'ROUTING_NOT_SOLVED',
               1: 'ROUTING_SUCCESS',
               2: 'ROUTING_FAIL',
               3: 'ROUTING_FAIL_TIMEOUT',
               4: 'ROUTING_INVALID'}

def optimize(df, startnode=None, stopnode=None, fixed=False):
    num_nodes = df.shape[0]
    mat = create_mat(df)
    dist_callback = create_distance_callback(mat)
    search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
#     search_parameters.time_limit_ms = int(1000*60*numminutes)
    search_parameters.solution_limit = num_iters
    search_parameters.first_solution_strategy = (
                                    routing_enums_pb2.FirstSolutionStrategy.LOCAL_CHEAPEST_INSERTION)
    search_parameters.local_search_metaheuristic = (
                            routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)

    if fixed:
        routemodel = pywrapcp.RoutingModel(num_nodes, 1, [startnode], [stopnode])
    else:
        routemodel = pywrapcp.RoutingModel(num_nodes, 1, startnode)
    routemodel.SetArcCostEvaluatorOfAllVehicles(dist_callback)

    print("optimizing {} cities".format(num_nodes))
    assignment = routemodel.SolveWithParameters(search_parameters)

    print("status: ", status_dict.get(routemodel.status()))
    print("travel distance: ",  str(assignment.ObjectiveValue()), "\n")
    return routemodel, assignment

def get_route(df, startnode, stopnode, fixed):
    routemodel, assignment = optimize(df, int(startnode), int(stopnode), fixed)
    route_number = 0
    node = routemodel.Start(route_number)
    route = []
    while not routemodel.IsEnd(node):
        route.append(node)
        node = assignment.Value(routemodel.NextVar(node))
    return route

```

```python
%%time
#%% parameters
num_iters=100

# main
nnode = int(cities.loc[0, 'mclust'])
locations = centers[['X', 'Y']].values
segment = get_route(locations, nnode, 0, fixed=False)
```

```python
opoints = centers.loc[segment]
centersline = opoints.hvplot.line('X', 'Y', xlim=(0,5100), ylim=(0,3400), color='green', width=500,
                            height=450, hover=False)
gausses*cents*npole*centersline
```

The path looks reasonable...

## Start and Stop Points
The next step is to find good start/stop points for each cluster along the path.
* Set the start point for cluster[n] as the point closest to the center of cluster[n-1].
* Set the stop point for cluster[n] as the point closest to the start point of cluster[n+1].

This idea comes from [Proposed Algorithms to solve Big Data traveling salesman problem](https://www.researchgate.net/publication/326325068_Proposed_Algorithms_to_solve_Big_Data_traveling_salesman_problem). Thank you Research Gate!

```python
opoints.reset_index(drop=True, inplace=True) #recall ordered points
cities['clustorder'] = cities.groupby('mclust').cumcount()
```

```python
from sklearn.neighbors import NearestNeighbors

startlist=[0]
neigh = NearestNeighbors(n_neighbors=1, n_jobs=-1)
for i,m in enumerate(opoints.mclust[1:], 0):
    neigh.fit(cities.loc[cities.mclust == m, ['X', 'Y']].values)
    lastcenter = opoints.loc[i, ['X', 'Y']].values.reshape(1, -1)
    closestart = neigh.kneighbors(lastcenter, return_distance=False)
    start = cities.index[(cities.mclust == m) & (cities.clustorder == closestart.item())].values[0]
    startlist.append(start)
opoints['startpt'] = startlist
```

```python
stoplist = []
for i,m in enumerate(opoints.mclust, 1):
    neigh.fit(cities.loc[cities.mclust == m, ['X', 'Y']].values)
    if m != opoints.mclust.values[-1]:
        nextstartnode = opoints.loc[i, 'startpt']
    else:
        nextstartnode = 0
    nextstart = cities.loc[nextstartnode, ['X', 'Y']].values.reshape(1, -1)
    closestop = neigh.kneighbors(nextstart, return_distance=False)
    stop = cities.index[(cities.mclust == m) & (cities.clustorder == closestop.item())].values[0]
    stoplist.append(stop)
opoints['stoppt'] = stoplist

display(cities.head(), opoints.head())
```

 Here's what  the new path looks like. The points are all on cluster borders which makes sense.

```python
coords = cities.loc[opoints.stoppt, ['X', 'Y', 'mclust']]
stops = plot_it(coords, 30, 'darkblue', 0.5)
stopsline = coords.hvplot.line('X', 'Y', xlim=(0,5100), ylim=(0,3400), color='green', width=500,
                            height=450, hover=False)
stopsline*npole*gausses*stops
```

## Optimal Segments for each Cluster
The next step is to iterate through clusters with the solver. The closed form of the solver with start and stop points is being difficult. For now I'll use start points only and take the hit of doubling back to the next cluster.

```python
%%time
num_iters = 100
seglist = []
total_clusts = cities.shape[0]
for i,m in enumerate(opoints.mclust):
    district = cities[cities.mclust == m]
    print("begin cluster {}, {} of {}".format(m, i, opoints.shape[0]-1))

    clstart = opoints.loc[i, 'startpt']
    nnode = district.loc[clstart, 'clustorder']
    clstop = opoints.loc[i, 'stoppt']
    pnode = district.loc[clstop, 'clustorder']
    locations = district[['X', 'Y']].values

    segnodes = get_route(locations, nnode, pnode, fixed=False) #output is type list
    ord_district =  district.iloc[segnodes]
    segment = ord_district.index.tolist()
    seglist.append(segment)

seglist.append([0])
path = np.concatenate(seglist)
```

```python
bestpath = cities.loc[path, ['X', 'Y']]
clustline = bestpath.hvplot.line('X', 'Y', xlim=(0,5100), ylim=(0,3400), width=500,
                            height=450, datashade=True, dynspread=True)
clustline*npole
```

```python
def score_it(path):
    path_df = cities.reindex(path).reset_index()
    path_df['step'] = np.sqrt((path_df.X - path_df.X.shift())**2 + (path_df.Y - path_df.Y.shift())**2)
    path_df['step_adj'] = np.where((path_df.index) % 10 != 0, path_df.step, path_df.step +
                            path_df.step*0.1*(~path_df.CityId.shift().isin(pnums)))
    return path_df.step_adj.sum()

display(score_it(path))

sub = pd.read_csv('../input/sample_submission.csv')
sub['Path'] = path
sub.to_csv('submission.csv', index=False)
sub.head()
```

Not bad, even with the wasted moves although it does take a while to run. It would be interesting to see what improvements come with a fixed-route model, fewer clusters and/or longer run times. All for now!


# Controllers

Controllers are used to determine the type of high-level control over a given robot arm. While all arms are directly controlled via their joint torques, the inputted action space for a given environment can vary depending on the type of desired control. Our controller options include `OSC_POSE`, `OSC_POSITION`, `JOINT_POSITION`, `JOINT_VELOCITY`, and `JOINT_TORQUE`.

For `OSC_POSE`, `OSC_POSITION`, and `JOINT_POSITION`, we include three variants that determine the action space. The most common variant is to use a predefined and constant set of impedance parameters; in that case, the action space only includes the desired pose, position, or joint configuration. We also include the option to specify either the stiffness values (and the damping will be automatically set to values that lead to a critically damped system), or all impedance parameters, both stiffness and damping, as part of the action at each step. These two variants lead to extended action spaces that can control the stiffness and damping behavior of the controller in a variable manner, providing full control to the policy/solution over the contact and dynamic behavior of the robot.

When using any position-based control (`OSC`, `IK`, or `JOINT_POSITION` controllers), input actions are, by default,
interpreted as delta values from the current state.

When using any end-effector pose controller (`IK`, `OSC_POSE`), delta rotations from the current end-effector orientation
in the form of axis-angle coordinates `(ax, ay, az)`, where the direction represents the axis and the magnitude
represents the angle (in radians). Note that for `OSC`, the rotation axes are taken relative to the global world
coordinate frame, whereas for `IK`, the rotation axes are taken relative to the end-effector origin, NOT the global world coordinate frame!

## Controllers

During runtime, the execution of the controllers is as follows. Controllers receive a desired configuration (reference value) and output joint torques that try to minimize the error between the current configuration and the desired one. Policies and solutions provide these desired configurations, elements of some action space, at what we call simulated policy frequency ($f_{p}$), e.g., 20Hz or 30Hz. **robosuite** will execute several iterations composed of a controller execution and a simulation step at simulation frequency, $f_s$ ($f_s = N\cdot f_p$), using the same reference signal until a new action is provided by the policy/solution. In these iterations, while the desired configuration is kept constant, the current state of the robot is changing, and thus, the error.

In the following we summarize the options, variables, and the control laws (equations) that convert desired values from the policy/solution and current robot states into executable joint torques to minimize the difference.

### Joint Space Control - Torque

Controller Type: `JOINT_TORQUE`

Action Dimensions (not including gripper): `n` (number of joints)

Since our controllers transform the desired values from the policies/solutions into joint torques, if these values are already joint torques, there is a one-to-one mapping between the reference value from the policy/solution and the output value from the joint torque controller at each step: $\tau = \tau_d$
\begin{equation}
\tau = \tau_d
\end{equation}

### Joint Space Control - Velocity

Controller Type: `JOINT_VELOCITY`

Action Dimensions (not including gripper): `n` (number of joints)

To control joint velocities, we create a proportional (P) control law between the desired value provided by the policy/solution (interpreted as desired velocity of each joint) and the current joint velocity of the robot. This control law, parameterized by a proportional constant, $k_p$, generates joint torques to execute at each simulation step:

\begin{equation}
\tau = k_p (\dot{q}_d - \dot{q})
\end{equation}

### Joint Space Control - Position with Fixed Impedance

Controller Type: `JOINT_POSITION`

Impedance: fixed

Action Dimensions (not including gripper): `n` (number of joints)

In joint position control, we create a proportional-derivative (PD) control law between the desired value provided by the policy/solution (interpreted as desired configuration for each joint) and the current joint positions of the robot. The control law that generates the joint torques to execute is parameterized by proportional and derivative gains, $k_p$ and $k_v$, and defined as

\begin{equation}
\tau = \Lambda \left[k_p \Delta_q - k_d\dot{q}\right]
\end{equation}

where $\Delta_q  = q_d - q$ is the difference between current and desired joint configurations, and $\Lambda$ is the inertia matrix, that we use to scale the error to remove the dynamic effects of the mechanism. The stiffness and damping parameters, $k_p$ and $k_d$, are determined in construction and kept fixed.

### Joint Space Control - Position with Variable Stiffness

Controller Type: `JOINT_POSITION`

Impedance: variable_kp

Action Dimensions (not including gripper): `2n` (number of joints)

The control law is the same as for fixed impedance but, in this controller, $k_p$ can be determined by the policy/solution at each policy step.

### Joint Space Control - Position with Variable Impedance

Controller Type: `JOINT_POSITION`

Impedance: variable

Action Dimensions (not including gripper): `3n` (number of joints)

Again, the control law is the same in the two previous control types, but now both the stiffness and damping parameters, $k_p$ and $k_d$, are controllable by the policy/solution and can be changed at each step.

### Operational Space Control - Pose with Fixed Impedance

Controller Type: `OSC_POSE`

Impedance: fixed

Action Dimensions (not including gripper): `6`

In the `OSC_POSE` controller, the desired value is the 6D pose (position and orientation) of a controlled frame. We follow the formalism from [\[Khatib87\]](https://ieeexplore.ieee.org/document/1087068). Our control frame is always the `eef_site` defined in the [Gripper Model](../modeling/gripper_model.html), placed at the end of the last link for robots without gripper or between the fingers for robots with gripper. The operational space control framework (OSC) computes the necessary joint torques to minimize the error between the desired and the current pose of the `eef_site` with the minimal kinematic energy.

Given a desired pose $\mathbf{x}_{\mathit{des}}$ and the current end-effector pose, , we first compute the end-effector acceleration that would help minimize the error between both, assumed. PD (proportional-derivative) control schema to improve convergence and stability. For that, we first decompose into a desired position, $p_d \in \mathbb{R}^3$, and a desired orientation, $R_d \in \mathbb{SO}(3)$. The end-effector acceleration to minimize the error should increase with the difference between desired end-effector pose and current pose, $p$ and $R$ (proportional term), and decrease with the current end-effector velocity, $v$ and $\omega$ (derivative term).

We then compute the robot actuation (joint torques) to achieve the desired end-effector space accelerations leveraging the kinematic and dynamic models of the robot with the dynamically-consistent operational space formulation in [\[Khatib1995a\]](https://journals.sagepub.com/doi/10.1177/027836499501400103). First, we compute the wrenches at the end-effector that corresponds to the desired accelerations, ${f}\in\mathbb{R}^{6}$.
Then, we map the wrenches in end-effector space ${f}$ to joint torque commands with the end-effector Jacobian at the current joint configuration $J=J(q)$: $\tau = J^T{f}$.

Thus, the function that maps end-effector space position and orientation to low-level robot commands is ($\textrm{ee} = \textrm{\it end-effector space}$):
\begin{equation}
\begin{aligned}
\tau &= J_p[\Lambda_p[k_p^p (p_d - p) - k_v^p v]] + J_R[\Lambda_R\left[k_p^R(R_d \ominus R) - k_d^R \omega \right]]
\end{aligned}
\end{equation}
where $\Lambda_p$ and $\Lambda_R$ are the parts corresponding to position and orientation in $\Lambda \in \mathbb{R}^{6\times6}$, the inertial matrix in the end-effector frame that decouples the end-effector motions, $J_p$ and $J_R$ are the position and orientation parts of the end-effector Jacobian, and $\ominus$ corresponds to the subtraction in $\mathbb{SO}(3)$. The difference between current and desired position ($\Delta_p= p_d - p$) and between current and desired orientation ($\Delta_R = R_d \ominus R$) can be used as alternative policy action space, $\mathcal{A}$. $k_p^p$, $k_p^d$, $k_p^R$, and $k_d^R$ are vectors of proportional and derivative gains for position and orientation (parameters $\kappa$), respectively, set once at initialization and kept fixed.

### Operational Space Control - Pose with Variable Stiffness

Controller Type: `OSC_POSE`

Impedance: variable_kp

Action Dimensions (not including gripper): `12`

The control law is the same as `OSC_POSE` but, in this case, the stiffness of the controller, $k_p$, is part of the action space and can be controlled and changed at each time step by the policy/solution. The damping parameters, $k_d$, are set to maintain the critically damped behavior of the controller.

### Operational Space Control - Pose with Variable Impedance

Controller Type: `OSC_POSE`

Impedance: variable

Action Dimensions (not including gripper): `18`

The control law is the same as in the to previous controllers, but now both the stiffness and the damping, $k_p$ and $k_d$, are part of the action space and can be controlled and changed at each time step by the policy/solution.

## Configurations
The config directory (found within the [Controllers](../source/robosuite.controllers) module) provides a set of default configuration files that hold default examples of parameters relevant to individual controllers. Note that when creating your controller config templates of a certain type of controller, the listed parameters in the default example are required and should be specified accordingly.

Note: Each robot has its own default controller configuration which is called by default unless a different controller config is called.

Below, a brief overview and description of each subset of controller parameters are shown:

#### Controller Settings
* `type`: Type of controller to control. Can be `OSC_POSE`, `OSC_POSITION`, `IK_POSE`, `JOINT_POSITION`, `JOINT_VELOCITY`, or `JOINT_TORQUE`
* `interpolation`: If not `null`, specified type of interpolation to use between desired actions. Currently only `linear` is supported.
* `ramp_ratio`: If using `linear` interpolation, specifies the proportion of allotted timesteps (value from [0, 1]) over which to execute the interpolated commands.
* `{...}_limits`: Limits for that specific controller. E.g.: for a `JOINT_POSITION`, the relevant limits are its joint positions, `qpos_limits` . Can be either a 2-element list (same min/max limits across entire relevant space), or a list of lists (specific limits for each component)
* `ik_{pos, ori}_limit`: Only applicable for IK controller. Limits the magnitude of the desired relative change in position / orientation.
* `{input,output}_{min,max}`: Scaling ranges for mapping action space inputs into controller inputs. Settings these limits will automatically clip the action space input to be within the `input_{min,max}` before mapping the requested value into the specified `output_{min,max}` range. Can be either a scalar (same limits across entire action space), or a list (specific limits for each action component)
* `kp`: Where relevant, specifies the proportional gain for the controller. Can be either be a scalar (same value for all controller dimensions), or a list (specific values for each dimension)
* `damping_ratio`: Where relevant, specifies the damping ratio constant for the controller.
* `impedance_mode`: For impedance-based controllers (`OSC_*`, `JOINT_POSITION`), determines the impedance mode for the controller, i.e. the nature of the impedance parameters. It can be `fixed`, `variable`, or `variable_kp` (kd is adjusted to provide critically damped behavior).
* `kp_limits, damping_ratio_limits`: Only relevant if `impedance_mode` is set to `variable` or `variable_kp`. Sets the limits for the resulting action space for variable impedance gains.
* `control_delta`: Only relevant for `OSC_POSE` or `OSC_POSITION` controllers. `true` interprets input actions as delta values from the current robot end-effector position. Otherwise, assumed to be absolute (global) values
* `uncouple_pos_ori`: Only relevant for `OSC_POSE`. `true` decouples the desired position and orientation torques when executing the controller

## Loading a Controller
By default, if no controller configuration is specified during environment creation, then `JOINT_VELOCITY` controllers with robot-specific configurations will be used.

#### Using a Default Controller Configuration
Any controller can be used with its default configuration, and can be easily loaded into a given environment by calling its name as shown below (where `controller_name` is one of acceptable controller `type` strings):

```python
import robosuite as suite
from robosuite import load_controller_config

# Load the desired controller's default config as a dict
config = load_controller_config(default_controller=controller_name)

# Create environment
env = suite.make("Lift", robots="Panda", controller_configs=config, ... )
```

#### Using a Custom Controller Configuration
A custom controller configuration can also be used by simply creating a new config (`.json`) file with the relevant parameters as specified above. All robosuite environments have an optional `controller_configs` argument that can be used to pass in specific controller settings. Note that this is expected to be a `dict`, so the new configuration must be read in and parsed as a `dict` before passing it during the environment `robosuite.make(...)` call. A brief example script showing how to import a custom controller configuration is shown below.

```python
import robosuite as suite
from robosuite import load_controller_config

# Path to config file
controller_fpath = "/your/custom/config/filepath/here/filename.json"

# Import the file as a dict
config = load_controller_config(custom_fpath=controller_fpath)

# Create environment
env = suite.make("Lift", robots="Panda", controller_configs=config, ... )
```