# Assignment 1: Auto Correct

Welcome to the first assignment of Course 2. This assignment will give you a chance to brush up on your python and probability skills. In doing so, you will implement an auto-correct system that is very effective and useful.

## Outline
- [0. Overview](#0)
    - [0.1 Edit Distance](#0-1)
- [1. Data Preprocessing](#1)
    - [1.1 Exercise 1](#ex-1)
    - [1.2 Exercise 2](#ex-2)
    - [1.3 Exercise 3](#ex-3)
- [2. String Manipulation](#2)
    - [2.1 Exercise 4](#ex-4)
    - [2.2 Exercise 5](#ex-5)
    - [2.3 Exercise 6](#ex-6)
    - [2.4 Exercise 7](#ex-7)
- [3. Combining the edits](#3)
    - [3.1 Exercise 8](#ex-8)
    - [3.2 Exercise 9](#ex-9)
    - [3.3 Exercise 10](#ex-10)
- [4. Minimum Edit Distance](#4)
    - [4.1 Exercise 11](#ex-11)
- [5. Backtrace (Optional)](#5)

## 0. Overview

You use autocorrect every day on your cell phone and computer. In this assignment, you will explore what really goes on behind the scenes. Of course, the model you are about to implement is not identical to the one used in your phone, but it is still quite good.

By completing this assignment you will learn how to:

- Get a word count given a corpus
- Get a word probability in the corpus
- Manipulate strings
- Filter strings
- Implement Minimum edit distance to compare strings and to help find the optimal path for the edits.
- Understand how dynamic programming works

Similar systems are used everywhere.
- For example, if you type in the word **"I am lerningg"**, chances are very high that you meant to write **"learning"**, as shown in **Figure 1**.

 Figure 1

#### 0.1 Edit Distance

In this assignment, you will implement models that correct words that are 1 and 2 edit distances away.
- We say two words are n edit distance away from each other when we need n edits to change one word into another.

An edit could consist of one of the following options:

- Delete (remove a letter): ‘hat’ => ‘at, ha, ht’
- Switch (swap 2 adjacent letters): ‘eta’ => ‘eat, tea,...’
- Replace (change 1 letter to another): ‘jat’ => ‘hat, rat, cat, mat, ...’
- Insert (add a letter): ‘te’ => ‘the, ten, ate, ...’

You will be using the four methods above to implement an Auto-correct.
- To do so, you will need to compute probabilities that a certain word is correct given an input.

This auto-correct you are about to implement was first created by [Peter Norvig](https://en.wikipedia.org/wiki/Peter_Norvig) in 2007.
- His [original article](https://norvig.com/spell-correct.html) may be a useful reference for this assignment.

The goal of our spell check model is to compute the following probability:

$$P(c|w) = \frac{P(w|c)\times P(c)}{P(w)} \tag{Eqn-1}$$

The equation above is [Bayes Rule](https://en.wikipedia.org/wiki/Bayes%27_theorem).
- Equation 1 says that the probability of a word being correct $P(c|w) $is equal to the probability of having a certain word $w$, given that it is correct $P(w|c)$, multiplied by the probability of being correct in general $P(C)$ divided by the probability of that word $w$ appearing $P(w)$ in general.
- To compute equation 1, you will first import a data set and then create all the probabilities that you need using that data set.

# Part 1: Data Preprocessing

```python
import re
import pandas as pd
import numpy as np
from collections import Counter

%matplotlib inline
%config InlineBackend.figure_format='svg'
```

As in any other machine learning task, the first thing you have to do is process your data set.
- Many courses load in pre-processed data for you.
- However, in the real world, when you build these NLP systems, you load the datasets and process them.
- So let's get some real world practice in pre-processing the data!

Your first task is to read in a file called **'shakespeare.txt'** which is found in your file directory. To look at this file you can go to `File ==> Open `.

### Exercise 1
Implement the function `process_data` which

1) Reads in a corpus (text file)

2) Changes everything to lowercase

3) Returns a list of words.

#### Options and Hints
- If you would like more of a real-life practice, don't open the 'Hints' below (yet) and try searching the web to derive your answer.
- If you want a little help, click on the green "General Hints" section by clicking on it with your mouse.
- If you get stuck or are not getting the expected results, click on the green 'Detailed Hints' section to get hints for each step that you'll take to complete this function.

```python
def process_data(file_name):
    """
    Input:
        A file_name which is found in your current directory. You just have to read it in.
    Output:
        words: a list containing all the words in the corpus (text file you read) in lower case.
    """
    words=[]

    with open(file_name) as f:
        file_name_data=f.read()
    file_name_data=file_name_data.lower()
    words=re.findall('\w+',file_name_data)

    return words
```

Note, in the following cell, 'words' is converted to a python `set`. This eliminates any duplicate entries.

```python
word_l = process_data('shakespeare.txt')
vocab = set(word_l)  # this will be your new vocabulary
print(f"The first ten words in the text are: \n{word_l[0:10]}")
print(f"There are {len(vocab)} unique words in the vocabulary.")
```

    The first ten words in the text are:
    ['o', 'for', 'a', 'muse', 'of', 'fire', 'that', 'would', 'ascend', 'the']
    There are 6116 unique words in the vocabulary.

### Exercise 2

Implement a `get_count` function that returns a dictionary
- The dictionary's keys are words
- The value for each word is the number of times that word appears in the corpus.

For example, given the following sentence: **"I am happy because I am learning"**, your dictionary should return the following:

     Key
     Value

     I
     2

    am
    2

    happy
    1

    because
    1

    learning
    1

**Instructions**:
Implement a `get_count` which returns a dictionary where the key is a word and the value is the number of times the word appears in the list.

```python
def get_count(word_l):
    '''
    Input:
        word_l: a set of words representing the corpus.
    Output:
        word_count_dict: The wordcount dictionary where key is the word and value is its frequency.
    '''
    word_count_dict=dict()

    word_count_dict=Counter(word_l)

    return word_count_dict
```

```python
word_count_dict = get_count(word_l)
print(f"There are {len(word_count_dict)} key values pairs")
print(f"The count for the word 'thee' is {word_count_dict.get('thee',0)}")
```

    There are 6116 key values pairs
    The count for the word 'thee' is 240

### Exercise 3
Given the dictionary of word counts, compute the probability that each word will appear if randomly selected from the corpus of words.

$$P(w_i) = \frac{C(w_i)}{M} \tag{Eqn-2}$$
where

$C(w_i)$ is the total number of times $w_i$ appears in the corpus.

$M$ is the total number of words in the corpus.

For example, the probability of the word 'am' in the sentence **'I am happy because I am learning'** is:

$$P(am) = \frac{C(w_i)}{M} = \frac {2}{7} \tag{Eqn-3}.$$

**Instructions:** Implement `get_probs` function which gives you the probability
that a word occurs in a sample. This returns a dictionary where the keys are words, and the value for each word is its probability in the corpus of words.

```python
def get_probs(word_count_dict):
    '''
    Input:
        word_count_dict: The wordcount dictionary where key is the word and value is its frequency.
    Output:
        probs: A dictionary where keys are the words and the values are the probability that a word will occur.
    '''
    probs={}

    m=sum(word_count_dict.values())
    for key,value in word_count_dict.items():
        probs[key]=value/m

    return probs
```

```python
probs = get_probs(word_count_dict)
print(f"Length of probs is {len(probs)}")
print(f"P('thee') is {probs['thee']:.4f}")
```

    Length of probs is 6116
    P('thee') is 0.0045

# Part 2: String Manipulations

Now, that you have computed $P(w_i)$ for all the words in the corpus, you will write a few functions to manipulate strings so that you can edit the erroneous strings and return the right spellings of the words. In this section, you will implement four functions:

* `delete_letter`: given a word, it returns all the possible strings that have **one character removed**.
* `switch_letter`: given a word, it returns all the possible strings that have **two adjacent letters switched**.
* `replace_letter`: given a word, it returns all the possible strings that have **one character replaced by another different letter**.
* `insert_letter`: given a word, it returns all the possible strings that have an **additional character inserted**.

#### List comprehensions

String and list manipulation in python will often make use of a python feature called  [list comprehensions](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions). The routines below will be described as using list comprehensions, but if you would rather implement them in another way, you are free to do so as long as the result is the same. Further, the following section will provide detailed instructions on how to use list comprehensions and how to implement the desired functions. If you are a python expert, feel free to skip the python hints and move to implementing the routines directly.

Python List Comprehensions embed a looping structure inside of a list declaration, collapsing many lines of code into a single line. If you are not familiar with them, they seem slightly out of order relative to for loops.

 Figure 2

The diagram above shows that the components of a list comprehension are the same components you would find in a typical for loop that appends to a list, but in a different order. With that in mind, we'll continue the specifics of this assignment. We will be very descriptive for the first function, `deletes()`, and less so in later functions as you become familiar with list comprehensions.

### Exercise 4

**Instructions for delete_letter():** Implement a `delete_letter()` function that, given a word, returns a list of strings with one character deleted.

For example, given the word **nice**, it would return the set: {'ice', 'nce', 'nic', 'nie'}.

**Step 1:** Create a list of 'splits'. This is all the ways you can split a word into Left and Right: For example,
'nice is split into : `[('', 'nice'), ('n', 'ice'), ('ni', 'ce'), ('nic', 'e'), ('nice', '')]`
This is common to all four functions (delete, replace, switch, insert).

 Figure 3

**Step 2:** This is specific to `delete_letter`. Here, we are generating all words that result from deleting one character.
This can be done in a single line with a list comprehension. You can makes use of this type of syntax:
`[f(a,b) for a, b in splits if condition]`

For our 'nice' example you get:
['ice', 'nce', 'nie', 'nic']

 Figure 4

#### Levels of assistance

Try this exercise with these levels of assistance.
- We hope that this will make it both a meaningful experience but also not a frustrating experience.
- Start with level 1, then move onto level 2, and 3 as needed.

    - Level 1. Try to think this through and implement this yourself.
    - Level 2. Click on the "Level 2 Hints" section for some hints to get started.
    - Level 3. If you would prefer more guidance, please click on the "Level 3 Hints" cell for step by step instructions.

- If you are still stuck, look at the images in the "list comprehensions" section above.

```python
def delete_letter(word,verbose=False):
    '''
    Input:
        word: the string/word for which you will generate all possible words
                in the vocabulary which have 1 missing character
    Output:
        delete_l: a list of all possible strings obtained by deleting 1 character from word
    '''

    delete_l=[]
    split_l=[]

    for c in range(len(word)):
        split_l.append((word[:c],word[c:]))

    for a,b in split_l:
        delete_l.append(a+b[1:])

    if verbose:
        print(f"input word: {word}, \nsplit_l = {split_l}, \ndelete_l = {delete_l}")

    return delete_l
```

```python
delete_word_l = delete_letter(word="cans",
                        verbose=True)
```

    input word: cans,
    split_l = [('', 'cans'), ('c', 'ans'), ('ca', 'ns'), ('can', 's')],
    delete_l = ['ans', 'cns', 'cas', 'can']

#### Note 1
You might get a slightly different result with split_l.
- Notice how it has the extra tuple `('cans', '')`.
- This will be fine as long as you have checked the size of the right-side substring in tuple (L,R).
- Can you explain why this will give you the same result for the list of deletion strings (delete_l)?

```Python
input word cans,
split_l = [('', 'cans'), ('c', 'ans'), ('ca', 'ns'), ('can', 's'), ('cans', '')],
delete_l = ['ans', 'cns', 'cas', 'can']
```

#### Note 2
If you end up getting the same word as your input word, like this:

```Python
input word cans,
split_l = [('', 'cans'), ('c', 'ans'), ('ca', 'ns'), ('can', 's'), ('cans', '')],
delete_l = ['ans', 'cns', 'cas', 'can', 'cans']
```

- Check how you set the `range`.
- See if you check the length of the string on the right-side of the split.

```python
print(f"Number of outputs of delete_letter('at') is {len(delete_letter('at'))}")
```

    Number of outputs of delete_letter('at') is 2

### Exercise 5

**Instructions for switch_letter()**: Now implement a function that switches two letters in a word. It takes in a word and returns a list of all the possible switches of two letters **that are adjacent to each other**.
- For example, given the word 'eta', it returns {'eat', 'tea'}, but does not return 'ate'.

**Step 1:** is the same as in delete_letter()
**Step 2:** A list comprehension or for loop which forms strings by swapping adjacent letters. This is of the form:
`[f(L,R) for L, R in splits if condition]`  where 'condition' will test the length of R in a given iteration. See below.

 Figure 5

#### Levels of difficulty

Try this exercise with these levels of difficulty.
- Level 1. Try to think this through and implement this yourself.
- Level 2. Click on the "Level 2 Hints" section for some hints to get started.
- Level 3. If you would prefer more guidance, please click on the "Level 3 Hints" cell for step by step instructions.

```python
def switch_letter(word,verbose=True):
    '''
    Input:
        word: input string
    Output:
        switches: a list of all possible strings with one adjacent charater switched
    '''

    def swap(c,i,j):
        c=list(c)
        c[i],c[j]=c[j],c[i]

        return ''.join(c)

    switch_l = []
    split_l = []

    len_word=len(word)

    for c in range(len_word):
        split_l.append((word[:c],word[c:]))

    switch_l=[a+b[1]+b[0]+b[2:] for a,b in split_l if len(b) >=2 ]

    if verbose: print(f"Input word = {word} \nsplit_l = {split_l} \nswitch_l = {switch_l}")

    return switch_l
```

```python
switch_word_l = switch_letter(word="eta",
                         verbose=True)
```

    Input word = eta
    split_l = [('', 'eta'), ('e', 'ta'), ('et', 'a')]
    switch_l = ['tea', 'eat']

#### Note 1

You may get this:
```Python
Input word = eta
split_l = [('', 'eta'), ('e', 'ta'), ('et', 'a'), ('eta', '')]
switch_l = ['tea', 'eat']
```
- Notice how it has the extra tuple `('eta', '')`.
- This is also correct.
- Can you think of why this is the case?

#### Note 2

If you get an error
```Python
IndexError: string index out of range
```
- Please see if you have checked the length of the strings when switching characters.

```python
print(f"Number of outputs of switch_letter('at') is {len(switch_letter('at'))}")
```

    Input word = at
    split_l = [('', 'at'), ('a', 't')]
    switch_l = ['ta']
    Number of outputs of switch_letter('at') is 1

### Exercise 6
**Instructions for replace_letter()**: Now implement a function that takes in a word and returns a list of strings with one **replaced letter** from the original word.

**Step 1:** is the same as in `delete_letter()`

**Step 2:** A list comprehension or for loop which form strings by replacing letters.  This can be of the form:
`[f(a,b,c) for a, b in splits if condition for c in string]`   Note the use of the second for loop.
It is expected in this routine that one or more of the replacements will include the original word. For example, replacing the first letter of 'ear' with 'e' will return 'ear'.

**Step 3:** Remove the original input letter from the output.

```python
def replace_letter(word,verbose=False):
    '''
    Input:
        word: the input string/word
    Output:
        replaces: a list of all possible strings where we replaced one letter from the original word.
    '''
    letters = 'abcdefghijklmnopqrstuvwxyz'
    replace_l = []
    split_l = []

    for c in range(len(word)):
        split_l.append((word[:c],word[c:]))

    replace_l = [a + l + (b[1:] if len(b)> 1 else '') for a,b in split_l if b for l in letters]
    replace_set=set(replace_l)
    replace_set.remove(word)

    replace_l = sorted(list(replace_set))

    if verbose: print(f"Input word = {word} \nsplit_l = {split_l} \nreplace_l {replace_l}")

    return replace_l
```

```python
replace_l = replace_letter(word='can',
                              verbose=True)
```

    Input word = can
    split_l = [('', 'can'), ('c', 'an'), ('ca', 'n')]
    replace_l ['aan', 'ban', 'caa', 'cab', 'cac', 'cad', 'cae', 'caf', 'cag', 'cah', 'cai', 'caj', 'cak', 'cal', 'cam', 'cao', 'cap', 'caq', 'car', 'cas', 'cat', 'cau', 'cav', 'caw', 'cax', 'cay', 'caz', 'cbn', 'ccn', 'cdn', 'cen', 'cfn', 'cgn', 'chn', 'cin', 'cjn', 'ckn', 'cln', 'cmn', 'cnn', 'con', 'cpn', 'cqn', 'crn', 'csn', 'ctn', 'cun', 'cvn', 'cwn', 'cxn', 'cyn', 'czn', 'dan', 'ean', 'fan', 'gan', 'han', 'ian', 'jan', 'kan', 'lan', 'man', 'nan', 'oan', 'pan', 'qan', 'ran', 'san', 'tan', 'uan', 'van', 'wan', 'xan', 'yan', 'zan']

#### Note 1
If you get something like this:

```Python
Input word = can
split_l = [('', 'can'), ('c', 'an'), ('ca', 'n'), ('can', '')]
replace_l ['aan', 'ban', 'caa', 'cab', 'cac', 'cad', 'cae', 'caf', 'cag', 'cah', 'cai', 'caj', 'cak', 'cal', 'cam', 'cao', 'cap', 'caq', 'car', 'cas', 'cat', 'cau', 'cav', 'caw', 'cax', 'cay', 'caz', 'cbn', 'ccn', 'cdn', 'cen', 'cfn', 'cgn', 'chn', 'cin', 'cjn', 'ckn', 'cln', 'cmn', 'cnn', 'con', 'cpn', 'cqn', 'crn', 'csn', 'ctn', 'cun', 'cvn', 'cwn', 'cxn', 'cyn', 'czn', 'dan', 'ean', 'fan', 'gan', 'han', 'ian', 'jan', 'kan', 'lan', 'man', 'nan', 'oan', 'pan', 'qan', 'ran', 'san', 'tan', 'uan', 'van', 'wan', 'xan', 'yan', 'zan']
```
- Notice how split_l has an extra tuple `('can', '')`, but the output is still the same, so this is okay.

#### Note 2
If you get something like this:
```Python
Input word = can
split_l = [('', 'can'), ('c', 'an'), ('ca', 'n'), ('can', '')]
replace_l ['aan', 'ban', 'caa', 'cab', 'cac', 'cad', 'cae', 'caf', 'cag', 'cah', 'cai', 'caj', 'cak', 'cal', 'cam', 'cana', 'canb', 'canc', 'cand', 'cane', 'canf', 'cang', 'canh', 'cani', 'canj', 'cank', 'canl', 'canm', 'cann', 'cano', 'canp', 'canq', 'canr', 'cans', 'cant', 'canu', 'canv', 'canw', 'canx', 'cany', 'canz', 'cao', 'cap', 'caq', 'car', 'cas', 'cat', 'cau', 'cav', 'caw', 'cax', 'cay', 'caz', 'cbn', 'ccn', 'cdn', 'cen', 'cfn', 'cgn', 'chn', 'cin', 'cjn', 'ckn', 'cln', 'cmn', 'cnn', 'con', 'cpn', 'cqn', 'crn', 'csn', 'ctn', 'cun', 'cvn', 'cwn', 'cxn', 'cyn', 'czn', 'dan', 'ean', 'fan', 'gan', 'han', 'ian', 'jan', 'kan', 'lan', 'man', 'nan', 'oan', 'pan', 'qan', 'ran', 'san', 'tan', 'uan', 'van', 'wan', 'xan', 'yan', 'zan']
```
- Notice how there are strings that are 1 letter longer than the original word, such as `cana`.
- Please check for the case when there is an empty string `''`, and if so, do not use that empty string when setting replace_l.

### Exercise 7

**Instructions for insert_letter()**: Now implement a function that takes in a word and returns a list with a letter inserted at every offset.

**Step 1:** is the same as in `delete_letter()`

**Step 2:** This can be a list comprehension of the form:
`[f(a,b,c) for a, b in splits if condition for c in string]`

```python
def insert_letter(word,verbose=False):
    '''
    Input:
        word: the input string/word
    Output:
        inserts: a set of all possible strings with one new letter inserted at every offset
    '''

    letters = 'abcdefghijklmnopqrstuvwxyz'
    insert_l = []
    split_l = []

    for c in range(len(word)+1):
        split_l.append((word[:c],word[c:]))

    insert_l=[a+l+b for a,b in split_l for l in letters]

    if verbose: print(f"Input word {word} \nsplit_l = {split_l} \ninsert_l = {insert_l}")

    return insert_l
```

```python
insert_l = insert_letter('at', True)
print(f"Number of strings output by insert_letter('at') is {len(insert_l)}")
```

    Input word at
    split_l = [('', 'at'), ('a', 't'), ('at', '')]
    insert_l = ['aat', 'bat', 'cat', 'dat', 'eat', 'fat', 'gat', 'hat', 'iat', 'jat', 'kat', 'lat', 'mat', 'nat', 'oat', 'pat', 'qat', 'rat', 'sat', 'tat', 'uat', 'vat', 'wat', 'xat', 'yat', 'zat', 'aat', 'abt', 'act', 'adt', 'aet', 'aft', 'agt', 'aht', 'ait', 'ajt', 'akt', 'alt', 'amt', 'ant', 'aot', 'apt', 'aqt', 'art', 'ast', 'att', 'aut', 'avt', 'awt', 'axt', 'ayt', 'azt', 'ata', 'atb', 'atc', 'atd', 'ate', 'atf', 'atg', 'ath', 'ati', 'atj', 'atk', 'atl', 'atm', 'atn', 'ato', 'atp', 'atq', 'atr', 'ats', 'att', 'atu', 'atv', 'atw', 'atx', 'aty', 'atz']
    Number of strings output by insert_letter('at') is 78

#### Note 1

If you get a split_l like this:
```Python
Input word at
split_l = [('', 'at'), ('a', 't')]
insert_l = ['aat', 'bat', 'cat', 'dat', 'eat', 'fat', 'gat', 'hat', 'iat', 'jat', 'kat', 'lat', 'mat', 'nat', 'oat', 'pat', 'qat', 'rat', 'sat', 'tat', 'uat', 'vat', 'wat', 'xat', 'yat', 'zat', 'aat', 'abt', 'act', 'adt', 'aet', 'aft', 'agt', 'aht', 'ait', 'ajt', 'akt', 'alt', 'amt', 'ant', 'aot', 'apt', 'aqt', 'art', 'ast', 'att', 'aut', 'avt', 'awt', 'axt', 'ayt', 'azt']
Number of strings output by insert_letter('at') is 52
```
- Notice that split_l is missing the extra tuple ('at', '').  For insertion, we actually **WANT** this tuple.
- The function is not creating all the desired output strings.
- Check the range that you use for the for loop.

#### Note 2
If you see this:
```Python
Input word at
split_l = [('', 'at'), ('a', 't'), ('at', '')]
insert_l = ['aat', 'bat', 'cat', 'dat', 'eat', 'fat', 'gat', 'hat', 'iat', 'jat', 'kat', 'lat', 'mat', 'nat', 'oat', 'pat', 'qat', 'rat', 'sat', 'tat', 'uat', 'vat', 'wat', 'xat', 'yat', 'zat', 'aat', 'abt', 'act', 'adt', 'aet', 'aft', 'agt', 'aht', 'ait', 'ajt', 'akt', 'alt', 'amt', 'ant', 'aot', 'apt', 'aqt', 'art', 'ast', 'att', 'aut', 'avt', 'awt', 'axt', 'ayt', 'azt']
Number of strings output by insert_letter('at') is 52
```

- Even though you may have fixed the split_l so that it contains the tuple `('at', '')`, notice that you're still missing some output strings.
    - Notice that it's missing strings such as 'ata', 'atb', 'atc' all the way to 'atz'.
- To fix this, make sure that when you set insert_l, you allow the use of the empty string `''`.

```python
print(f"Number of outputs of insert_letter('at') is {len(insert_letter('at'))}")
```

    Number of outputs of insert_letter('at') is 78

# Part 3: Combining the edits

Now that you have implemented the string manipulations, you will create two functions that, given a string, will return all the possible single and double edits on that string. These will be `edit_one_letter()` and `edit_two_letters()`.

## 3.1 Edit one letter

### Exercise 8

**Instructions**: Implement the `edit_one_letter` function to get all the possible edits that are one edit away from a word. The edits  consist of the replace, insert, delete, and optionally the switch operation. You should use the previous functions you have already implemented to complete this function. The 'switch' function  is a less common edit function, so its use will be selected by an "allow_switches" input argument.

Note that those functions return *lists* while this function should return a *python set*. Utilizing a set eliminates any duplicate entries.

```python
def edit_one_letter(word,allow_switches=True):
    """
    Input:
        word: the string/word for which we will generate all possible wordsthat are one edit away.
    Output:
        edit_one_set: a set of words with one possible edit. Please return a set. and not a list.
    """

    edit_one_set=set()

    edit_one_set.update(delete_letter(word))

    if allow_switches:
        edit_one_set.update(switch_letter(word))

    edit_one_set.update(replace_letter(word))
    edit_one_set.update(insert_letter(word))

    return edit_one_set
```

```python
tmp_word = "at"
tmp_edit_one_set = edit_one_letter(tmp_word)
# turn this into a list to sort it, in order to view it
tmp_edit_one_l = sorted(list(tmp_edit_one_set))

print(f"input word {tmp_word} \nedit_one_l \n{tmp_edit_one_l}\n")
print(f"The type of the returned object should be a set {type(tmp_edit_one_set)}")
print(f"Number of outputs from edit_one_letter('at') is {len(edit_one_letter('at'))}")
```

    Input word = at
    split_l = [('', 'at'), ('a', 't')]
    switch_l = ['ta']
    input word at
    edit_one_l
    ['a', 'aa', 'aat', 'ab', 'abt', 'ac', 'act', 'ad', 'adt', 'ae', 'aet', 'af', 'aft', 'ag', 'agt', 'ah', 'aht', 'ai', 'ait', 'aj', 'ajt', 'ak', 'akt', 'al', 'alt', 'am', 'amt', 'an', 'ant', 'ao', 'aot', 'ap', 'apt', 'aq', 'aqt', 'ar', 'art', 'as', 'ast', 'ata', 'atb', 'atc', 'atd', 'ate', 'atf', 'atg', 'ath', 'ati', 'atj', 'atk', 'atl', 'atm', 'atn', 'ato', 'atp', 'atq', 'atr', 'ats', 'att', 'atu', 'atv', 'atw', 'atx', 'aty', 'atz', 'au', 'aut', 'av', 'avt', 'aw', 'awt', 'ax', 'axt', 'ay', 'ayt', 'az', 'azt', 'bat', 'bt', 'cat', 'ct', 'dat', 'dt', 'eat', 'et', 'fat', 'ft', 'gat', 'gt', 'hat', 'ht', 'iat', 'it', 'jat', 'jt', 'kat', 'kt', 'lat', 'lt', 'mat', 'mt', 'nat', 'nt', 'oat', 'ot', 'pat', 'pt', 'qat', 'qt', 'rat', 'rt', 'sat', 'st', 't', 'ta', 'tat', 'tt', 'uat', 'ut', 'vat', 'vt', 'wat', 'wt', 'xat', 'xt', 'yat', 'yt', 'zat', 'zt']

    The type of the returned object should be a set <class 'set'>
    Input word = at
    split_l = [('', 'at'), ('a', 't')]
    switch_l = ['ta']
    Number of outputs from edit_one_letter('at') is 129

## Part 3.2 Edit two letters

### Exercise 9

Now you can generalize this to implement to get two edits on a word. To do so, you would have to get all the possible edits on a single word and then for each modified word, you would have to modify it again.

**Instructions**: Implement the `edit_two_letters` function that returns a set of words that are two edits away. Note that creating additional edits based on the `edit_one_letter` function may 'restore' some one_edits to zero or one edits. That is allowed here. This accounted for in get_corrections.

```python
def edit_two_letters(word,allow_switches=True):
    '''
    Input:
        word: the input string/word
    Output:
        edit_two_set: a set of strings with all possible two edits
    '''

    edit_two_set = set()
    edit_one = edit_one_letter(word,allow_switches=allow_switches)

    for w in edit_one:
        if w:
            edit_two = edit_one_letter(w,allow_switches=allow_switches)
            edit_two_set.update(edit_two)

    return edit_two_set
```

```python
tmp_edit_two_set = edit_two_letters("a")
tmp_edit_two_l = sorted(list(tmp_edit_two_set))
print(f"Number of strings with edit distance of two: {len(tmp_edit_two_l)}")
print(f"First 10 strings {tmp_edit_two_l[:10]}")
print(f"Last 10 strings {tmp_edit_two_l[-10:]}")
print(f"The data type of the returned object should be a set {type(tmp_edit_two_set)}")
print(f"Number of strings that are 2 edit distances from 'at' is {len(edit_two_letters('at'))}")
```

    Input word = a
    split_l = [('', 'a')]
    switch_l = []
    Input word = r
    split_l = [('', 'r')]
    switch_l = []
    Input word = da
    split_l = [('', 'da'), ('d', 'a')]
    switch_l = ['ad']
    Input word = ea
    split_l = [('', 'ea'), ('e', 'a')]
    switch_l = ['ae']
    Input word = l
    split_l = [('', 'l')]
    switch_l = []
    Input word = aj
    split_l = [('', 'aj'), ('a', 'j')]
    switch_l = ['ja']
    Input word = ap
    split_l = [('', 'ap'), ('a', 'p')]
    switch_l = ['pa']
    Input word = b
    split_l = [('', 'b')]
    switch_l = []
    Input word = pa
    split_l = [('', 'pa'), ('p', 'a')]
    switch_l = ['ap']
    Input word = y
    split_l = [('', 'y')]
    switch_l = []
    Input word = ka
    split_l = [('', 'ka'), ('k', 'a')]
    switch_l = ['ak']
    Input word = la
    split_l = [('', 'la'), ('l', 'a')]
    switch_l = ['al']
    Input word = ba
    split_l = [('', 'ba'), ('b', 'a')]
    switch_l = ['ab']
    Input word = g
    split_l = [('', 'g')]
    switch_l = []
    Input word = ya
    split_l = [('', 'ya'), ('y', 'a')]
    switch_l = ['ay']
    Input word = z
    split_l = [('', 'z')]
    switch_l = []
    Input word = ay
    split_l = [('', 'ay'), ('a', 'y')]
    switch_l = ['ya']
    Input word = am
    split_l = [('', 'am'), ('a', 'm')]
    switch_l = ['ma']
    Input word = qa
    split_l = [('', 'qa'), ('q', 'a')]
    switch_l = ['aq']
    Input word = za
    split_l = [('', 'za'), ('z', 'a')]
    switch_l = ['az']
    Input word = p
    split_l = [('', 'p')]
    switch_l = []
    Input word = e
    split_l = [('', 'e')]
    switch_l = []
    Input word = x
    split_l = [('', 'x')]
    switch_l = []
    Input word = f
    split_l = [('', 'f')]
    switch_l = []
    Input word = aq
    split_l = [('', 'aq'), ('a', 'q')]
    switch_l = ['qa']
    Input word = as
    split_l = [('', 'as'), ('a', 's')]
    switch_l = ['sa']
    Input word = al
    split_l = [('', 'al'), ('a', 'l')]
    switch_l = ['la']
    Input word = oa
    split_l = [('', 'oa'), ('o', 'a')]
    switch_l = ['ao']
    Input word = ac
    split_l = [('', 'ac'), ('a', 'c')]
    switch_l = ['ca']
    Input word = va
    split_l = [('', 'va'), ('v', 'a')]
    switch_l = ['av']
    Input word = ad
    split_l = [('', 'ad'), ('a', 'd')]
    switch_l = ['da']
    Input word = k
    split_l = [('', 'k')]
    switch_l = []
    Input word = h
    split_l = [('', 'h')]
    switch_l = []
    Input word = ia
    split_l = [('', 'ia'), ('i', 'a')]
    switch_l = ['ai']
    Input word = af
    split_l = [('', 'af'), ('a', 'f')]
    switch_l = ['fa']
    Input word = ca
    split_l = [('', 'ca'), ('c', 'a')]
    switch_l = ['ac']
    Input word = c
    split_l = [('', 'c')]
    switch_l = []
    Input word = n
    split_l = [('', 'n')]
    switch_l = []
    Input word = ae
    split_l = [('', 'ae'), ('a', 'e')]
    switch_l = ['ea']
    Input word = v
    split_l = [('', 'v')]
    switch_l = []
    Input word = ax
    split_l = [('', 'ax'), ('a', 'x')]
    switch_l = ['xa']
    Input word = at
    split_l = [('', 'at'), ('a', 't')]
    switch_l = ['ta']
    Input word = wa
    split_l = [('', 'wa'), ('w', 'a')]
    switch_l = ['aw']
    Input word = ta
    split_l = [('', 'ta'), ('t', 'a')]
    switch_l = ['at']
    Input word = ab
    split_l = [('', 'ab'), ('a', 'b')]
    switch_l = ['ba']
    Input word = an
    split_l = [('', 'an'), ('a', 'n')]
    switch_l = ['na']
    Input word = av
    split_l = [('', 'av'), ('a', 'v')]
    switch_l = ['va']
    Input word = ak
    split_l = [('', 'ak'), ('a', 'k')]
    switch_l = ['ka']
    Input word = w
    split_l = [('', 'w')]
    switch_l = []
    Input word = ha
    split_l = [('', 'ha'), ('h', 'a')]
    switch_l = ['ah']
    Input word = ga
    split_l = [('', 'ga'), ('g', 'a')]
    switch_l = ['ag']
    Input word = aw
    split_l = [('', 'aw'), ('a', 'w')]
    switch_l = ['wa']
    Input word = ao
    split_l = [('', 'ao'), ('a', 'o')]
    switch_l = ['oa']
    Input word = j
    split_l = [('', 'j')]
    switch_l = []
    Input word = t
    split_l = [('', 't')]
    switch_l = []
    Input word = fa
    split_l = [('', 'fa'), ('f', 'a')]
    switch_l = ['af']
    Input word = ra
    split_l = [('', 'ra'), ('r', 'a')]
    switch_l = ['ar']
    Input word = xa
    split_l = [('', 'xa'), ('x', 'a')]
    switch_l = ['ax']
    Input word = m
    split_l = [('', 'm')]
    switch_l = []
    Input word = ja
    split_l = [('', 'ja'), ('j', 'a')]
    switch_l = ['aj']
    Input word = ah
    split_l = [('', 'ah'), ('a', 'h')]
    switch_l = ['ha']
    Input word = ua
    split_l = [('', 'ua'), ('u', 'a')]
    switch_l = ['au']
    Input word = q
    split_l = [('', 'q')]
    switch_l = []
    Input word = au
    split_l = [('', 'au'), ('a', 'u')]
    switch_l = ['ua']
    Input word = az
    split_l = [('', 'az'), ('a', 'z')]
    switch_l = ['za']
    Input word = i
    split_l = [('', 'i')]
    switch_l = []
    Input word = sa
    split_l = [('', 'sa'), ('s', 'a')]
    switch_l = ['as']
    Input word = aa
    split_l = [('', 'aa'), ('a', 'a')]
    switch_l = ['aa']
    Input word = u
    split_l = [('', 'u')]
    switch_l = []
    Input word = ar
    split_l = [('', 'ar'), ('a', 'r')]
    switch_l = ['ra']
    Input word = ai
    split_l = [('', 'ai'), ('a', 'i')]
    switch_l = ['ia']
    Input word = d
    split_l = [('', 'd')]
    switch_l = []
    Input word = na
    split_l = [('', 'na'), ('n', 'a')]
    switch_l = ['an']
    Input word = s
    split_l = [('', 's')]
    switch_l = []
    Input word = ag
    split_l = [('', 'ag'), ('a', 'g')]
    switch_l = ['ga']
    Input word = o
    split_l = [('', 'o')]
    switch_l = []
    Input word = ma
    split_l = [('', 'ma'), ('m', 'a')]
    switch_l = ['am']
    Number of strings with edit distance of two: 2654
    First 10 strings ['', 'a', 'aa', 'aaa', 'aab', 'aac', 'aad', 'aae', 'aaf', 'aag']
    Last 10 strings ['zv', 'zva', 'zw', 'zwa', 'zx', 'zxa', 'zy', 'zya', 'zz', 'zza']
    The data type of the returned object should be a set <class 'set'>
    Input word = at
    split_l = [('', 'at'), ('a', 't')]
    switch_l = ['ta']
    Input word = axt
    split_l = [('', 'axt'), ('a', 'xt'), ('ax', 't')]
    switch_l = ['xat', 'atx']
    Input word = gat
    split_l = [('', 'gat'), ('g', 'at'), ('ga', 't')]
    switch_l = ['agt', 'gta']
    Input word = bat
    split_l = [('', 'bat'), ('b', 'at'), ('ba', 't')]
    switch_l = ['abt', 'bta']
    Input word = dt
    split_l = [('', 'dt'), ('d', 't')]
    switch_l = ['td']
    Input word = aat
    split_l = [('', 'aat'), ('a', 'at'), ('aa', 't')]
    switch_l = ['aat', 'ata']
    Input word = apt
    split_l = [('', 'apt'), ('a', 'pt'), ('ap', 't')]
    switch_l = ['pat', 'atp']
    Input word = atv
    split_l = [('', 'atv'), ('a', 'tv'), ('at', 'v')]
    switch_l = ['tav', 'avt']
    Input word = aj
    split_l = [('', 'aj'), ('a', 'j')]
    switch_l = ['ja']
    Input word = ap
    split_l = [('', 'ap'), ('a', 'p')]
    switch_l = ['pa']
    Input word = ft
    split_l = [('', 'ft'), ('f', 't')]
    switch_l = ['tf']
    Input word = azt
    split_l = [('', 'azt'), ('a', 'zt'), ('az', 't')]
    switch_l = ['zat', 'atz']
    Input word = mat
    split_l = [('', 'mat'), ('m', 'at'), ('ma', 't')]
    switch_l = ['amt', 'mta']
    Input word = fat
    split_l = [('', 'fat'), ('f', 'at'), ('fa', 't')]
    switch_l = ['aft', 'fta']
    Input word = kat
    split_l = [('', 'kat'), ('k', 'at'), ('ka', 't')]
    switch_l = ['akt', 'kta']
    Input word = ats
    split_l = [('', 'ats'), ('a', 'ts'), ('at', 's')]
    switch_l = ['tas', 'ast']
    Input word = uat
    split_l = [('', 'uat'), ('u', 'at'), ('ua', 't')]
    switch_l = ['aut', 'uta']
    Input word = cat
    split_l = [('', 'cat'), ('c', 'at'), ('ca', 't')]
    switch_l = ['act', 'cta']
    Input word = vt
    split_l = [('', 'vt'), ('v', 't')]
    switch_l = ['tv']
    Input word = ait
    split_l = [('', 'ait'), ('a', 'it'), ('ai', 't')]
    switch_l = ['iat', 'ati']
    Input word = yat
    split_l = [('', 'yat'), ('y', 'at'), ('ya', 't')]
    switch_l = ['ayt', 'yta']
    Input word = ay
    split_l = [('', 'ay'), ('a', 'y')]
    switch_l = ['ya']
    Input word = amt
    split_l = [('', 'amt'), ('a', 'mt'), ('am', 't')]
    switch_l = ['mat', 'atm']
    Input word = atk
    split_l = [('', 'atk'), ('a', 'tk'), ('at', 'k')]
    switch_l = ['tak', 'akt']
    Input word = avt
    split_l = [('', 'avt'), ('a', 'vt'), ('av', 't')]
    switch_l = ['vat', 'atv']
    Input word = am
    split_l = [('', 'am'), ('a', 'm')]
    switch_l = ['ma']
    Input word = oat
    split_l = [('', 'oat'), ('o', 'at'), ('oa', 't')]
    switch_l = ['aot', 'ota']
    Input word = sat
    split_l = [('', 'sat'), ('s', 'at'), ('sa', 't')]
    switch_l = ['ast', 'sta']
    Input word = wt
    split_l = [('', 'wt'), ('w', 't')]
    switch_l = ['tw']
    Input word = wat
    split_l = [('', 'wat'), ('w', 'at'), ('wa', 't')]
    switch_l = ['awt', 'wta']
    Input word = ayt
    split_l = [('', 'ayt'), ('a', 'yt'), ('ay', 't')]
    switch_l = ['yat', 'aty']
    Input word = gt
    split_l = [('', 'gt'), ('g', 't')]
    switch_l = ['tg']
    Input word = atq
    split_l = [('', 'atq'), ('a', 'tq'), ('at', 'q')]
    switch_l = ['taq', 'aqt']
    Input word = atx
    split_l = [('', 'atx'), ('a', 'tx'), ('at', 'x')]
    switch_l = ['tax', 'axt']
    Input word = atf
    split_l = [('', 'atf'), ('a', 'tf'), ('at', 'f')]
    switch_l = ['taf', 'aft']
    Input word = atn
    split_l = [('', 'atn'), ('a', 'tn'), ('at', 'n')]
    switch_l = ['tan', 'ant']
    Input word = alt
    split_l = [('', 'alt'), ('a', 'lt'), ('al', 't')]
    switch_l = ['lat', 'atl']
    Input word = bt
    split_l = [('', 'bt'), ('b', 't')]
    switch_l = ['tb']
    Input word = ato
    split_l = [('', 'ato'), ('a', 'to'), ('at', 'o')]
    switch_l = ['tao', 'aot']
    Input word = atu
    split_l = [('', 'atu'), ('a', 'tu'), ('at', 'u')]
    switch_l = ['tau', 'aut']
    Input word = jat
    split_l = [('', 'jat'), ('j', 'at'), ('ja', 't')]
    switch_l = ['ajt', 'jta']
    Input word = mt
    split_l = [('', 'mt'), ('m', 't')]
    switch_l = ['tm']
    Input word = aq
    split_l = [('', 'aq'), ('a', 'q')]
    switch_l = ['qa']
    Input word = et
    split_l = [('', 'et'), ('e', 't')]
    switch_l = ['te']
    Input word = atb
    split_l = [('', 'atb'), ('a', 'tb'), ('at', 'b')]
    switch_l = ['tab', 'abt']
    Input word = as
    split_l = [('', 'as'), ('a', 's')]
    switch_l = ['sa']
    Input word = ct
    split_l = [('', 'ct'), ('c', 't')]
    switch_l = ['tc']
    Input word = ath
    split_l = [('', 'ath'), ('a', 'th'), ('at', 'h')]
    switch_l = ['tah', 'aht']
    Input word = al
    split_l = [('', 'al'), ('a', 'l')]
    switch_l = ['la']
    Input word = it
    split_l = [('', 'it'), ('i', 't')]
    switch_l = ['ti']
    Input word = ant
    split_l = [('', 'ant'), ('a', 'nt'), ('an', 't')]
    switch_l = ['nat', 'atn']
    Input word = atj
    split_l = [('', 'atj'), ('a', 'tj'), ('at', 'j')]
    switch_l = ['taj', 'ajt']
    Input word = nat
    split_l = [('', 'nat'), ('n', 'at'), ('na', 't')]
    switch_l = ['ant', 'nta']
    Input word = aht
    split_l = [('', 'aht'), ('a', 'ht'), ('ah', 't')]
    switch_l = ['hat', 'ath']
    Input word = ac
    split_l = [('', 'ac'), ('a', 'c')]
    switch_l = ['ca']
    Input word = ut
    split_l = [('', 'ut'), ('u', 't')]
    switch_l = ['tu']
    Input word = ad
    split_l = [('', 'ad'), ('a', 'd')]
    switch_l = ['da']
    Input word = pt
    split_l = [('', 'pt'), ('p', 't')]
    switch_l = ['tp']
    Input word = aot
    split_l = [('', 'aot'), ('a', 'ot'), ('ao', 't')]
    switch_l = ['oat', 'ato']
    Input word = lt
    split_l = [('', 'lt'), ('l', 't')]
    switch_l = ['tl']
    Input word = pat
    split_l = [('', 'pat'), ('p', 'at'), ('pa', 't')]
    switch_l = ['apt', 'pta']
    Input word = hat
    split_l = [('', 'hat'), ('h', 'at'), ('ha', 't')]
    switch_l = ['aht', 'hta']
    Input word = atw
    split_l = [('', 'atw'), ('a', 'tw'), ('at', 'w')]
    switch_l = ['taw', 'awt']
    Input word = af
    split_l = [('', 'af'), ('a', 'f')]
    switch_l = ['fa']
    Input word = akt
    split_l = [('', 'akt'), ('a', 'kt'), ('ak', 't')]
    switch_l = ['kat', 'atk']
    Input word = ae
    split_l = [('', 'ae'), ('a', 'e')]
    switch_l = ['ea']
    Input word = ax
    split_l = [('', 'ax'), ('a', 'x')]
    switch_l = ['xa']
    Input word = eat
    split_l = [('', 'eat'), ('e', 'at'), ('ea', 't')]
    switch_l = ['aet', 'eta']
    Input word = aft
    split_l = [('', 'aft'), ('a', 'ft'), ('af', 't')]
    switch_l = ['fat', 'atf']
    Input word = lat
    split_l = [('', 'lat'), ('l', 'at'), ('la', 't')]
    switch_l = ['alt', 'lta']
    Input word = xt
    split_l = [('', 'xt'), ('x', 't')]
    switch_l = ['tx']
    Input word = atr
    split_l = [('', 'atr'), ('a', 'tr'), ('at', 'r')]
    switch_l = ['tar', 'art']
    Input word = aqt
    split_l = [('', 'aqt'), ('a', 'qt'), ('aq', 't')]
    switch_l = ['qat', 'atq']
    Input word = act
    split_l = [('', 'act'), ('a', 'ct'), ('ac', 't')]
    switch_l = ['cat', 'atc']
    Input word = ta
    split_l = [('', 'ta'), ('t', 'a')]
    switch_l = ['at']
    Input word = aty
    split_l = [('', 'aty'), ('a', 'ty'), ('at', 'y')]
    switch_l = ['tay', 'ayt']
    Input word = an
    split_l = [('', 'an'), ('a', 'n')]
    switch_l = ['na']
    Input word = ab
    split_l = [('', 'ab'), ('a', 'b')]
    switch_l = ['ba']
    Input word = av
    split_l = [('', 'av'), ('a', 'v')]
    switch_l = ['va']
    Input word = ak
    split_l = [('', 'ak'), ('a', 'k')]
    switch_l = ['ka']
    Input word = rat
    split_l = [('', 'rat'), ('r', 'at'), ('ra', 't')]
    switch_l = ['art', 'rta']
    Input word = abt
    split_l = [('', 'abt'), ('a', 'bt'), ('ab', 't')]
    switch_l = ['bat', 'atb']
    Input word = aet
    split_l = [('', 'aet'), ('a', 'et'), ('ae', 't')]
    switch_l = ['eat', 'ate']
    Input word = zt
    split_l = [('', 'zt'), ('z', 't')]
    switch_l = ['tz']
    Input word = aw
    split_l = [('', 'aw'), ('a', 'w')]
    switch_l = ['wa']
    Input word = ao
    split_l = [('', 'ao'), ('a', 'o')]
    switch_l = ['oa']
    Input word = jt
    split_l = [('', 'jt'), ('j', 't')]
    switch_l = ['tj']
    Input word = agt
    split_l = [('', 'agt'), ('a', 'gt'), ('ag', 't')]
    switch_l = ['gat', 'atg']
    Input word = tat
    split_l = [('', 'tat'), ('t', 'at'), ('ta', 't')]
    switch_l = ['att', 'tta']
    Input word = ajt
    split_l = [('', 'ajt'), ('a', 'jt'), ('aj', 't')]
    switch_l = ['jat', 'atj']
    Input word = atp
    split_l = [('', 'atp'), ('a', 'tp'), ('at', 'p')]
    switch_l = ['tap', 'apt']
    Input word = t
    split_l = [('', 't')]
    switch_l = []
    Input word = adt
    split_l = [('', 'adt'), ('a', 'dt'), ('ad', 't')]
    switch_l = ['dat', 'atd']
    Input word = rt
    split_l = [('', 'rt'), ('r', 't')]
    switch_l = ['tr']
    Input word = atz
    split_l = [('', 'atz'), ('a', 'tz'), ('at', 'z')]
    switch_l = ['taz', 'azt']
    Input word = vat
    split_l = [('', 'vat'), ('v', 'at'), ('va', 't')]
    switch_l = ['avt', 'vta']
    Input word = ah
    split_l = [('', 'ah'), ('a', 'h')]
    switch_l = ['ha']
    Input word = atg
    split_l = [('', 'atg'), ('a', 'tg'), ('at', 'g')]
    switch_l = ['tag', 'agt']
    Input word = au
    split_l = [('', 'au'), ('a', 'u')]
    switch_l = ['ua']
    Input word = ati
    split_l = [('', 'ati'), ('a', 'ti'), ('at', 'i')]
    switch_l = ['tai', 'ait']
    Input word = yt
    split_l = [('', 'yt'), ('y', 't')]
    switch_l = ['ty']
    Input word = az
    split_l = [('', 'az'), ('a', 'z')]
    switch_l = ['za']
    Input word = kt
    split_l = [('', 'kt'), ('k', 't')]
    switch_l = ['tk']
    Input word = st
    split_l = [('', 'st'), ('s', 't')]
    switch_l = ['ts']
    Input word = iat
    split_l = [('', 'iat'), ('i', 'at'), ('ia', 't')]
    switch_l = ['ait', 'ita']
    Input word = ate
    split_l = [('', 'ate'), ('a', 'te'), ('at', 'e')]
    switch_l = ['tae', 'aet']
    Input word = art
    split_l = [('', 'art'), ('a', 'rt'), ('ar', 't')]
    switch_l = ['rat', 'atr']
    Input word = atm
    split_l = [('', 'atm'), ('a', 'tm'), ('at', 'm')]
    switch_l = ['tam', 'amt']
    Input word = zat
    split_l = [('', 'zat'), ('z', 'at'), ('za', 't')]
    switch_l = ['azt', 'zta']
    Input word = atc
    split_l = [('', 'atc'), ('a', 'tc'), ('at', 'c')]
    switch_l = ['tac', 'act']
    Input word = aa
    split_l = [('', 'aa'), ('a', 'a')]
    switch_l = ['aa']
    Input word = ht
    split_l = [('', 'ht'), ('h', 't')]
    switch_l = ['th']
    Input word = ot
    split_l = [('', 'ot'), ('o', 't')]
    switch_l = ['to']
    Input word = ar
    split_l = [('', 'ar'), ('a', 'r')]
    switch_l = ['ra']
    Input word = dat
    split_l = [('', 'dat'), ('d', 'at'), ('da', 't')]
    switch_l = ['adt', 'dta']
    Input word = tt
    split_l = [('', 'tt'), ('t', 't')]
    switch_l = ['tt']
    Input word = ai
    split_l = [('', 'ai'), ('a', 'i')]
    switch_l = ['ia']
    Input word = att
    split_l = [('', 'att'), ('a', 'tt'), ('at', 't')]
    switch_l = ['tat', 'att']
    Input word = ag
    split_l = [('', 'ag'), ('a', 'g')]
    switch_l = ['ga']
    Input word = ata
    split_l = [('', 'ata'), ('a', 'ta'), ('at', 'a')]
    switch_l = ['taa', 'aat']
    Input word = aut
    split_l = [('', 'aut'), ('a', 'ut'), ('au', 't')]
    switch_l = ['uat', 'atu']
    Input word = atd
    split_l = [('', 'atd'), ('a', 'td'), ('at', 'd')]
    switch_l = ['tad', 'adt']
    Input word = a
    split_l = [('', 'a')]
    switch_l = []
    Input word = nt
    split_l = [('', 'nt'), ('n', 't')]
    switch_l = ['tn']
    Input word = ast
    split_l = [('', 'ast'), ('a', 'st'), ('as', 't')]
    switch_l = ['sat', 'ats']
    Input word = xat
    split_l = [('', 'xat'), ('x', 'at'), ('xa', 't')]
    switch_l = ['axt', 'xta']
    Input word = awt
    split_l = [('', 'awt'), ('a', 'wt'), ('aw', 't')]
    switch_l = ['wat', 'atw']
    Input word = qt
    split_l = [('', 'qt'), ('q', 't')]
    switch_l = ['tq']
    Input word = atl
    split_l = [('', 'atl'), ('a', 'tl'), ('at', 'l')]
    switch_l = ['tal', 'alt']
    Input word = qat
    split_l = [('', 'qat'), ('q', 'at'), ('qa', 't')]
    switch_l = ['aqt', 'qta']
    Number of strings that are 2 edit distances from 'at' is 7154

## Part 3-3: suggest spelling suggestions

Now you will use your `edit_two_letters` function to get a set of all the possible 2 edits on your word. You will then use those strings to get the most probable word you meant to type aka your typing suggestion.

### Exercise 10
**Instructions**: Implement `get_corrections`, which returns a list of zero to n possible suggestion tuples of the form (word, probability_of_word).

**Step 1:** Generate suggestions for a supplied word: You'll use the edit functions you have developed. The 'suggestion algorithm' should follow this logic:
* If the word is in the vocabulary, suggest the word.
* Otherwise, if there are suggestions from `edit_one_letter` that are in the vocabulary, use those.
* Otherwise, if there are suggestions from `edit_two_letters` that are in the vocabulary, use those.
* Otherwise, suggest the input word.*
* The idea is that words generated from fewer edits are more likely than words with more edits.

Note:
- Edits of one or two letters may 'restore' strings to either zero or one edit. This algorithm accounts for this by preferentially selecting lower distance edits first.

#### Short circuit
In Python, logical operations such as `and` and `or` have two useful properties. They can operate on lists and they have ['short-circuit' behavior](https://docs.python.org/3/library/stdtypes.html). Try these:

```python
# example of logical operation on lists or sets
print( [] and ["a","b"] )
print( [] or ["a","b"] )
#example of Short circuit behavior
val1 =  ["Most","Likely"] or ["Less","so"] or ["least","of","all"]  # selects first, does not evalute remainder
print(val1)
val2 =  [] or [] or ["least","of","all"] # continues evaluation until there is a non-empty list
print(val2)
```

    []
    ['a', 'b']
    ['Most', 'Likely']
    ['least', 'of', 'all']

The logical `or` could be used to implement the suggestion algorithm very compactly. Alternately, if/then constructs could be used.

**Step 2**: Create a 'best_words' dictionary where the 'key' is a suggestion and the 'value' is the probability of that word in your vocabulary. If the word is not in the vocabulary, assign it a probability of 0.

**Step 3**: Select the n best suggestions. There may be fewer than n.

```python
def get_corrections(word,probs,vocab,n=2,verbose=False):
    '''
    Input:
        word: a user entered string to check for suggestions
        probs: a dictionary that maps each word to its probability in the corpus
        vocab: a set containing all the vocabulary
        n: number of possible word corrections you want returned in the dictionary
    Output:
        n_best: a list of tuples with the most probable n corrected words and their probabilities.
    '''

    suggestions = []
    n_best = []

    suggestions=list((word in vocab and word) or edit_one_letter(word).intersection(vocab)\
                     or edit_two_letters(word).intersection(vocab))

    n_best=[[s,probs[s]] for s in list(reversed(suggestions))]

    if verbose: print("suggestions = ", suggestions)

    return n_best
```

```python
my_word = 'dys'
tmp_corrections = get_corrections(my_word, probs, vocab, 2, verbose=False)
for i, word_prob in enumerate(tmp_corrections):
    print(f"word {i}: {word_prob[0]}, probability {word_prob[1]:.6f}")
# CODE REVIEW COMMENT: using "tmp_corrections" insteads of "cors". "cors" is not defined
print(f"data type of corrections {type(tmp_corrections)}")
```

    Input word = dys
    split_l = [('', 'dys'), ('d', 'ys'), ('dy', 's')]
    switch_l = ['yds', 'dsy']
    word 0: dye, probability 0.000019
    word 1: days, probability 0.000410
    data type of corrections <class 'list'>

# Part 4: Minimum Edit distance

Now that you have implemented your auto-correct, how do you evaluate the similarity between two strings? For example: 'waht' and 'what'

Also how do you efficiently find the shortest path to go from the word, 'waht' to the word 'what'?

You will implement a dynamic programming system that will tell you the minimum number of edits required to convert a string into another string.

### Part 4.1 Dynamic Programming

Dynamic Programming breaks a problem down into subproblems which can be combined to form the final solution. Here, given a string source[0..i] and a string target[0..j], we will compute all the combinations of substrings[i, j] and calculate their edit distance. To do this efficiently, we will use a table to maintain the previously computed substrings and use those to calculate larger substrings.

You have to create a matrix and update each element in the matrix as follows:

$$\text{Initialization}$$

\begin{align}
D[0,0] &= 0 \\
D[i,0] &= D[i-1,0] + del\_cost(source[i]) \tag{4}\\
D[0,j] &= D[0,j-1] + ins\_cost(target[j]) \\
\end{align}

$$\text{Per Cell Operations}$$
\begin{align}
 \\
D[i,j] =min
\begin{cases}
D[i-1,j] + del\_cost\\
D[i,j-1] + ins\_cost\\
D[i-1,j-1] + \left\{\begin{matrix}
rep\_cost; & if src[i]\neq tar[j]\\
0 ; & if src[i]=tar[j]
\end{matrix}\right.
\end{cases}
\tag{5}
\end{align}

So converting the source word **play** to the target word **stay**, using an input cost of one, a delete cost of 1, and replace cost of 2 would give you the following table:

     #
     s
     t
     a
     y

       #
     0
     1
     2
     3
     4

       p
     1
  2
     3
     4
    5

      l
    2
    3
    4
    5
    6

      a
    3
     4
     5
     4
     5

      y
    4
      5
     6
     5
     4

The operations used in this algorithm are 'insert', 'delete', and 'replace'. These correspond to the functions that you defined earlier: insert_letter(), delete_letter() and replace_letter(). switch_letter() is not used here.

The diagram below describes how to initialize the table. Each entry in D[i,j] represents the minimum cost of converting string source[0:i] to string target[0:j]. The first column is initialized to represent the cumulative cost of deleting the source characters to convert string "EER" to "". The first row is initialized to represent the cumulative cost of inserting the target characters to convert from "" to "NEAR".

 Figure 6 Initializing Distance Matrix

Filling in the remainder of the table utilizes the 'Per Cell Operations' in the equation (5) above. Note, the diagram below includes in the table some of the 3 sub-calculations shown in light grey. Only 'min' of those operations is stored in the table in the `min_edit_distance()` function.

 Figure 7 Filling Distance Matrix

Note that the formula for $D[i,j]$ shown in the image is equivalent to:

\begin{align}
 \\
D[i,j] =min
\begin{cases}
D[i-1,j] + del\_cost\\
D[i,j-1] + ins\_cost\\
D[i-1,j-1] + \left\{\begin{matrix}
rep\_cost; & if src[i]\neq tar[j]\\
0 ; & if src[i]=tar[j]
\end{matrix}\right.
\end{cases}
\tag{5}
\end{align}

The variable `sub_cost` (for substitution cost) is the same as `rep_cost`; replacement cost.  We will stick with the term "replace" whenever possible.

Below are some examples of cells where replacement is used. This also shows the minimum path from the lower right final position where "EER" has been replaced by "NEAR" back to the start. This provides a starting point for the optional 'backtrace' algorithm below.

 Figure 8 Examples Distance Matrix

### Exercise 11

Again, the word "substitution" appears in the figure, but think of this as "replacement".

**Instructions**: Implement the function below to get the minimum amount of edits required given a source string and a target string.

```python
def min_edit_distance(source, target, ins_cost = 1, del_cost = 1, rep_cost = 2):
    '''
    Input:
        source: a string corresponding to the string you are starting with
        target: a string corresponding to the string you want to end with
        ins_cost: an integer setting the insert cost
        del_cost: an integer setting the delete cost
        rep_cost: an integer setting the replace cost
    Output:
        D: a matrix of len(source)+1 by len(target)+1 containing minimum edit distances
        med: the minimum edit distance (med) required to convert the source string to the target
    '''

    # use deletion and insertion cost as 1
    m=len(source)
    n=len(target)

    # initialize cost matrix with zeros and dimensions (m+1,n+1)
    D=np.zeros((m+1,n+1),dtype=int)

    for row in range(1,m+1):
        D[row,0]=D[row-1,0]+del_cost

    for col in range(1,n+1):
        D[0,col]=D[0,col-1]+ins_cost

    for row in range(1,m+1):
        for col in range(1,n+1):
            r_cost=rep_cost

            if source[row-1]==target[col-1]:
                r_cost=0

            D[row,col]=min([D[row-1,col]+del_cost, D[row,col-1]+ins_cost, D[row-1,col-1]+r_cost])

    med=D[m,n]

    return D,med
```

```python
# Testing
source =  'play'
target = 'stay'
matrix, min_edits = min_edit_distance(source, target)
print("minimum edits: ",min_edits, "\n")
idx = list('#' + source)
cols = list('#' + target)
df = pd.DataFrame(matrix, index=idx, columns= cols)
print(df)
```

    minimum edits:  4

       #  s  t  a  y
    #  0  1  2  3  4
    p  1  2  3  4  5
    l  2  3  4  5  6
    a  3  4  5  4  5
    y  4  5  6  5  4

```python
# Testing
source =  'eer'
target = 'near'
matrix, min_edits = min_edit_distance(source, target)
print("minimum edits: ",min_edits, "\n")
idx = list(source)
idx.insert(0, '#')
cols = list(target)
cols.insert(0, '#')
df = pd.DataFrame(matrix, index=idx, columns= cols)
print(df)
```

    minimum edits:  3

       #  n  e  a  r
    #  0  1  2  3  4
    e  1  2  1  2  3
    e  2  3  2  3  4
    r  3  4  3  4  3

We can now test several of our routines at once:

```python
source = "eer"
targets = edit_one_letter(source,allow_switches = False)  #disable switches since min_edit_distance does not include them
for t in targets:
    _, min_edits = min_edit_distance(source, t,1,1,1)  # set ins, del, sub costs all to one
    if min_edits != 1: print(source, t, min_edits)
```

**Expected Results:**  (empty)

The 'replace()' routine utilizes all letters a-z one of which returns the original word.

```python
source = "eer"
targets = edit_two_letters(source,allow_switches = False) #disable switches since min_edit_distance does not include them
for t in targets:
    _, min_edits = min_edit_distance(source, t,1,1,1)  # set ins, del, sub costs all to one
    if min_edits != 2 and min_edits != 1: print(source, t, min_edits)
```

    eer eer 0

# Submission
Make sure you submit your assignment before you modify anything below

# Part 5: Optional - Backtrace

Once you have computed your matrix using minimum edit distance, how would find the shortest path from the top left corner to the bottom right corner?

Note that you could use backtrace algorithm.  Try to find the shortest path given the matrix that your `min_edit_distance` function returned.

You can use these [lecture slides on minimum edit distance](https://web.stanford.edu/class/cs124/lec/med.pdf) by Dan Jurafsky to learn about the algorithm for backtrace.

#### References
- Dan Jurafsky - Speech and Language Processing - Textbook
- This auto-correct explanation was first done by Peter Norvig in 2007

```python

```

NLP/Learn_by_deeplearning.ai/Course 2 - Probabilistic Models/Labs/Week 1/C2-W1-assignment-Auto Correct.ipynb


# Wave Equation

\begin{equation}
\begin{aligned}
    \partial_t^2 u(t,x) &= \partial_x^2 u(t,x), && t \in (0,T), x \in (x_{min}, x_{max}), \\
    u(0,x) &= u_0(x), && x \in (x_{min}, x_{max}), \\
    \partial_t u(0,x) &= v_0(x), && x \in (x_{min}, x_{max}), \\
    \text{boundary conditions}, &&& x \in \partial (x_{min}, x_{max}).
\end{aligned}
\end{equation}

```julia
using Revise
using SummationByPartsOperators, OrdinaryDiffEq
using Plots, LaTeXStrings, Printf

# general parameters
xmin = -1.
xmax = +1.
tspan = (0., 8.0)
u0func(x) = exp(-20x^2)
du0func(x) = zero(x)
# HomogeneousNeumann, HomogeneousDirichlet, NonReflecting BCs are implemented up to now
left_bc  = Val(:HomogeneousNeumann)
right_bc = Val(:HomogeneousDirichlet)

# discretisation parameters
interior_order = 4
N = 101

# setup spatial semidiscretization
D2 = derivative_operator(MattssonSvärdShoeybi2008(), 2, interior_order, xmin, xmax, N)
semi = WaveEquationNonperiodicSemidiscretization(D2, left_bc, right_bc)
ode = semidiscretize(du0func, u0func, semi, tspan)

# solve ode
sol = solve(ode, DPRKN6(), dt=0.25*D2.Δx, adaptive=false,
            saveat=range(first(tspan), stop=last(tspan), length=200))

# visualise the result
plot(xguide=L"x")
plot!(evaluate_coefficients(sol[end].x[2], semi), label=L"u")
plot!(evaluate_coefficients(sol[end].x[1], semi), label=L"\partial_t u")
```

```julia
# make a movie
anim = Animation()
idx = 1
x, u = evaluate_coefficients(sol[idx].x[2], semi)

fig = plot(x, u, xguide=L"x", yguide=L"u", xlim=extrema(x), ylim=(-1., 1.),
           #size=(1024,768), dpi=250,
           label="", title=@sprintf("\$t = %6.2f \$", sol.t[idx]))
for idx in 1:length(sol.t)
    fig[1] = x, sol.u[idx].x[2]
    plot!(title=@sprintf("\$t = %6.2f \$", sol.t[idx]))
    frame(anim)
end
gif(anim)
```

## 2D

Since lazy tensor/Kronecker products are not implemented yet,
the operators are converted to sparse matrices via `LinearMaps.jl`.

```julia
using LinearMaps
using SparseArrays, LinearAlgebra

# general parameters
xmin = -1.
xmax = +1.
ymin = -1.
ymax = +1.
tspan = (0., 4.0)
u0func(x,y) = exp(-20*(x^2+y^2))
du0func(x,y) = zero(x)
# HomogeneousNeumann, HomogeneousDirichlet BCs are implemented up to now
# NonReflecting requires the values of du and is not usable in the simplified approach below
xmin_bc = Val(:HomogeneousDirichlet)
xmax_bc = Val(:HomogeneousDirichlet)
ymin_bc = Val(:HomogeneousNeumann)
ymax_bc = Val(:HomogeneousNeumann)

# discretisation parameters
interior_order = 4
Nx = 151
Ny = 151

# setup spatial semidiscretization
D2x = derivative_operator(MattssonSvärdShoeybi2008(), 2, interior_order, xmin, xmax, Nx)
semidiscx = WaveEquationNonperiodicSemidiscretization(D2x, xmin_bc, xmax_bc)
opx = LinearMap{Float64}((ddu,u)->semidiscx(ddu,zero(u),u,nothing,0.), Nx, ismutating=true) |> sparse

D2y = derivative_operator(MattssonSvärdShoeybi2008(), 2, interior_order, ymin, ymax, Ny)
semidiscy = WaveEquationNonperiodicSemidiscretization(D2y, ymin_bc, ymax_bc)
opy = LinearMap{Float64}((ddu,u)->semidiscy(ddu,zero(u),u,nothing,0.), Ny, ismutating=true) |> sparse

x = SummationByPartsOperators.grid(D2x)
y = SummationByPartsOperators.grid(D2y)
op = kron(opx, sparse(I, size(opy)...)) + kron(sparse(I, size(opx)...), opy)
u0 = u0func.(x, y') |> vec
du0 = du0func.(x, y') |> vec
ode = SecondOrderODEProblem((ddu,du,u,p,t)->mul!(ddu,op,u), du0, u0, tspan)

# solve ode
sol2D = solve(ode, DPRKN6(), dt=0.25*min(D2x.Δx,D2y.Δx), adaptive=false,
              saveat=range(first(tspan), stop=last(tspan), length=200))

# visualise the result
plot(xguide=L"x", yguide=L"y", clim=(-1.,1.), aspect_ratio=:equal, size=(600,520))
heatmap!(x, y, sol2D[end].x[2], c=:bluesreds)
```

```julia
# make a movie
anim = Animation()
idx = 1

fig = heatmap(x, y, sol2D.u[idx].x[2], xguide=L"x", yguide=L"y", clim=(-1., 1.), c=:bluesreds,
              aspect_ratio=:equal, size=(600,520),
               #size=(1024,768), dpi=250,
               label="", title=@sprintf("\$t = %6.2f \$", sol2D.t[idx]))
for idx in 1:length(sol.t)
    fig[1] = x, y, sol2D.u[idx].x[2]
    heatmap!(title=@sprintf("\$t = %6.2f \$", sol2D.t[idx]))
    frame(anim)
end
gif(anim)
```

```julia

```


# Microplastic as Prey

Aleksandra Samotyja, John S Butler

# Introduction
This webpage will describe a mathematical model that simulates impact of the introduction of microplastics on fish and salmon population using Python.
The webpage is divided into six parts:
* The first section outlines the normal predator prey interaction first observed in hare and lynx;
* The second section  will mathematically model the predator-prey relationship of salmon and small fish;
* The third section describes and models the growth of the human population and increase in the production of microplastics;
* The fourth section describes the impact of microplastics on the small fish, salmon and human population;
* The fifth section shows the results of the simulations of the ecosystem  for different levels of plastic production rates;
* The final section presents some questions that arise from this problem.

The webpage is written such that it can be read by some one with no programming experience but if you want to read the code you can click on the button below.

```python
import numpy as np
import math

%matplotlib inline
import matplotlib.pyplot as plt # side-stepping mpl backend
import matplotlib.gridspec as gridspec # subplots
import warnings
warnings.filterwarnings("ignore")
from __future__ import print_function
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
plastic_widget1 = widgets.FloatSlider(min=0.001, max=1, step=0.01,value=0.1)
plastic_widget = widgets.FloatSlider(min=0.001, max=1, step=0.01,value=0.1)
from IPython.display import HTML

HTML('''
<form action="javascript:code_toggle()"><input type="submit" value="Click here to toggle on/off the raw code."></form>''')
```

<form action="javascript:code_toggle()"><input type="submit" value="Click here to toggle on/off the raw code."></form>

# Predator Prey
## Predators and prey
The cyclical relationship of Predators and Prey in the wild  means that when there is an abundance of prey, the predators will eat more and the prey population will increase. On the other hand, if there is a shortage in the number of prey, the number of predators will decrease due to the scaricty of food source [1].

## Fish and Salmon
In a balanced eco system the relationship between small fish (prey) and salmon (predator) can be described mathematically using a Loketta-Volterra model [2-4]
\begin{equation}
\begin{array}{cl}
\frac{d F}{dt}=F-0.02SF,\\
\frac{d S}{dt}=-0.05S+0.001 F S,\\
\end{array}
\end{equation}
this set of equations models the interaction between small fish (F) and salmon (S).

The equations can be written in words as:

| The rate of change of population | is equal to  | the current population with a control factor|
|-----------------------------------|--------------|------------------------------------------------------------------|
| The increase in fish population  $\frac{d F}{dt}$ | is equal to  | the current fish population $F$ minus the amount of fish salmon eat $-0.02FS$ |
| The increase in salmon population $\frac{d S}{dt}$ | is equal to | the current salmon population $-0.05S$ plus the amount of fish salmon eat $+0.001FS$|

Or they can be summed up as the salmon are the predators who eat the small fish to survive.

Figure 2 shows a simulation of the populations of salmon and small fish from 2000 to 2100, assuming that nothing is added to enviroment.  The salmon population fluxuates between 45 and 55 billion while the small fish population fluctuates from 25 to 85 billion. The equation illustates a relative equilibrium between the two populations ensuring neither population gets too big or too small.

```python
# INITIAL CONDITIONS
N=10000
time = np.linspace(2000, 2100, N )              # time
h=100/N
fish=50*np.ones(N) # 60 billions small fish
salmon=45*np.ones(N) # 30 billion salmon
SMALLFISH = np.array( [1,-0.02])
SALMON= np.array([-0.05,0.001])
for i in range (1,N):
    fish[i]=fish[i-1]+h*fish[i-1]*(SMALLFISH[0]+SMALLFISH[1]*salmon[i-1])
    salmon[i]=salmon[i-1]+h*salmon[i-1]*(SALMON[0]+SALMON[1]*fish[i-1])
## PLOTTING
fig = plt.figure(figsize=(8,4))
plt.plot(time, fish, 'r-', label='small fish')
plt.plot(time, salmon, 'b-', label='salmon')
plt.grid()
plt.xlabel('time (years)')
plt.ylabel('Population (billions)')
plt.title('Figure 2: Predator Predy relationship of Salmon and small fish')
plt.legend(loc='best')
plt.tight_layout()
plt.show()
```

# Humans
## Human population growth
Humans  are adaptable omnivores who have fire and tools so we are able to eat most animals and plants [5]. This means humans violate this the cyclical relationship. When humans have eaten all of one prey, such as the dodo, we just move on to another prey with seemingly little impact on human population growth. From 1800 to 1900 the human population increased by 60% from 1 billion to 1.6 billion. From 1900 to 2000 the human population has increased by almost 400% from 1.6 to 6 billion. Figure 3 shows the predicted increase in human population (yellow) in billions from 6 billion in 2000 to around 16 billion in 2100. The population should level off as number of foods sources can only sustain so many huamns. This growth can be descibed as a the equation:
\begin{equation}
\frac{d H}{dt}=0.05H-0.003H^2,
\end{equation}
where $H$ is human population.
The equations can be written in words as:

| Rate of change of Population                       | is equal to  | the current population and a control factor                                                     |
|----------------------------------------------------|-----------------|------------------------------------------------------------------------------------------------|
| The increase in human population  $\frac{d H}{dt}$ | is equal to  | the current human population $0.05H$ minus a factor to control for over population $-0.003H^2$ |
It can be summed up as the human population keeps increasing until there is nothing left to eat.

```python
humans=6*np.ones(N) # 6 billion humans

for i in range (1,N):
    humans[i]=humans[i-1]+h*humans[i-1]*(0.05-0.003*humans[i-1])
fig= plt.figure(figsize=(8,4))
plt.plot(time, humans, 'y-', label = 'Humans')
plt.xlabel('time (yrs)')
plt.ylabel('Population (billions)')
plt.legend(loc='best')
plt.tight_layout()
plt.ylim(0,20)
plt.title('Figure 3: Human Population')
plt.show()
```

## Impact of Humans
Due to the increasing human population the demand on food sources is getting highter and higher. This demand could put popular food sources at risk of being eaten into extinction. In the recent decades, governments have tried to address this by introduction quotas and identifying at risk animals. While this can address the direct impact of humans on the ecosystem it does not address the indirect by products of modern life such as microplastics.

## MicroPlastics and Humans
Microplastics is any plastic entity which is approximately less than 5mm in size [6] (http://www.dit.ie/physics/research/microplastics/).
The increase in microplastics can be modelled as a relationship with the human population. This can be descrived by the equation:
\begin{equation}
\begin{array}{cl}
\frac{d P}{dt}=plastic_{rate}H.
\end{array}
\end{equation}

The parameter $plastic_{rate}$ is used the describe the relationship between the human population (H) and the rate of increase in microplastics (P) production.

| The rate of change of population | is equal to $=$ | the current population with a control factor|
|-----------------------------------|--------------|------------------------------------------------------------------|
| The increase in microplastics  $\frac{d P}{dt}$ | is equal to $=$ | the current human population $H$ times the plastic production rate $plastic_{rate}$ |

Positive values of production rate mean the more humans, the more microplastics.
Even if the production of micropalstics is stopped, the amount of microplastics will remain the same. So even after humans are gone, our plastic foot print will remain.

```python
def plastic_model(plastic_rate):
    humans=6*np.ones(N) # 30 billion salmon

    plastics=1*np.ones(N) # 1 billion microplastics
    for i in range (1,N):
        plastics[i]=plastics[i-1]+h*humans[i-1]*(plastic_rate)
        humans[i]=humans[i-1]+h*humans[i-1]*(0.05-0.003*humans[i-1])

    fig= plt.figure(figsize=(8,3))
    plt.plot(time, plastics, 'g', label = 'Microplastics')
    plt.plot(time, humans, 'y-', label = 'Humans')
    plt.xlabel('time (yrs)')
    plt.ylabel('Population')
    plt.legend(loc='best')
    plt.tight_layout()
    plt.title('Figure 4: Relationship of Human Population and Microplastics with a plastic rate =  %s'%(plastic_rate))
    plt.show()
```

In Figure 4 the rate of microplastic production can be manipulated to illustrate the result of a change in amount of microplastics.  The figure plots modelled microplastics (green) and human population as a function of time from 2000 to 2100.

```python
plastic_model(0.01);
```

# Microplastics and Fish
While fishing quotas account for the direct impact of man on fish it does not account for by-products of modern life like mircroplastics to the ecosystem. It has been documented that fish are now
consuming microplastics which can impact the population of fish.
For these reasons we consider microplastics as a predator of small fish.
To model the impact of microplastics on small fish (prey) and the knock on effect it has on salmon (prey).

To model the negative impact of microplastics on the small fish population an extra term is added to the small fish equation:
\begin{equation}
\begin{array}{cl}
\frac{d F}{dt}=F-0.02SF-0.01FP.\\
\end{array}
\end{equation}

| The rate of change of population   | is equal to $=$ | the current population with a control factor|
|-----------------------------------|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| The change in small fish  $\frac{d F}{dt}$ | is equal to $=$ | the current fish population $F$ minus the amount salmon eat $-0.02SF$ minus the amount that die due to microplastics $-0.01FP$ |

## The ecosystem model
Bringing all these equations together results in a system of four first order differential equations which describe a simple ecosystem of,

the population of Small Fish (F),
\begin{equation}
\frac{d F}{dt}=F-0.02SF-0.01FP;
\end{equation}
the population of salmon (S),
\begin{equation}
\frac{d S}{dt}=-0.05S+0.001 F S;
\end{equation}
plastic production (P),
\begin{equation}
\frac{d P}{dt}=plastic_{rate}H;
\end{equation}
and human population (H),
\begin{equation}
\frac{d H}{dt}=0.0001H-0.002H^2+0.0006SH+0.001HF.
\end{equation}
The system was modeled from 2000 to the year 2100 are shown in the Figures bellow for different plastic rates. The figures show the small fish population  in red, the salmon population in blue, the plastics in green and human population in yellow.

```python
def plastic_fish_model(plastic_rate):
    # INITIAL CONDITIONS
    fish=50*np.ones(N) # 60 billions small fish
    salmon=45*np.ones(N) # 30 billion salmon
    plastics=0.001*np.ones(N) # 1 billion microplastics
    humans=6*np.ones(N) # 7 billion humans

    SMALLFISH = np.array( [1,-0.02,-0.01])
    SALMON= np.array([-0.05,0.001,0])
    for i in range (1,N):
        fish[i]=fish[i-1]+h*fish[i-1]*(SMALLFISH[0]+SMALLFISH[1]*salmon[i-1]+SMALLFISH[2]*(plastics [i-1]))
        salmon[i]=salmon[i-1]+h*salmon[i-1]*(SALMON[0]+SALMON[1]*fish[i-1]+SALMON[2]*plastics [i-1])
        plastics[i]=plastics[i-1]+h*humans[i-1]*(plastic_rate)
        humans[i]=humans[i-1]+h*humans[i-1]*(0.05-0.003*humans[i-1])
    fig = plt.figure(figsize=(8,6))
    plt.title('Figure: Impact of plastic rate =  %s on the ecosystem'%(plastic_rate))
    plt.plot(time, fish, 'r-', label='small fish')
    plt.plot(time, salmon, 'b-', label='salmon')
    plt.plot(time, plastics, 'g', label='Microplastics')
    plt.plot(time, humans, 'y', label='Humans')
    plt.grid()
    plt.title('The impact of microplastics')
    plt.xlabel('time (years)')
    plt.ylabel('Population in Billions')
    plt.legend(loc='best')
    plt.tight_layout()
    plt.show()
```

# Results
The figure below shows the output of the model for a plastic rate of 0.05. The introduction of microplastics results in a decrease in the small fish population which then impacts the salmon population as small fish are their primary food source.

```python
plastic_fish_model(0.05)
```

If the production rate of plastics is doubled to 0.1. The introduction of microplastics in to the ecosystem will lead to the extinction of small fish (red) by 2080 and salmon (blue) will be more or less extinct by 2100.

```python
plastic_fish_model(0.1)
```

# Open Questions

The model shows the destructive impact of microplastics on a simple ecosystem model of small fish, salmon, microplastics and humans. Mathematical models of this type can be used to discuss

The immediate questions that arise from this simulation are:
1. What is our current rate of plastic production?
2. How will the extinction of small fish impact the human population?
3. How can we do to prevent this from happening?

# References

[1] Lynx-hare  fur  trade.https://theglyptodon.wordpress.com/2011/05/02/the-fur-trades-records/.  Accessed:2018-03-20.

[2] Wikipedia contributors. (2018, July 27). Lotka–Volterra equations. In Wikipedia, The Free Encyclopedia. Retrieved 09:33, September 7, 2018, from https://en.wikipedia.org/w/index.php?title=Lotka%E2%80%93Volterra_equations&oldid=852191432

[3] Alfred J Lotka.   Analytical note on certain rhythmic  relations inorganic  systems.Proceedings of the National Academy of Sciences,6(7):410–415,1920.

[4]  Vito Volterra.  Fluctuations in the abundance of a species consid-ered mathematically.Nature,118:558–560,1926.

[5] Harari, Yuval N. (2015). Sapiens : a brief history of humankind. New York :Harper.

[6] Gordon Chambers, (2017) Microplastics, Retrieved 09:33, September 7, 2018, http://www.dit.ie/physics/research/microplastics/

```python

```

.ipynb_checkpoints/Predator Prey Model Plastics Reduced-checkpoint.ipynb


# Two photon emission rate of Rb-87 atom in the free space

This code calculates the two photon emission rates for rubidium-87 atom in the free space. The details of the physics is given in ref. [1]. This code uses lots of the libraries developed by https://arc-alkali-rydberg-calculator.readthedocs.io/en/latest/detailed_doc.html [2].

[1]. Generating heralded high-dimensional hyper-entangled photons using Rydberg atoms,...

[2]. N. Sibalic, J.D. Pritchard, C.S. Adams, and K.J.Weatherill, Arc: An open-source library for calculating properties of alkali rydberg atoms, Computer Physics Communications 220, 319-331 (2017).

In this file, we have calculated the TPE rates of Rb atom without cavity. We have not added the contribution of continuum states in this calculation because we found that for Rb atoms the contribution of continuum states are negligible. Detail explanation is given in ref.[1]

The TPE rate for rubidium atom in the free space is given as:
\begin{align}
\Gamma   &= \frac{3^2 Z^{10}}{2^{11}} R_H \alpha^6 c \Big( \frac{k_{fi}}{k_0}\Big)^5 \int_{y=0}^{1} y^3 (1-y)^3 dy  \Big| \sum_m d_{fm}d_{mi} \Big( \frac{1}{y - y_{im}} + \frac{1}{1 - y - y_{im}} \Big) \Big|^2
\end{align}

Please check the Appendix B of ref. [1] for better understanding of the formula above.

```python
#This program is to connect with the library

# Configure the matplotlib graphics library and configure it to show
# show figures inline in the notebook
%matplotlib inline
import matplotlib.pyplot as plt  # Import library for direct plotting functions
import numpy as np               # Import Numerical Python
from IPython.core.display import display, HTML #Import HTML for formatting output

# NOTE: Uncomment following lines ONLY if you are not using installation via pip
import sys,os
rootDir = '/home/sutapa/ARC-Alkali-Rydberg-Calculator-2.0.5/' # e.g. '/Users/Username/Desktop/ARC-Alkali-Rydberg-Calculator'
sys.path.insert(0,rootDir)

from arc import *                 #Import ARC (Alkali Rydberg Calculator)
```

The two photon rate calculation for Rb Rydberg state

```python
from scipy.special import assoc_laguerre
from scipy.special import factorial
from scipy import integrate
import array as arr
import matplotlib.pyplot as plt

#This function calculates the product of the dipole matrix for the transition from (n1,l1,j1) to (m,lm,jm) and the transition from (m,lm,jm) to (n2,l2,j2)

def kappa(n1,l1,n2,l2,m,lm):
    Adip1=atom.getRadialMatrixElement(n2,l2,j2,m,lm,jm)
    Adip2=atom.getRadialMatrixElement(m,lm,jm,n1,l1,j1)
    return(Adip1*Adip2)

def Mnr(y,ym):
    fm= 1/(y+ym)-(1/(y-1-ym))
    return(fm)

# Defining the atoms
atom=Rubidium87()
atom1=Hydrogen()

# Initial state
n1 = 60
l1 = 0
j1=0.5

#final state
n2= 5
l2=0
j2=0.5

#virtual state,m
lm = 1
jm=1.5

#Physical parameters
alpha= 1/137.036   #fine structure spliting
c = 2.99792458*pow(10,8)
Rh = 10973731.5685   #Rydberg constant

coefficient=9*pow(alpha,6)*Rh*c/2**10*(abs((atom.getEnergy(n2,l2,j2)-atom.getEnergy(n1,l1,j1))/(atom1.getEnergy(2,0,0.5)-atom1.getEnergy(1,0,0.5))))**5
sumnr=0.0;
y= np.arange(0.00001,1,0.00001)

for m in range(n1,n1+100):
    ym=(atom.getEnergy(m,lm,jm)-atom.getEnergy(n1,l1,j1))/(atom.getEnergy(n1,l1,j1)-atom.getEnergy(n2,l2,j2))
    kap=kappa(n1,l1,n2,l2,m,lm)
    sumnr=sumnr+(Mnr(y,ym)*abs(kap))

#TPE spectrum
phi= coefficient*pow(y,3)*pow((1-y),3)*(sumnr*sumnr)

#TPE rates
Anr=0.5*integrate.simps(phi,y)
print(Anr)

#Plot
plt.figure(1)
fig, axs = plt.subplots(1, 1, constrained_layout=True)
plt.plot(y,phi, color='blue', linewidth=4,label='60S to 6S')
plt.legend(loc="upper right")
plt.ylabel('$ϕ_{free}$($sec^{-1}$)', fontsize = 20)
plt.xlabel('$E/E_{if}$', fontsize = 20)
#plt.title('Rb-atom, 60S -> 6S', fontsize = 20)
plt.xticks(size = 10)
plt.yticks(size = 10)

#np.savetxt('phi_free_60D_6S.csv', phi)
#np.savetxt('y_free_60D_6S.csv',y)

#plt.savefig('phi_free_60S.pdf',dpi=100)
```


```python
%matplotlib inline
```

.. currentmodule:: dgl

Message Passing Tutorial
========================

**Author**: `Minjie Wang <https://jermainewang.github.io/>`_, Quan Gan, Yu Gai,
Zheng Zhang

In this tutorial, you learn how to use different levels of the message
passing API with PageRank on a small graph. In DGL, the message passing and
feature transformations are **user-defined functions** (UDFs).

The PageRank algorithm
----------------------
In each iteration of PageRank, every node (web page) first scatters its
PageRank value uniformly to its downstream nodes. The new PageRank value of
each node is computed by aggregating the received PageRank values from its
neighbors, which is then adjusted by the damping factor:

\begin{align}PV(u) = \frac{1-d}{N} + d \times \sum_{v \in \mathcal{N}(u)}
   \frac{PV(v)}{D(v)}\end{align}

where $N$ is the number of nodes in the graph; $D(v)$ is the
out-degree of a node $v$; and $\mathcal{N}(u)$ is the neighbor
nodes.

A naive implementation
----------------------
Create a graph with 100 nodes by using ``networkx`` and then convert it to a
:class:`DGLGraph`.

```python
import networkx as nx
import matplotlib.pyplot as plt
import torch
import dgl

N = 100  # number of nodes
DAMP = 0.85  # damping factor
K = 10  # number of iterations
g = nx.nx.erdos_renyi_graph(N, 0.1)
g = dgl.DGLGraph(g)
nx.draw(g.to_networkx(), node_size=50, node_color=[[.5, .5, .5,]])
plt.show()
```

According to the algorithm, PageRank consists of two phases in a typical
scatter-gather pattern. Initialize the PageRank value of each node
to $\frac{1}{N}$ and then store each node's out-degree as a node feature.

```python
g.ndata['pv'] = torch.ones(N) / N
g.ndata['deg'] = g.out_degrees(g.nodes()).float()
```

Define the message function, which divides every node's PageRank
value by its out-degree and passes the result as message to its neighbors.

```python
def pagerank_message_func(edges):
    return {'pv' : edges.src['pv'] / edges.src['deg']}
```

In DGL, the message functions are expressed as **Edge UDFs**.  Edge UDFs
take in a single argument ``edges``.  It has three members ``src``, ``dst``,
and ``data`` for accessing source node features, destination node features,
and edge features.  Here, the function computes messages only
from source node features.

Define the reduce function, which removes and aggregates the
messages from its ``mailbox``, and computes its new PageRank value.

```python
def pagerank_reduce_func(nodes):
    msgs = torch.sum(nodes.mailbox['pv'], dim=1)
    pv = (1 - DAMP) / N + DAMP * msgs
    return {'pv' : pv}
```

The reduce functions are **Node UDFs**.  Node UDFs have a single argument
``nodes``, which has two members ``data`` and ``mailbox``.  ``data``
contains the node features and ``mailbox`` contains all incoming message
features, stacked along the second dimension (hence the ``dim=1`` argument).

The message UDF works on a batch of edges, whereas the reduce UDF works on
a batch of edges but outputs a batch of nodes. Their relationships are as
follows:

Register the message function and reduce function, which will be called
later by DGL.

```python
g.register_message_func(pagerank_message_func)
g.register_reduce_func(pagerank_reduce_func)
```

The algorithm is straightforward. Here is the code for one
PageRank iteration.

```python
def pagerank_naive(g):
    # Phase #1: send out messages along all edges.
    for u, v in zip(*g.edges()):
        g.send((u, v))
    # Phase #2: receive messages to compute new PageRank values.
    for v in g.nodes():
        g.recv(v)
```

Batching semantics for a large graph
------------------------------------
The above code does not scale to a large graph because it iterates over all
the nodes. DGL solves this by allowing you to compute on a *batch* of nodes or
edges. For example, the following codes trigger message and reduce functions
on multiple nodes and edges at one time.

```python
def pagerank_batch(g):
    g.send(g.edges())
    g.recv(g.nodes())
```

You are still using the same reduce function ``pagerank_reduce_func``,
where ``nodes.mailbox['pv']`` is a *single* tensor, stacking the incoming
messages along the second dimension.

You might wonder if this is even possible to perform reduce on all
nodes in parallel, since each node may have different number of incoming
messages and you cannot really "stack" tensors of different lengths together.
In general, DGL solves the problem by grouping the nodes by the number of
incoming messages, and calling the reduce function for each group.

Use higher-level APIs for efficiency
---------------------------------------
DGL provides many routines that combine basic ``send`` and ``recv`` in
various ways. These routines are called **level-2 APIs**. For example, the next code example
shows how to further simplify the PageRank example with such an API.

```python
def pagerank_level2(g):
    g.update_all()
```

In addition to ``update_all``, you can use ``pull``, ``push``, and ``send_and_recv``
in this level-2 category. For more information, see :doc:`API reference <../../api/python/graph>`.

Use DGL ``builtin`` functions for efficiency
------------------------------------------------
Some of the message and reduce functions are used frequently. For this reason, DGL also
provides ``builtin`` functions. For example, two ``builtin`` functions can be
used in the PageRank example.

* :func:`dgl.function.copy_src(src, out) <function.copy_src>` - This
  code example is an edge UDF that computes the
  output using the source node feature data. To use this, specify the name of
  the source feature data (``src``) and the output name (``out``).

* :func:`dgl.function.sum(msg, out) <function.sum>` - This code example is a node UDF
  that sums the messages in
  the node's mailbox. To use this, specify the message name (``msg``) and the
  output name (``out``).

The following PageRank example shows such functions.

```python
import dgl.function as fn

def pagerank_builtin(g):
    g.ndata['pv'] = g.ndata['pv'] / g.ndata['deg']
    g.update_all(message_func=fn.copy_src(src='pv', out='m'),
                 reduce_func=fn.sum(msg='m',out='m_sum'))
    g.ndata['pv'] = (1 - DAMP) / N + DAMP * g.ndata['m_sum']
```

In the previous example code, you directly provide the UDFs to the :func:`update_all <DGLGraph.update_all>`
as its arguments.
This will override the previously registered UDFs.

In addition to cleaner code, using ``builtin`` functions also gives DGL the
opportunity to fuse operations together. This results in faster execution.  For
example, DGL will fuse the ``copy_src`` message function and ``sum`` reduce
function into one sparse matrix-vector (spMV) multiplication.

`The following section <spmv_>`_ describes why spMV can speed up the scatter-gather
phase in PageRank.  For more details about the ``builtin`` functions in DGL,
see :doc:`API reference <../../api/python/function>`.

You can also download and run the different code examples to see the differences.

```python
for k in range(K):
    # Uncomment the corresponding line to select different version.
    # pagerank_naive(g)
    # pagerank_batch(g)
    # pagerank_level2(g)
    pagerank_builtin(g)
print(g.ndata['pv'])
```

Using spMV for PageRank
-----------------------
Using ``builtin`` functions allows DGL to understand the semantics of UDFs.
This allows you to create an efficient implementation. For example, in the case
of PageRank, one common method to accelerate it is by using its linear algebra
form.

\begin{align}\mathbf{R}^{k} = \frac{1-d}{N} \mathbf{1} + d \mathbf{A}*\mathbf{R}^{k-1}\end{align}

Here, $\mathbf{R}^k$ is the vector of the PageRank values of all nodes
at iteration $k$; $\mathbf{A}$ is the sparse adjacency matrix
of the graph.
Computing this equation is quite efficient because there is an efficient
GPU kernel for the sparse matrix-vector multiplication (spMV). DGL
detects whether such optimization is available through the ``builtin``
functions. If a certain combination of ``builtin`` can be mapped to an spMV
kernel (e.g., the PageRank example), DGL uses it automatically. We recommend
using ``builtin`` functions whenever possible.

Next steps
----------

* Learn how to use DGL (:doc:`builtin functions<../../features/builtin>`) to write
  more efficient message passing.
* To see model tutorials, see the :doc:`overview page<../models/index>`.
* To learn about Graph Neural Networks, see :doc:`GCN tutorial<../models/1_gnn/1_gcn>`.
* To see how DGL batches multiple graphs, see :doc:`TreeLSTM tutorial<../models/2_small_graph/3_tree-lstm>`.
* Play with some graph generative models by following tutorial for :doc:`Deep Generative Model of Graphs<../models/3_generative_model/5_dgmg>`.
* To learn how traditional models are interpreted in a view of graph, see
  the tutorials on :doc:`CapsuleNet<../models/4_old_wines/2_capsule>` and
  :doc:`Transformer<../models/4_old_wines/7_transformer>`.