# Markdown 與 LaTeX簡介

每個範例內容將範例語法與輸出分為不同的 Cell 放置，雙擊 Cell 也可以查看原始碼，進行進一步的研究與實驗。

## 1. Markdown 主要語法

### 1.1 段落和斷行

_範例語法：_
```
前軍至夏口，周瑜問：「荊州有人在前面接否？」人報：「劉皇叔使糜竺來見都督。」瑜喚至，問勞軍如何。糜竺曰：「主公皆準備安排下了。」瑜曰：「皇叔何在？」竺曰：「在荊州城門相等，與都督把盞。」瑜曰：「今為汝家之事，出兵遠征；勞軍之禮，休得輕易。」糜竺領了言語先回。<br />戰船密密排在江上，依次而進。
```
_輸出：(可以看到在"戰船"前面有換行)_

前軍至夏口，周瑜問：「荊州有人在前面接否？」人報：「劉皇叔使糜竺來見都督。」瑜喚至，問勞軍如何。糜竺曰：「主公皆準備安排下了。」瑜曰：「皇叔何在？」竺曰：「在荊州城門相等，與都督把盞。」瑜曰：「今為汝家之事，出兵遠征；勞軍之禮，休得輕易。」糜竺領了言語先回。
戰船密密排在江上，依次而進。

### 1.2 標題 (Heading)

_範例語法：_

```
# 主標題 (等同於 HTML `<h1></h1>`)

## 次標題 (等同於 `<h2></h12`)

### 第三階 (等同於 `<h3></h3>`)

#### 第四階 (等同於 `<h4></h4>`)

##### 第五階 (等同於 `<h5></h5>`)

###### 第六階 (等同於 `<h6></h6>`)
```

_輸出：_

# 主標題 (等同於 HTML `<h1></h1>`)

## 次標題 (等同於 `<h2></h12`)

### 第三階 (等同於 `<h3></h3>`)

#### 第四階 (等同於 `<h4></h4>`)

##### 第五階 (等同於 `<h5></h5>`)

###### 第六階 (等同於 `<h6></h6>`)

### 1.3 無序號列表 (Bullet list)

_範例語法：_

```
六都清單：
- 台北市
    - 大安區
    - 大同區
- 新北市
* 桃園市
* 台中市
+ 台南市
+ 高雄市
```

_輸出：_

六都清單：
- 台北市
    - 大安區
    - 大同區
- 新北市
* 桃園市
* 台中市
+ 台南市
+ 高雄市

### 1.4 序號列表 (Numbered list)

_範例語法：_

```
1. 第一項
2. 第二項
3. 第三項
```

_輸出：_

1. 第一項
2. 第二項
3. 第三項

### 1.5 區塊引言 (blockquoting)

_範例語法：_

```
> 卻說魯肅回見周瑜，說玄德，孔明歡喜不疑，準備出城勞軍。周瑜大笑曰：「原來今番也中了吾計！」便教魯肅稟報吳侯，並遣程普引兵接應。周瑜此時箭瘡已漸平愈，身軀無事，使甘寧為先鋒，自與徐盛，丁奉為第二；淩統，呂蒙為後隊。水陸大兵五百萬，望荊州而來。

周瑜在船中，時復歡笑，以為孔明中計。

> 前軍至夏口，周瑜問：「荊州有人在前面接否？」人報：「劉皇叔使糜竺來見都督。」瑜喚至，問勞軍如何。
```

_輸出：_

> 卻說魯肅回見周瑜，說玄德，孔明歡喜不疑，準備出城勞軍。周瑜大笑曰：「原來今番也中了吾計！」便教魯肅稟報吳侯，並遣程普引兵接應。周瑜此時箭瘡已漸平愈，身軀無事，使甘寧為先鋒，自與徐盛，丁奉為第二；淩統，呂蒙為後隊。水陸大兵五百萬，望荊州而來。

周瑜在船中，時復歡笑，以為孔明中計。

> 前軍至夏口，周瑜問：「荊州有人在前面接否？」人報：「劉皇叔使糜竺來見都督。」瑜喚至，問勞軍如何。

### 1.6 程式碼區塊

_範例語法：_

\`\`\`julia

println("Hello Julia")

\`\`\`

_輸出：_

```julia
println("Hello Julia")
```

### 1.7 分隔線

_範例語法：_

```
卻說魯肅回見周瑜，說玄德，孔明歡喜不疑，準備出城勞軍。周瑜大笑曰：「原來今番也中了吾計！」便教魯肅稟報吳侯，並遣程普引兵接應。周瑜此時箭瘡已漸平愈，身軀無事，使甘寧為先鋒，自與徐盛，丁奉為第二；淩統，呂蒙為後隊。水陸大兵五百萬，望荊州而來。周瑜在船中，時復歡笑，以為孔明中計。

---

前軍至夏口，周瑜問：「荊州有人在前面接否？」人報：「劉皇叔使糜竺來見都督。」瑜喚至，問勞軍如何。糜竺曰：「主公皆準備安排下了。」瑜曰：「皇叔何在？」竺曰：「在荊州城門相等，與都督把盞。」瑜曰：「今為汝家之事，出兵遠征；勞軍之禮，休得輕易。」糜竺領了言語先回。<br />戰船密密排在江上，依次而進。
```

_輸出：_

卻說魯肅回見周瑜，說玄德，孔明歡喜不疑，準備出城勞軍。周瑜大笑曰：「原來今番也中了吾計！」便教魯肅稟報吳侯，並遣程普引兵接應。周瑜此時箭瘡已漸平愈，身軀無事，使甘寧為先鋒，自與徐盛，丁奉為第二；淩統，呂蒙為後隊。水陸大兵五百萬，望荊州而來。周瑜在船中，時復歡笑，以為孔明中計。

---

前軍至夏口，周瑜問：「荊州有人在前面接否？」人報：「劉皇叔使糜竺來見都督。」瑜喚至，問勞軍如何。糜竺曰：「主公皆準備安排下了。」瑜曰：「皇叔何在？」竺曰：「在荊州城門相等，與都督把盞。」瑜曰：「今為汝家之事，出兵遠征；勞軍之禮，休得輕易。」糜竺領了言語先回。
戰船密密排在江上，依次而進。

### 1.8 超連結

_範例語法：_

```
[Cupoy - 為你探索世界的新知](https://www.cupoy.com)
```

_輸出：_

[Cupoy - 為你探索世界的新知](https://www.cupoy.com)

### 1.9 嵌入圖片

_範例語法：_

```

```

_輸出：_

### 1.10 表格

_範例語法：_

```
|姓名|國家|地址|年齡|
|---|---|---|---|
|John Doe|中華民國台灣|台北市大安區敦化街1號|25|
```

_輸出：_

|姓名|國家|地址|年齡|
|---|---|---|---|
|John Doe|中華民國台灣|台北市大安區敦化街1號|25|

## 2. 用 LaTeX 寫數學公式

### 2.1 Inline 模式

_範例語法：_

```
$\LARGE f(\displaystyle\sum_i w_i x_i + b)$
```

_輸出：_

$\LARGE f(\displaystyle\sum_i w_i x_i + b)$

### 2.2 Block 模式

_範例語法：_

```
$$\LARGE f(\displaystyle\sum_i w_i x_i + b)$$
```

_輸出：_

$$\LARGE f(\displaystyle\sum_i w_i x_i + b)$$

_範例語法：_

```
\begin{equation}
\LARGE f(\displaystyle\sum_i w_i x_i + b)
\end{equation}
```

_輸出：_

\begin{equation}
\LARGE f(\displaystyle\sum_i w_i x_i + b)
\end{equation}

_範例語法：_

```
\begin{align}
\LARGE f(\displaystyle\sum_i w_i x_i + b)
\end{align}
```

_輸出：_

\begin{align}
\LARGE f(\displaystyle\sum_i w_i x_i + b)
\end{align}

## 3. 結合 Markdown 和 LaTeX 數學公式

_範例語法：_

```
公式 $\LARGE f(\displaystyle\sum_i w_i x_i + b)$ 是 Deep Learning 課程中常會見到的基本公式

結合 Markdown 和 LaTeX 數學公式，我們可以撰寫出漂亮的文件筆記，讓學習更有效率。
```

_輸出：_

公式 $\LARGE f(\displaystyle\sum_i w_i x_i + b)$ 是 Deep Learning 課程中常會見到的基本公式

結合 Markdown 和 LaTeX 數學公式，我們可以撰寫出漂亮的文件筆記，讓學習更有效率。

```julia

```


Lambda School Data Science

*Unit 2, Sprint 3, Module 1*

---

# Define ML problems

You will use your portfolio project dataset for all assignments this sprint.

## Assignment

Complete these tasks for your project, and document your decisions.

- [ ] Choose your target. Which column in your tabular dataset will you predict?
- [ ] Is your problem regression or classification?
- [ ] How is your target distributed?
    - Classification: How many classes? Are the classes imbalanced?
    - Regression: Is the target right-skewed? If so, you may want to log transform the target.
- [ ] Choose which observations you will use to train, validate, and test your model.
    - Are some observations outliers? Will you exclude them?
    - Will you do a random split or a time-based split?
- [ ] Choose your evaluation metric(s).
    - Classification: Is your majority class frequency > 50% and < 70% ? If so, you can just use accuracy if you want. Outside that range, accuracy could be misleading. What evaluation metric will you choose, in addition to or instead of accuracy?
- [ ] Begin to clean and explore your data.
- [ ] Begin to choose which features, if any, to exclude. Would some features "leak" future information?

```python
import pandas as pd
adoption_url = 'https://data.austintexas.gov/resource/9t4d-g238.csv?$limit=100000'
adoption = pd.read_csv(adoption_url)
# in order to see all of the columns:
pd.options.display.max_columns = 100
```

# Target

```python
adoption.head()
```

      animal_id
      name
      datetime
      monthyear
      date_of_birth
      outcome_type
      outcome_subtype
      animal_type
      sex_upon_outcome
      age_upon_outcome
      breed
      color

      0
      A808084
      *Dori
      2019-11-13T16:56:00.000
      2019-11-13T16:56:00.000
      2018-11-03T00:00:00.000
      Adoption
      NaN
      Dog
      Spayed Female
      1 year
      Australian Cattle Dog Mix
      Brown/Tricolor

      1
      A808715
      Kanga
      2019-11-13T16:41:00.000
      2019-11-13T16:41:00.000
      2019-04-12T00:00:00.000
      Adoption
      NaN
      Dog
      Intact Female
      7 months
      Dutch Shepherd/Belgian Malinois
      Brown Brindle

      2
      A802292
      George
      2019-11-13T16:39:00.000
      2019-11-13T16:39:00.000
      2017-08-16T00:00:00.000
      Adoption
      NaN
      Dog
      Neutered Male
      2 years
      Rhod Ridgeback/Labrador Retriever
      Brown

      3
      A697397
      Blue
      2019-11-13T16:33:00.000
      2019-11-13T16:33:00.000
      2006-11-12T00:00:00.000
      Euthanasia
      Suffering
      Cat
      Neutered Male
      13 years
      Domestic Shorthair Mix
      Blue

      4
      A802452
      Hobbey
      2019-11-13T16:29:00.000
      2019-11-13T16:29:00.000
      2019-02-18T00:00:00.000
      Return to Owner
      NaN
      Dog
      Neutered Male
      8 months
      German Shepherd Mix
      Black/Tan

```python
adoption.shape
```

    (100000, 12)

The target in this project will be to predict whether or not an animal will be adopted or not (transferred to another shelter or, sadly, euthanized) so that perhaps animal shelters, though overwhelmed, can give some extra love or use unique methods to get those animals that may not have the best odds forever homes.

```python
adoption['outcome_type'].value_counts(dropna=False)
```

    Adoption           44383
    Transfer           29862
    Return to Owner    17514
    Euthanasia          6297
    Died                 969
    Rto-Adopt            507
    Disposal             387
    Missing               61
    Relocate              17
    NaN                    3
    Name: outcome_type, dtype: int64

```python
# this might be a feature that can create leakage, will come back to it.
adoption['outcome_subtype'].value_counts(dropna=False)
```

    NaN                    54836
    Partner                24944
    Foster                  7950
    Rabies Risk             2790
    SCRP                    2640
    Suffering               2503
    Snr                     2275
    In Kennel                508
    Aggressive               360
    Offsite                  281
    Medical                  254
    In Foster                243
    At Vet                   171
    Behavior                  82
    Enroute                   65
    Underage                  29
    Court/Investigation       21
    In Surgery                19
    Possible Theft            15
    Field                      8
    Barn                       4
    Prc                        1
    Customer S                 1
    Name: outcome_subtype, dtype: int64

# Classification or Regression?

There are 9 classes of outcomes but for this project I'd like to focus on the animals that were adopted or not. There are a large number of animals that were returned to owners but that would just be due to them getting out etc but they do have a home so I will not include those in my project. "Rto-adopt" or return to owner adoption will also be included with "return to owner."

For animals that were adopted I will consider that to be:
-adoption
-rto-adopt (return to owner through adoption)

I will combine the following for not adopted:
-transfer
-euthanasia
-relocate
-missing (animals that went missing from the shelter--still unsuccessful in getting them homes)
"Died" and "disposal" are animals that may have died while at the shelter or were brought in that needed to be properly disposed of so I will not include these either as they may have been very ill when brought in.

# How is the target distributed?
## Are the classes imbalanced?

```python
adoption['outcome_type'].value_counts(normalize=True)
```

    Adoption           0.443843
    Transfer           0.298629
    Return to Owner    0.175145
    Euthanasia         0.062972
    Died               0.009690
    Rto-Adopt          0.005070
    Disposal           0.003870
    Missing            0.000610
    Relocate           0.000170
    Name: outcome_type, dtype: float64

will need to drop the rows where the outcome type are the ones listed above to be excluded:
-Return to owner
-Rto-adopt
-Died
-Disposal

```python
# adoption updated to drop the outcomes we are excluding:
adoption_upd = adoption[~adoption['outcome_type'].isin(['Return to Owner',
                                                        'Rto-Adopt',
                                                        'Died',
                                                        'Disposal'])]
```

```python
adoption_upd['outcome_type'].value_counts(dropna=False)
```

    Adoption      44383
    Transfer      29862
    Euthanasia     6297
    Missing          61
    Relocate         17
    NaN               3
    Name: outcome_type, dtype: int64

```python
# am going to drop all animals except cats and dogs, 2 of the unknown
# outcome types are dogs and 1 also has a lot of other missing info so
# will drop these rows.
adoption_upd = adoption_upd.dropna(subset = ['outcome_type'])

```

```python
# need to redefine classes as binary. Adoption as 'adopted' and
# the rest as 'not adopted'.
def new_status(outcome):
    if outcome == 'Transfer' or outcome == 'Euthanasia' or outcome == 'Missing' or outcome == 'Relocate':
      return 'Not adopted'
    else:
      return 'Adopted'

```

```python
adoption_upd = adoption_upd.copy()
adoption_upd['new_outcome_type'] = adoption_upd['outcome_type'].apply(new_status)
```

```python
adoption_upd['new_outcome_type'].value_counts(normalize=True)
```

    Adopted        0.550521
    Not adopted    0.449479
    Name: new_outcome_type, dtype: float64

The classes now are combined into a binary classification and the classes are not imbalanced.

```python
# drop the original 'Outcome_Type' column:
adoption_upd = adoption_upd.drop(columns='outcome_type')
```

# Choose Observations

As mentioned above, since the focus of my project is predicting if animals that are in need of forever homes will be adopted or not, I have already excluded the following observations from my model:
-Return to Owner
-Rto-Adopt
-Died
-Disposal

There are 3 missing values for the outcome ,so we can try to look at the outcome subtype to see if we can determine what happened to them but the remaining animals have been categorized into 'adopted' or 'not adopted.'

# How to Split Data:

```python
adoption_upd.dtypes
```

    animal_id           object
    name                object
    datetime            object
    monthyear           object
    date_of_birth       object
    outcome_subtype     object
    animal_type         object
    sex_upon_outcome    object
    age_upon_outcome    object
    breed               object
    color               object
    new_outcome_type    object
    dtype: object

```python
adoption_upd['datetime'] = pd.to_datetime(adoption_upd['datetime'], infer_datetime_format=True)
```

```python
adoption_upd['datetime'].dt.year.value_counts()
```

    2015    14792
    2019    14339
    2016    14119
    2017    14058
    2018    13361
    2014     9951
    Name: datetime, dtype: int64

The description of the data set said that Austin is becoming a more pet-friendly city so there may be more animals going in and out of shelters in the more recent data vs the earlier data. I will therefore split the data based on time with the most recent data being the test set and then create a test and validation set with the remaining data.

test = adoption_upd[(adoption_upd['datetime'].dt.year == 2019)]
val =  adoption_upd[(adoption_upd['datetime'].dt.year == 2018)]
train = adoption_upd[(adoption_upd['datetime'].dt.year < 2018)]

```python
# how big to make test set? 2019:
# 14292 observations
```

# Evaluation Metrics

since the classes aren't imbalanced I can use accuracy but will also explore the precision and recall for this problem.

precision positive: correctly predict all the animals that were adopted.

\begin{align}
precision = \frac{accurately \ predicted \ adopted}{total\ predicted \ adopted}
\end{align}

recall positive: of all the animals that were adopted, how many were we able to identify?
\begin{align}
recall = \frac{accurately \ predicted \ adopted}{actually \ adopted}
\end{align}

# Begin to clean data and feature selection

```python
adoption_upd.isnull().sum()
```

    animal_id               0
    name                30055
    datetime                0
    monthyear               0
    date_of_birth           0
    outcome_subtype     36347
    animal_type             0
    sex_upon_outcome        2
    age_upon_outcome       24
    breed                   0
    color                   0
    new_outcome_type        0
    dtype: int64

```python
# the name column has a lot of missing values, will change NaN's to Unknown
adoption_upd['name'].fillna("Unknown", inplace = True)
```

```python
# the outcome subtype is missing 36353 values, almost half of all of our
# data, since we will know all of the outcome types and this may cause
# leakage into the test set because certain outcomes can be deduced from
# the outcome subtype, I will drop that entire column.

adoption_upd = adoption_upd.drop(columns='outcome_subtype')
adoption_upd.shape

```

    (80620, 11)

```python
# the sex_upon_outcome column has 2 missing values but noticed an 'unknown'
# value so check to see if that's common:
adoption_upd['sex_upon_outcome'].value_counts()
# will one hot encode this column.
```

    Neutered Male    27562
    Spayed Female    26009
    Intact Female    10101
    Intact Male       9375
    Unknown           7571
    Name: sex_upon_outcome, dtype: int64

```python
# age_upon_outcome has 25 missing values but date_of_birth has none, lets
# look at how many times 'unknown' shows up in the data:
adoption_upd.isin(['Unknown']).sum()
# the name already has 18 unknowns, so will stick to changing NaN's to unknown.
```

    animal_id               0
    name                30073
    datetime                0
    monthyear               0
    date_of_birth           0
    animal_type             0
    sex_upon_outcome     7571
    age_upon_outcome        0
    breed                   0
    color                   0
    new_outcome_type        0
    dtype: int64

```python
# to see if 'Other is a common entry as well'
adoption_upd.isin(['Other']).sum()
```

    animal_id              0
    name                   0
    datetime               0
    monthyear              0
    date_of_birth          0
    animal_type         4450
    sex_upon_outcome       0
    age_upon_outcome       0
    breed                  0
    color                  0
    new_outcome_type       0
    dtype: int64

```python
# to see what kind of animals come in to the shelter
adoption_upd['animal_type'].value_counts()
```

    Dog          39752
    Cat          35977
    Other         4450
    Bird           432
    Livestock        9
    Name: animal_type, dtype: int64

```python
# initially thinking of only using cats and dogs.
adoption_upd = adoption_upd[~adoption_upd['animal_type'].isin(['Bird',
                                                        'Other',
                                                        'Livestock'])]
```

```python
# check for missing values now:
adoption_upd.isnull().sum()
```

    animal_id           0
    name                0
    datetime            0
    monthyear           0
    date_of_birth       0
    animal_type         0
    sex_upon_outcome    2
    age_upon_outcome    9
    breed               0
    color               0
    new_outcome_type    0
    dtype: int64

```python
adoption_upd['date_of_birth'] = pd.to_datetime(adoption_upd['date_of_birth'], infer_datetime_format=True)
```

```python
adoption_upd.loc[adoption_upd['age_upon_outcome'].isnull()]
```

      animal_id
      name
      datetime
      monthyear
      date_of_birth
      animal_type
      sex_upon_outcome
      age_upon_outcome
      breed
      color
      new_outcome_type

      18
      A808636
      Unknown
      2019-11-13 13:46:00
      2019-11-13T13:46:00.000
      2004-11-11
      Cat
      Intact Female
      NaN
      Siamese
      Seal Point
      Not adopted

      55
      A808702
      Keepers
      2019-11-12 15:20:00
      2019-11-12T15:20:00.000
      2009-11-12
      Dog
      Neutered Male
      NaN
      Golden Retriever
      Gold
      Not adopted

      90
      A808649
      Unknown
      2019-11-11 17:49:00
      2019-11-11T17:49:00.000
      2019-10-11
      Cat
      Intact Male
      NaN
      Domestic Shorthair
      Brown Tabby
      Not adopted

      91
      A738697
      Boots
      2019-11-11 17:48:00
      2019-11-11T17:48:00.000
      2007-11-17
      Dog
      NaN
      NaN
      Miniature Schnauzer Mix
      Black
      Not adopted

      116
      A807543
      Unknown
      2019-11-11 15:22:00
      2019-11-11T15:22:00.000
      2016-10-26
      Cat
      Neutered Male
      NaN
      Domestic Shorthair
      Brown Tabby/White
      Not adopted

      120
      A808626
      Unknown
      2019-11-11 13:57:00
      2019-11-11T13:57:00.000
      2019-09-11
      Cat
      Intact Female
      NaN
      Domestic Shorthair
      Brown Tabby
      Not adopted

      173
      A808466
      Unknown
      2019-11-10 09:35:00
      2019-11-10T09:35:00.000
      2019-09-09
      Cat
      Intact Male
      NaN
      Domestic Shorthair
      Brown/Black
      Not adopted

      306
      A808352
      Unknown
      2019-11-07 16:15:00
      2019-11-07T16:15:00.000
      2011-11-07
      Dog
      Intact Male
      NaN
      Dachshund Mix
      Tan
      Not adopted

      6608
      A752967
      Gray
      2019-07-24 11:42:00
      2019-07-24T11:42:00.000
      2015-06-29
      Dog
      NaN
      NaN
      Pit Bull Mix
      Blue/White
      Not adopted

```python
# 2 of the rows that are missing the age are also missing the sex so I will
# drop those:
adoption_upd = adoption_upd.dropna(subset = ['sex_upon_outcome'])
```

```python
# since there are no missing or unknown values for DOB which still seems strange
# especially for stray animals that were found but maybe they approximated. We can
# create a column where we subtract 2019 from the born on year to get the age and see
# if there are a lot of differences.

now = pd.Timestamp('now')
# first, get DOB year column and DOB month columns:
adoption_upd['DOB_month'] = adoption_upd['date_of_birth'].dt.month
# now, subtract current year from DOB year:
adoption_upd['DOB_year'] = now.year - adoption_upd['date_of_birth'].dt.year
# now turn DOB_year into months by multiplying by 12
adoption_upd['DOB_year'] = adoption_upd['DOB_year'] * 12
# add DOB_year which is now in months to DOB month and divide by 12 to get years
adoption_upd['calculated_age'] = adoption_upd['DOB_year'] + adoption_upd['DOB_month']
def calculate_age(age):
        if age >= 12:
            return(f'{age // 12} years')
        else:
            return(f'{age} months')
adoption_upd['calculated_age'] = adoption_upd['calculated_age'].apply(calculate_age)
# fill NaN's in age upon outcome column with calculated age
adoption_upd['age_upon_outcome'].fillna(adoption_upd['calculated_age'], inplace=True)
# drop DOB month, DOB year, calculated age:
adoption_upd.drop(columns =['DOB_month', 'DOB_year', 'calculated_age'], inplace=True)
```

```python
adoption_upd.isnull().sum()
# no more missing values!
```

    animal_id           0
    name                0
    datetime            0
    monthyear           0
    date_of_birth       0
    animal_type         0
    sex_upon_outcome    0
    age_upon_outcome    0
    breed               0
    color               0
    new_outcome_type    0
    dtype: int64

```python
adoption_upd.head()
```

      animal_id
      name
      datetime
      monthyear
      date_of_birth
      animal_type
      sex_upon_outcome
      age_upon_outcome
      breed
      color
      new_outcome_type

      0
      A808084
      *Dori
      2019-11-13 16:56:00
      2019-11-13T16:56:00.000
      2018-11-03
      Dog
      Spayed Female
      1 year
      Australian Cattle Dog Mix
      Brown/Tricolor
      Adopted

      1
      A808715
      Kanga
      2019-11-13 16:41:00
      2019-11-13T16:41:00.000
      2019-04-12
      Dog
      Intact Female
      7 months
      Dutch Shepherd/Belgian Malinois
      Brown Brindle
      Adopted

      2
      A802292
      George
      2019-11-13 16:39:00
      2019-11-13T16:39:00.000
      2017-08-16
      Dog
      Neutered Male
      2 years
      Rhod Ridgeback/Labrador Retriever
      Brown
      Adopted

      3
      A697397
      Blue
      2019-11-13 16:33:00
      2019-11-13T16:33:00.000
      2006-11-12
      Cat
      Neutered Male
      13 years
      Domestic Shorthair Mix
      Blue
      Not adopted

      5
      A808465
      Unknown
      2019-11-13 16:28:00
      2019-11-13T16:28:00.000
      2019-08-29
      Dog
      Spayed Female
      2 months
      Chihuahua Shorthair/Yorkshire Terrier
      Black/Brown
      Adopted

```python
# looking at redundant columns, date of birth can be dropped since the ages are all accounted for.
# also datetime and monthyear are the same, I will get rid of datetime for now.
adoption_upd.drop(columns=['date_of_birth'], inplace=True)
```

```python
# going to create a new column for the season the animal came into the shelter to see if certain
# seasons have higher adoption rates:
adoption_upd['monthyear'] = pd.to_datetime(adoption_upd['monthyear'], infer_datetime_format=True)
adoption_upd['month_arrived'] = adoption_upd['monthyear'].dt.month
adoption_upd.head()

```

      animal_id
      name
      datetime
      monthyear
      animal_type
      sex_upon_outcome
      age_upon_outcome
      breed
      color
      new_outcome_type
      month_arrived

      0
      A808084
      *Dori
      2019-11-13 16:56:00
      2019-11-13 16:56:00
      Dog
      Spayed Female
      1 year
      Australian Cattle Dog Mix
      Brown/Tricolor
      Adopted
      11

      1
      A808715
      Kanga
      2019-11-13 16:41:00
      2019-11-13 16:41:00
      Dog
      Intact Female
      7 months
      Dutch Shepherd/Belgian Malinois
      Brown Brindle
      Adopted
      11

      2
      A802292
      George
      2019-11-13 16:39:00
      2019-11-13 16:39:00
      Dog
      Neutered Male
      2 years
      Rhod Ridgeback/Labrador Retriever
      Brown
      Adopted
      11

      3
      A697397
      Blue
      2019-11-13 16:33:00
      2019-11-13 16:33:00
      Cat
      Neutered Male
      13 years
      Domestic Shorthair Mix
      Blue
      Not adopted
      11

      5
      A808465
      Unknown
      2019-11-13 16:28:00
      2019-11-13 16:28:00
      Dog
      Spayed Female
      2 months
      Chihuahua Shorthair/Yorkshire Terrier
      Black/Brown
      Adopted
      11

```python
def season(arrival_month):
    if arrival_month == 1 or arrival_month == 2 or arrival_month == 3:
        return 'Winter'
    if arrival_month == 3 or arrival_month == 4 or arrival_month ==5:
        return 'Spring'
    if arrival_month == 6 or arrival_month == 7 or arrival_month ==8:
        return 'Summer'
    if arrival_month == 9 or arrival_month == 10 or arrival_month ==11:
        return 'Fall'
adoption_upd['season_arrived'] = adoption_upd['month_arrived'].apply(season)
```

```python
# want to change the datetime column to year only since we have month_arrived now:
adoption_upd['year_arrived'] = adoption_upd['datetime'].dt.year
```

```python
# going to drop datetime and monthyear now:
adoption_upd.drop(columns=['datetime', 'monthyear'], inplace=True)
```

```python
adoption_upd.head(10)
```

      animal_id
      name
      animal_type
      sex_upon_outcome
      age_upon_outcome
      breed
      color
      new_outcome_type
      month_arrived
      season_arrived
      year_arrived

      0
      A808084
      *Dori
      Dog
      Spayed Female
      1 year
      Australian Cattle Dog Mix
      Brown/Tricolor
      Adopted
      11
      Fall
      2019

      1
      A808715
      Kanga
      Dog
      Intact Female
      7 months
      Dutch Shepherd/Belgian Malinois
      Brown Brindle
      Adopted
      11
      Fall
      2019

      2
      A802292
      George
      Dog
      Neutered Male
      2 years
      Rhod Ridgeback/Labrador Retriever
      Brown
      Adopted
      11
      Fall
      2019

      3
      A697397
      Blue
      Cat
      Neutered Male
      13 years
      Domestic Shorthair Mix
      Blue
      Not adopted
      11
      Fall
      2019

      5
      A808465
      Unknown
      Dog
      Spayed Female
      2 months
      Chihuahua Shorthair/Yorkshire Terrier
      Black/Brown
      Adopted
      11
      Fall
      2019

      6
      A804539
      Bubbles
      Cat
      Spayed Female
      3 months
      Domestic Shorthair Mix
      Brown Tabby
      Adopted
      11
      Fall
      2019

      9
      A807697
      Lucy
      Dog
      Spayed Female
      1 year
      Basenji Mix
      Red
      Adopted
      11
      Fall
      2019

      10
      A799663
      Cookie
      Dog
      Neutered Male
      2 years
      Rat Terrier
      Black/Tricolor
      Adopted
      11
      Fall
      2019

      11
      A808495
      Unknown
      Cat
      Intact Male
      7 months
      Domestic Shorthair
      Black/White
      Not adopted
      11
      Fall
      2019

      12
      A808494
      Unknown
      Cat
      Intact Male
      7 months
      Domestic Shorthair
      Black
      Not adopted
      11
      Fall
      2019

```python
# how many different breeds are there:
adoption_upd['breed'].value_counts()
# 2042...very high cardinality
```

    Domestic Shorthair Mix                            26070
    Pit Bull Mix                                       4722
    Labrador Retriever Mix                             4373
    Chihuahua Shorthair Mix                            3974
    Domestic Shorthair                                 3552
    Domestic Medium Hair Mix                           2549
    German Shepherd Mix                                1816
    Domestic Longhair Mix                              1184
    Siamese Mix                                        1022
    Australian Cattle Dog Mix                           968
    Dachshund Mix                                       627
    Border Collie Mix                                   594
    Boxer Mix                                           572
    Domestic Medium Hair                                478
    Miniature Poodle Mix                                454
    Catahoula Mix                                       432
    Labrador Retriever                                  405
    Staffordshire Mix                                   401
    Chihuahua Shorthair                                 399
    Pit Bull                                            384
    Australian Shepherd Mix                             380
    Pointer Mix                                         367
    Great Pyrenees Mix                                  367
    Rat Terrier Mix                                     364
    Beagle Mix                                          358
    German Shepherd                                     347
    Yorkshire Terrier Mix                               346
    Siberian Husky Mix                                  344
    Jack Russell Terrier Mix                            338
    Cairn Terrier Mix                                   335
                                                      ...
    Anatol Shepherd/Mastiff                               1
    Domestic Longhair/Persian                             1
    Bull Terrier/Pit Bull                                 1
    Standard Schnauzer/Soft Coated Wheaten Terrier        1
    Yorkshire Terrier/Bruss Griffon                       1
    Labrador Retriever/English Pointer                    1
    Pembroke Welsh Corgi/Chihuahua Longhair               1
    Newfoundland/Border Collie                            1
    Chow Chow/Basset Hound                                1
    Jack Russell Terrier/Unknown                          1
    English Bulldog/Pit Bull                              1
    Basset Hound/Pembroke Welsh Corgi                     1
    Chinese Sharpei/Boxer                                 1
    Border Collie/Miniature Pinscher                      1
    Greater Swiss Mountain Dog Mix                        1
    Basset Hound/Black/Tan Hound                          1
    Plott Hound/Mastiff                                   1
    Lhasa Apso/Dachshund                                  1
    Australian Shepherd/Australian Kelpie                 1
    Whippet                                               1
    Cardigan Welsh Corgi/Australian Kelpie                1
    Staffordshire/Pit Bull                                1
    Whippet/German Shepherd                               1
    Boykin Span/Dachshund                                 1
    Smooth Fox Terrier/Labrador Retriever                 1
    English Coonhound/Border Collie                       1
    Jack Russell Terrier/Anatol Shepherd                  1
    Chihuahua Shorthair/Boxer                             1
    Greyhound/Border Terrier                              1
    Pit Bull/Rhod Ridgeback                               1
    Name: breed, Length: 1866, dtype: int64

```python
# how many colors are there
adoption_upd['color'].value_counts()[:10]
#477 different values, high cardinality as well.
```

```python

```


# Announcments

* Start the CP if you haven't already
* We have a course tutor, Brady Moore bradenm3@illinois.edu

```python
%matplotlib inline

import matplotlib
import numpy as np
import matplotlib.pyplot as plt

plt.rcParams["figure.figsize"] = (12, 9)
plt.rcParams["font.size"] = 18
```

## Binary Nuclear Reactions

### Learning Objectives:

- Connect concepts in particle collisions and decay to binary reactions
- Categorize nuclear reactions using standard nomenclature
- Apply conservation of nucleons to binary nuclear reactions
- Formulate Q value equations for binary nuclear reactions
- Apply conservation of energy and linear momentum to scattering
- Apply coulombic threshold
- Apply kinematic threshold
- Determine when coulombic and kinematic thresholds apply or do not

## Recall from Weeks 3 & 4

To acheive these objectives, we need to recall 3 major themes from weeks three and four.

### 1: Compare Exothermic and Endothermic reactions

- In **_exothermic_** or **_exoergic_** reactions, energy is **emitted** ($Q>0$)
- In **_endothermic_** or **_endoergic_** reactions, energy is **absorbed** ($Q<0$)

(credit: BBC)

### 2:  Relate energy and mass  $E=mc^2$

When the masses of reactions change, this is tied to a change in energy from whence we learn the Q value.
This change in mass is equivalent to a change in energy because **$E=mc^2$**

\begin{align}
A + B + \cdots &\rightarrow C + D + \cdots\\
\mbox{(reactants)} &\rightarrow \mbox{(products)}\\
\implies \Delta M &= (\mbox{reactants}) - (\mbox{products})\\
         &= (M_A + M_B + \cdots) - (M_C + M_D + \cdots)\\
\implies \Delta E &= \left[(M_A + M_B + \cdots) - (M_C + M_D + \cdots)\right]c^2\\
\end{align}

### 3: Apply conservation of energy and momentum to scattering collisions

Conservation of total energy and linear momentum can inform Compton scattering reactions. X-rays scattered from electrons had a change in wavelength $\Delta\lambda = \lambda' - \lambda$ proportional to $(1-\cos{\theta_s})$

We used the law of cosines:

\begin{align}
p_e^2 &= p_\lambda^2 + p_{\lambda'}^2 - 2p_\lambda p_{\lambda'}\cos{\theta_s}
\end{align}

And we also used conservation of energy:
\begin{align}
p_\lambda c+m_ec^2 &=  p_{\lambda'}c + mc^2\\
\mbox{where }&\\
m_e&=\mbox{rest mass of the electron}\\
m &= \mbox{relativistic electron mass after scattering}
\end{align}

Combining these with our understanding of photon energy ($E=h\nu=pc$) gives:

\begin{align}
\lambda' - \lambda &= \frac{h}{m_ec}(1-\cos{\theta_s})\\
\implies \frac{1}{E'} - \frac{1}{E} &= \frac{1}{m_ec^2}(1-\cos{\theta_s})\\
\implies E'  &= \left[\frac{1}{E} + \frac{1}{m_ec^2}(1-\cos{\theta_s})\right]^{-1}\\
\end{align}

## More Types of Reactions

Previously we were interested in fundamental particles striking one another (e.g. the electron and proton in Compton scattering) or nuclei emitting such particles (e.g. $\beta^\pm$ decay).

**Today:** We are interested in myriad additional reactants and/or products. In particular, we're interested in:

- neutron absorption and production reactions
- _binary, two-product nuclear reactions_ in which two products emerge with new energies after the collision.

## Reaction Nomenclature

**Transfer Reactions:** Nucleons (1 or 2) are transferred between the projectile and product.

**Scattering reactions:** The projectile and product emerge from a collision with the same identities as when they started, exchanging only kinetic energy.

**Knockout reactions:** The projectile directly interacts with the target nucleus and is re-emitted **along with** nucleons from the target nucleus.

**capture reactions:** The projectile is absorbed, typically exciting the nucleus. The excited nucleus may emit that energy decaying via photon emission.

**nuclear photoeffect:** A photon projectile liberates a nucleon from the target nucleus.

### Think Pair Share : categorize these reactions

One example of each of the above appears below. Use the definitions to categorize them.

- $(n, n)$
- $(n, \gamma)$
- $(n, 2n)$
- $(\gamma, n)$
- $(\alpha, n)$

## Binary, two-product nuclear reactions

**Two initial nuclei collide to form two product nuclei.**

\begin{align}
^{A_1}_{Z_1}X_1 + ^{A_2}_{Z_2}X_2 \longrightarrow ^{A_3}_{Z_3}X_3 + ^{A_4}_{Z_4}X_4
\end{align}

#### Applying Conservation of Neutrons and Protons

The total number of nucleons is always conserved.
If the `______________` force is not involved, we can also apply this conservation separately.

In most binary, two-product nuclear reactions, this is the case, so the number of protons and neutrons are conserved. Thus:

\begin{align}
Z_1 + Z_2 = Z_3 + Z_4\\
A_1 + A_2 = A_3 + A_4
\end{align}

Apply this to the following:

\begin{align}
^{3}_{1}H + ^{16}_{8}O \longrightarrow \left(X\right)^* \longrightarrow  ^{16}_{7}N + ^{A_4}_{Z_4}X_4
\end{align}

### Think Pair Share:

What are :

- $A_4$
- $Z_4$
- $X_4$?

- Bonus: What is $\left(X\right)^*$?

#### Applying conservation of mass and energy.

The Q-value calculation is the same as it has been before.
The Q value represents the `________` in kinetic energy and, equivalently, a `________` in the rest masses.

\begin{align}
Q &= E_y + E_Y − E_x − E_X \\
 &= (m_x + m_X − m_y − m_Y )c^2\\
 &= \left(m\left(^{A_1}_{Z_1}X_1\right) + m\left(^{A_2}_{Z_2}X_2\right) - m\left(^{A_3}_{Z_3}X_3\right) - m\left(^{A_4}_{Z_4}X_4\right)\right)c^2\\
\end{align}

If proton numbers are conserved (true for everything but electron capture or reactions involving the weak force.), we can use the approximation that $m(X) = M(X)$.

\begin{align}
Q &= E_y + E_Y − E_x − E_X \\
  &= (m_x + m_X − m_y − m_Y )c^2\\
  &= (M_x + M_X − M_y − M_Y )c^2\\
   &= \left(M\left(^{A_1}_{Z_1}X_1\right) + M\left(^{A_2}_{Z_2}X_2\right) - M\left(^{A_3}_{Z_3}X_3\right) - M\left(^{A_4}_{Z_4}X_4\right)\right)c^2\\
\end{align}

```python
def q(m_reactants, m_products):
    """Returns Q

    Parameters
    ----------
    m_reactants: list (of doubles)
        the masses of the reactant atoms [amu]
    m_products : list (of doubles)
        the masses of the product atoms [amu]
    """
    amu_to_mev = 931.5 # MeV/amu conversion
    m_difference = sum(m_reactants) - sum(m_products)
    return m_difference*amu_to_mev

# Look up the masses:
h_3_mass = 3.0160492675
o_16_mass = 15.9949146221
he_3_mass = 3.0160293097
n_16_mass = 16.0061014

m_react = [h_3_mass, o_16_mass]
m_prods = [he_3_mass, n_16_mass]

print("Q: ", q(m_react, m_prods))
```

#### Applying conservation of linear momentum

Let's get back to collision kinematics.

First, we'll assume the target nucleus ($X_2$) is initially at rest.

# Kinematic Threshold

Relying on a combination of kinetic energies $E_i$ and corresponding linear momenta:

\begin{align}
p_i = \sqrt{2m_iE_i}
\end{align}

We can determine that some reactions aren't possible without a certain minimum quantity of kinetic energy.

The solution to $E_3$ can become nonphysical if :

- $\cos{\theta_3} < 0$
- $Q < 0$
- $m_4 - m_1 < 0$

## For Exoergic Reactions ($Q>0$)

For $Q>0$ and $m_{4} > m_{1}$, $E_{3} = (a + \sqrt{a^2+b^2})^2$ is the only real, positive, meaningful solution.

The kinetic energy of $E_3$ is, at minimum, the energy arrived at when $p_1 = 0$. Thus:

\begin{align}
E_3 \longrightarrow& \frac{m_4}{m_3 + m_4}Q\\
&\mbox{ when } Q>0, p_1=0
\end{align}

So, no exoergic reactions are restricted by kinetics, as $Q = E_3 + E_4$, for the minimum linear momentum case, which is real and positive.

## For Endoergic Reactions ($Q<0$)
Some $Q<0$ reactions aren't possible without a certain minimum quantity of kinetic energy.

For $Q<0$ and $m_{4} > m_{1}$, some values of $E_{1}$ are too small to carry forward a real, positive solution. That is, the incident projectile must supply a minimum amount of kinetic energy before the reaction can occur. Without this energy, the solution for $E_3$ results in physically meaningless values. This minimum energy can be found from eqn 6.11 in your book and is :

\begin{align}
E_1^{th,k} = -\frac{m_3 + m_4}{m_3 + m_4 - m_1}Q.
\end{align}

One can often simplify this (assuming $m_i >> Q/c^2$ and $m_3 + m_4 - m_1 \simeq m_2$)  :

\begin{align}
E_1^{th,k} \simeq - \left( 1 + \frac{m_1}{m_2} \right)Q.
\end{align}

```python
def kinematic_threshold(m_1, m_3, m_4, Q):
    """Returns the kinematic threshold energy [MeV]

    Parameters
    ----------
    m_1: double
        mass of incident projectile
    m_3: double
        mass of first product
    m_3: double
        mass of second product
    Q : double
        Q-value for the reaction [MeV]
    """
    num = -(m_3 + m_4)*Q
    denom = m_3 + m_4 - m_1
    return num/denom

def kinematic_threshold_simple(m_1, m_2, Q):
    """Returns the coulombic threshold energy [MeV]

    Parameters
    ----------
    m_1: double
        mass of incident projectile
    m_2: double
        mass of target
    Q : double
        Q-value for the reaction [MeV]
    """
    to_return = -(1 + m_1/m_2)*Q
    return to_return
```

# Coulombic Threshold

Coulomb forces repel a projectile if it is:

- a positively charged nucleus
- a proton

The force between the projectile (particle 1) and the target nucleus (particle 2) is :

\begin{align}
&F_C = \frac{Z_1Z_2e^2}{4\pi\epsilon_0r^2}\\
\mbox{where}&&\\
&\epsilon_0 = \mbox{the permittivity of free space.}
\end{align}

### Think pair share:
What are the other terms in the above equation:

- $Z_1$ ?
- $Z_2$ ?
- $e$ ?
- $r$ ?

By evaluating the work function for approach to the nucleus with a coulomb barrier, we can establish that the coulombic threshold energy (in MeV) is :

\begin{align}
E_1^{th,C} \simeq 1.20 \frac{Z_1Z_2}{A_1^{1/3}+A_2^{1/3}}
\end{align}

```python
def colombic_threshold(z_1, z_2, a_1, a_2):
    """Returns the coulombic threshold energy [MeV]

    Parameters
    ----------
    z_1: int
        proton number of incident projectile
    z_2: int
        proton number of target
    a_1 : int or double
        mass number of the incident projectile [amu]
    a_2 : int or double
        mass number of the target [amu]
    """
    num = 1.20*z_1*z_2
    denom = pow(a_1, 1/3) + pow(a_2, 1/3)
    return num/denom
```

### Think Pair Share

Which thresholds apply to the below situations:

- A chargeless incident particle, reaction $Q>0$
- A chargeless incident particle, reaction $Q<0$
- A positively charged incident particle, reaction $Q>0$
- A positively charged incident particle, reaction $Q<0$

## Overall threshold

For the case where both thresholds apply, the minimum energy for the reaction to occur is the highest of the two thresholds.

\begin{align}
\min{\left(E_1^{th}\right)}	= \max{\left(E^{th,C}_1,E_1^{th,k}\right)}.
\end{align}

## Example

Take the (p, n) reaction from $^{9}Be\longrightarrow^{9}B$. We will need to calculate:

- The Q value
- The kinematic threshold (if it applies)
- The coulombic threshold (if it applies)
- Determine which one is higher

```python
# Q value
# Look up the masses:
be_9_mass = 9.0121821
b_9_mass = 9.0133288
n_mass = 1.0086649158849
p_mass = 1.007825032 # hydrogen nucleus!

m_react = [be_9_mass, p_mass]
m_prods = [b_9_mass, n_mass]

q_example = q(m_react, m_prods)
print("Q: ", q_example)
```

```python
# Kinematic Threshold
# Which particles were which again?
m_1 = p_mass
m_2 = be_9_mass
m_3 = n_mass
m_4 = b_9_mass

# Calculate using both regular and simpler methods
E_k_th = kinematic_threshold(m_1, m_3, m_4, q_example)
E_k_th_simple = kinematic_threshold_simple(m_1, m_2, q_example)
print("E_k_th: ", E_k_th)
print("E_k_th (simplified): ", E_k_th_simple)
```

```python
# Coulombic Threshold
# Need some charge info and mass numbers
z_1 = 1 # proton
z_2 = 4 # Be
a_1 = 1 # proton
a_2 = 9 # Be

E_c_th = colombic_threshold(z_1, z_2, a_1, a_2)

print("E_c_th: ", E_c_th)
```

```python
## Which one is higher?

print("Total threshold: ", max(E_c_th, E_k_th))
```

# Applications: Neutron Detection
Neutron's don't tend to directly ionize matter as they pass through. However, they can instigate nuclear reactions which produce charged products. These products, in turn, can be detected due to the ionization they create. The scheme for a Boron Trifluoride detector is below (hosted at https://www.orau.org/ptp/collection/proportional%20counters/bf3info.htm).

The wall effect results in the following spectrum (approximately):

In (n,p) reactions, for example, variation in emission angle of particle 3 can be used to determine the energy of the original incident neutron.

# Applications: Neutron Production
Specific neutron energies can be targetted by collecting them at a certain angle away from the production collision.

The accelerator and spallation target at LANSCE and other spallation experiments rely on this fact.

## Two energies

In (p,n) reactions, for example, certain proton energies may result in more than one neutron energy observed at a single angle. How?

Recall the equation (Shultis and Faw 6.11):

\begin{align}
\sqrt{E_y}=&\sqrt{\frac{m_xm_yE_x}{(m_y + m_Y)^2}}\cos\theta_y \\
&\pm \sqrt{\frac{m_xm_yE_x}{(m_y + m_Y)^2}\cos^2\theta_y + \left[\frac{m_Y-m_x}{(m_y + m_Y)}E_x + \frac{m_YQ}{(m_y + m_Y)}\right]}
\end{align}

Dr. Munk prefers this notation:
\begin{align}
\sqrt{E_3}=&\sqrt{\frac{m_1m_3E_1}{(m_3 + m_4)^2}}\cos\theta_3 \\
&\pm \sqrt{\frac{m_1m_3E_1}{(m_3 + m_4)^2}\cos^2\theta_3 + \left[\frac{m_4-m_1}{(m_3 + m_4)}E_1 + \frac{m_4Q}{(m_3 + m_4)}\right]}
\end{align}

## Heavy Particle scattering from an electron

Much like the Compton reaction we saw between photons and electrons, we can see a similar reaction with heavy particles. Occaisionally, a heavy particle (e.g. a small nucleus, like an $\alpha$ particle) strikes the orbital electrons in atoms of a medium.

Thus: particles 2 and 3 are the electron. So:

\begin{align}
m_2 &= m_3 = m_e = \mbox{(the electron mass)}\\
E_3 &= E_e = \mbox{(the recoil electron energy)}\\
m_1 &= m_4 = \mbox{(the mass of the heavy particle)}\\
E_1 &= E_4 = \mbox{(the kinetic energy of the incident heavy particle)}
\end{align}

For this scattering process, there is no change in the rest masses of the reactants, so Q = 0.

We can use the Shutlis and Faw 6.11 equation above to arrive at:

\begin{align}
\sqrt{E_e}=& \frac{2}{m_4 + m_e}\sqrt{m_4m_eE_4}\cos{\theta_e}
\end{align}

We can approximate that $m_4 >> m_e$ such that the electron recoil energy becomes:

\begin{align}
\implies E_e =& 4\frac{m_e}{m_4}E_4\cos^2{\theta_e}
\end{align}

## Think Pair Share
What angle, $\theta_e$, corresponds to the maximimum loss of kinetic energy by the incident heavy particle?

At $\theta_e=0$, we find that:

\begin{align}
(E_e)_{max} = 4\frac{m_e}{m_4}E_4
\end{align}

```python
import math
def recoil_energy(m_4, e_4, theta_e):
    m_e = 0.0005486 # amu
    num = 4*m_e*e_4*pow(math.cos(theta_e), 2)
    return num/m_4

```

```python
th = [math.radians(-90),
      math.radians(-75),
      math.radians(-60),
      math.radians(-45),
      math.radians(-30),
      math.radians(-15),
      math.radians(0),
      math.radians(15),
      math.radians(30),
      math.radians(45),
      math.radians(60),
      math.radians(75),
      math.radians(90)]

m_4 = 4.003 # alpha particle

to_plot_4 = np.arange(0.,len(th))
to_plot_10 = np.arange(0.,len(th))

for k, v in enumerate(th):
    to_plot_4[k] = (recoil_energy(m_4, 4, v))
    to_plot_10[k] = (recoil_energy(m_4, 10, v))

plt.plot(th, to_plot_4, label="$4MeV$")
plt.plot(th, to_plot_10, label="$10MeV$")

plt.ylabel("Electron Recoil Energy ($MeV$)")
plt.xlabel("Angle (radians)")
plt.legend(loc=2)
```

```python

th = 0
m_4 = 4.003 # alpha particle
e_4 = 4 # MeV

print("Max (4MeV alpha): ", recoil_energy(m_4, e_4, th))
```

## Neutron Scattering

### Neutron interactions with matter.

\begin{align}
^1_0n + {^a_z}X \longrightarrow
\begin{cases}
^1_0n + {^a_z}X  & \mbox{Elastic Scattering}\\
^1_0n + \left({^a_z}X\right)^*  & \mbox{Inlastic Scattering}
\end{cases}
\end{align}

Using the ubiquitous equation 6.11 for a neutron scatter:

\begin{align}
\sqrt{E_3}=&\sqrt{\frac{m_1m_3E_1}{(m_3 + m_4)^2}}\cos\theta_3 \\
&\pm \sqrt{\frac{m_1m_3E_1}{(m_3 + m_4)^2}\cos^2\theta_3 + \left[\frac{m_4-m_1}{(m_3 + m_4)}E_1 + \frac{m_4Q}{(m_3 + m_4)}\right]}\\
\end{align}

We can define our particles as a neutron hitting a nucleus and changing in its energy.

\begin{align}
m_1 = m_3 = m_n\\
E_1 = E_n\\
E_3 = E_n'\\
\end{align}

Such that:

\begin{align}
\sqrt{E_n'} =&\sqrt{\frac{m_nm_nE_n}{(m_n + m_4)^2}}\cos\theta_s \\
&\pm \sqrt{\frac{m_nm_nE_n}{(m_n + m_4)^2}\cos^2\theta_s + \left[\frac{m_4-m_n}{(m_n + m_4)}E_n + \frac{m_4Q}{(m_n + m_4)}\right]}
\end{align}

We can also agree that $m_2=m_4$, which is some nucleus with a mass that is approximately the same at the beginning and end of the scatter (approximate if the scattering is inelastic) . This gives, with some rearrangement:

\begin{align}
\sqrt{E_n'} &= \frac{1}{m_4 + m_n}\times\\ &\left[\sqrt{m_n^2E_n}\cos{\theta_s} \pm \sqrt{E(m_4^2 + m_n^2\cos^2{\theta_s} − m_n^2) + m_4 ( m_4 + m_n ) Q }\right]
\end{align}

And, for elastic scattering ($Q=0$):

\begin{align}
E' = \frac{1}{(A+1)^2}\left[\sqrt{E}\cos{\theta_s} + \sqrt{E(A^2 - 1 + \cos{\theta_s}^2)}\right]^2
\end{align}

```python
def scattered_neutron_energy(A, E, th):
    """Returns the energy of a scattered neutron [MeV]
    Parameters
    ----------
    A: int or double
        mass number of medium
    E: double
        kinetic energy of the incident neutron [MeV]
    th : double
        scattering angle, in degrees
    """
    cos_th = math.cos(math.radians(th))
    term1 = 1/((A+1)**2)
    term2 = math.sqrt(E)*cos_th
    term3 = math.sqrt(E*(A**2 - 1 + cos_th**2))
    return term1*((term2 + term3)**2)
```

```python
th = [math.radians(-90),
      math.radians(-75),
      math.radians(-60),
      math.radians(-45),
      math.radians(-30),
      math.radians(-15),
      math.radians(0),
      math.radians(15),
      math.radians(30),
      math.radians(45),
      math.radians(60),
      math.radians(75),
      math.radians(90)]

e_initial = 2.0 # 2 MeV is special
a_light = 4.003 # alpha particle
a_heavy = 235.0 # uranium atom

to_plot_light = np.arange(0.,len(th))
to_plot_heavy = np.arange(0.,len(th))

for k, v in enumerate(th):
    to_plot_light[k] = (scattered_neutron_energy(a_light, e_initial, v))
    to_plot_heavy[k] = (scattered_neutron_energy(a_heavy, e_initial, v))

plt.plot(th, to_plot_light, label="light")
plt.plot(th, to_plot_heavy, label="heavy")

plt.ylabel("Scattered Neutron Energy ($MeV$)")
plt.xlabel("Angle (radians)")
plt.legend(loc=2)
```

## Average Energy Loss

For elastic scattering (Q = 0), we see the minimum and maxium energies occur at the maximum and minimum angles.

\begin{align}
E'_{max} &= E'(\theta_{s,min})\\
         &= E'(\theta_{s}=0)\\
         &= E\\
E'_{min} &= E'(\theta_{s,max})\\
         &= E'(\theta_{s}=\pi)\\
         &= \frac{(A-1)^2}{(A+1)^2} E\\
         &\equiv \alpha E\\
\end{align}

For isotropic scattering, we can find the average loss:

\begin{align}
(\Delta E)_{av} &\equiv E - E'_{av}\\
&= E− 1(E+\alpha E)\\
& = 1(1- \alpha)E
\end{align}

```python
def alpha(a):
    """Returns the average energy loss of a
    scattered neutron [MeV]
    Parameters
    ----------
    A: int or double
        mass number of medium
    """
    num = (a-1)**2
    denom = (a+1)**2
    return num/denom

def average_energy_loss(A, E):
    """Returns the average energy loss of a scattered neutron [MeV]
    Parameters
    ----------
    A: int or double
        mass number of medium
    E: double
        kinetic energy of the incident neutron [MeV]
    """
    return 1*(1-alpha(A))*E
```

```python
e_initial = np.arange(0, 2, 0.001)

to_plot_light = np.arange(0.,len(e_initial))
to_plot_heavy = np.arange(0.,len(e_initial))

for k, v in enumerate(e_initial):
    to_plot_light[k] = (average_energy_loss(a_light, v))
    to_plot_heavy[k] = (average_energy_loss(a_heavy, v))

plt.plot(e_initial, to_plot_light, label="light atom")
plt.plot(e_initial, to_plot_heavy, label="heavy atom")

plt.ylabel("Average Neutron Energy Loss ($MeV$)")
plt.xlabel("Initial neutron energy ($MeV$)")
plt.legend(loc=2)
```

## Logarithmic Energy Loss

It turns out, on a logarithmic energy scale, a neutron loses the same amount of logarithmic energy per elastic scatter, regardless of its initial energy. So, this is a helpful term, particularly since neutron energies can range by many orders of magnitude. So, we often use 'logarithmic energy loss' when discussing this downscattering. This is also called "lethargy".

\begin{align}
\left(\ln{(E)} - \ln{(E')}\right)_{av} & = \overline{\ln{\left(\frac{E}{E'}\right)}} \\
&= 1 + \frac{\alpha}{1-\alpha}\\
&= \xi\\
&= \mbox{average logarithmic energy loss per elastic scatter}\\
&= \mbox{lethargy}
\end{align}

```python
def lethargy(a):
    """Returns the average logarithmic energy
    loss per elastic scatter
    Parameters
    ----------
    A: int or double
        mass number of medium
    """
    return 1.0 + alpha(a)/(1-alpha(a))
```

```python
a = np.arange(1, 240)
plt.plot([lethargy(i) for i in a])
plt.ylabel("$\\xi$")
plt.xlabel("A($amu$)")

```

## Thermal Neutrons

1. a fast neutron slows down
2. may eventually come into thermal equilibrium with the medium
3. thermal motion of atoms in medium are in Maxwellian distribution
4. neutron may gain kinetic energy upon scattering from a rapidly moving nucleus
5. neutron may lose energy upon scattering from a slowly moving nucleus.

At room temperature, 293 K:
- the most probable kinetic energy of thermal neutrons is 0.025 eV
- 0.025 eV corresponds to a neutron speed of about 2200 m/s.

## Epithermal

Neutrons that are faster than thermal neutrons, but aren't quite "fast" are called _epithermal_. ($0.2eV < E_{epi} < 1 MeV$)

## Fast

$> 1MeV$

## Neutron Capture

- Free neutrons will eventually be absorbed by a nucleus (or escape the domain of interest)
- Neutron capture leaves the nucleus excited
- Actually, very excited (Recall: what is a typical binding energy per nucleon?)
- When it's released as a $\gamma$ that energy can be very hazardous

Neutron slowing down can help us to reduce very high energy $\gamma$ emissions.

## Fission Reactions

Some nuclei spontaneously fission (e.g. $^{252}Cf$). However, this isn't common.

\begin{align}
^1_0n + ^{235}_{92}U \longrightarrow \left( ^{236}_{92}U \right)^*
\begin{cases}
^{235}_{92}U + ^1_0n & \mbox{Elastic Scattering}\\
^{235}_{92}U + ^1_0n' +  \gamma & \mbox{Inelastic Scattering}\\
^{236}_{92}U + \gamma & \mbox{Radiative Capture}\\
^{A_H}_{Z_H}X_H + ^{A_L}_{Z_L}X_L + ^1_0n  + \cdots & \mbox{Fission}
\end{cases}
\end{align}

# Announcements

* I am modifying the schedule a bit. On Friday I will introduce different reactor types and then on Monday we are going to have a guest lecture on the nuclear fuel cycle by **Amanda Bachmann**.
* The homework I assign on Friday will be due the Monday after spring break
* If you wanted an adjustment on your midterm exam, you **need to email me** with the details so I can record it properly.
* Exam 2 moved to 4/4. Syllabus updated on github.

### Recall: Cross sections

The likelihood of each of these scattering events is captured by cross sections.

- $\sigma_x = $ microscopic cross section $[cm^2]$
- $\Sigma_x = $ macroscopic cross section $[1/length]$
- $\Sigma_x = N\sigma_x $
- $N = $ number density of target atoms $[\#/volume]$

### Cross sections are in units of area. Explain this to your neighbor.

### What energy neutron do we prefer for fission in $^{235}U$?

Nuclei that undergo neutron induced fission can be categorized into three types:

- fissile: can fission with a slow neutron ($^{235}U$, $^{233}U$, $^{239}Pu$)
- fissionable: require high energy (>1MeV) neutron ($^{238}U$, $^{240}Pu$)
- fertile: can be converted into fissile or fissionable nuclide (breeding reactions)

Key breeding reactions are :

\begin{align}
{^{232}_{90}}Th + ^1_0n \longrightarrow {^{233}_{90}}Th  \overset{\beta^-}{\longrightarrow}  {^{233}_{91}}Pa   \overset{\beta^-}{\longrightarrow}  {^{233}_{92}}U\\
{^{238}_{92}}U + ^1_0n \longrightarrow {^{239}_{92}}U  \overset{\beta^-}{\longrightarrow}  {^{239}_{93}}Np   \overset{\beta^-}{\longrightarrow}  {^{239}_{94}}Pu\\
\end{align}

## The fission process

\begin{align}
^1_0n + ^{235}_{92}U \longrightarrow \left( ^{236}_{92}U \right)^* \longrightarrow  X_H + X_L + \nu_p\left(^1_0n\right) + \gamma_p
\end{align}

Conserving neutrons and protons:

\begin{align}
A_L + A_H + \nu_p &= 236\\
N_L + N_H + \nu_p &= 144\\
Z_L + Z_H &= 92\\
\end{align}

## Fission Product Decay

The fission fragments end up very neutron rich.

### Think Pair Share
Recall the chart of the nuclides. How will these fission products likely decay?

### Fission Spectrum

$\chi(E)$ is an empirical probability density function describing the energies of prompt fission neutrons.

\begin{align}
\chi (E) &= 0.453e^{-1.036E}\sinh\left(\sqrt{2.29E}\right)\\
\end{align}

```python
import numpy as np
import math
def chi(energy):
    return 0.453*np.exp(-1.036*energy)*np.sinh(np.sqrt(2.29*energy))

energies = np.arange(0.0,10.0, 0.1)

plt.plot(energies, chi(energies))
plt.title(r'Prompt Neutron Energy Distribution $\chi(E)$')
plt.xlabel("Prompt Neutron Energy [MeV]")
plt.ylabel("probability")
```

#### Questions about this plot:

- What is the most likely prompt neutron energy?
- Can you write an equation for the average neutron energy?
- Can you write an equation for the average neutron energy?

```python
print(max([chi(e) for e in energies]), chi(0.7))
```

#### Expectation Value

Recall that the average energy will be the expectation value of the probability density function.

\begin{align}
<E> &= \int E\chi(E)dE\\
&= E \chi(E)
\end{align}

```python
plt.plot(energies, [chi(e)*e for e in energies])
```

## Prompt and Delayed neutrons

- Most of the neutrons in fission are emitted within $10^{-14}s$.
  - **prompt** neutrons
  - $\nu_p$
- Some, ($<1\%$) are produced by delayed decay of fission products.
  - **delayed** neutrons
  - $\nu_d$

We define the delayed neutron fraction as :

\begin{align}
\beta \equiv \frac{\nu_d}{\nu_d + \nu_p}
\end{align}

## Energy from fission

### Reaction Rates

- The microscopic cross section is just the likelihood of the event per unit area.
- The macroscopic cross section is just the likelihood of the event per unit area of a certain density of target isotopes.
- The reaction rate is the macroscopic cross section times the flux of incident neutrons.

\begin{align}
R_{i,j}(\vec{r}) &= N_j(\vec{r})\int dE \phi(\vec{r},E)\sigma_{i,j}(E)\\
R_{i,j}(\vec{r}) &= \mbox{reactions of type i involving isotope j } [reactions/cm^3s]\\
N_j(\vec{r}) &= \mbox{number of nuclei participating in the reactions } [\#/cm^3]\\
E &= \mbox{energy} [MeV]\\
\phi(\vec{r},E)&= \mbox{flux of neutrons with energy E at position i } [\#/cm^2s]\\
\sigma_{i,j}(E)&= \mbox{cross section } [cm^2]\\
\end{align}

This can be written more simply as $R_x = \Sigma_x I N$, where I is intensity of the neutron flux.

### Source term

The source of neutrons in a reactor are the neutrons from fission.

\begin{align}
s &=\nu \Sigma_f \phi
\end{align}

where

\begin{align}
s &= \mbox{neutrons available for next generation of fissions}\\
\nu &= \mbox{the number born per fission}\\
\Sigma_f &= \mbox{the number of fissions in the material}\\
\phi &= \mbox{initial neutron flux}
\end{align}

This can also be written as:

\begin{align}
s &= \nu\Sigma_f\phi\\
  &= \nu\frac{\Sigma_f}{\Sigma_{a,fuel}}\frac{\Sigma_{a,fuel}}{\Sigma_a}{\Sigma_a} \phi\\
  &= \eta f {\Sigma_a} \phi\\
\eta &= \frac{\nu\Sigma_f}{\Sigma_{a,fuel}} \\
      &= \mbox{number of neutrons produced per neutron absorbed by the fuel, "neutron reproduction factor"}\\
f &= \frac{\Sigma_{a,fuel}}{\Sigma_a} \\
   &= \mbox{number of neutrons absorbed in the fuel per neutron absorbed anywhere, "fuel utilization factor"}\\
\end{align}

This absorption and flux term at the end seeks to capture the fact that some of the neutrons escape. However, if we assume an infinite reactor, we know that all the neutrons are eventually absorbed in either the fuel or the coolant, so we can normalize by $\Sigma_a\phi$ and therefore:

\begin{align}
k_\infty &= \frac{\eta f \Sigma_a\phi}{\Sigma_a \phi}\\
&= \eta f
\end{align}


```python
# %load /Users/facai/Study/book_notes/preconfig.py
%matplotlib inline

import matplotlib.pyplot as plt
import seaborn as sns

import numpy as np

import pandas as pd

from IPython.display import Image
```

Chapter 6 Temporal-Difference Learning
=====================

DP, TD, and Monte Carlo methods all use some variation of generalized policy iteration: primarily differences in their approaches to the prediction problem.

### 6.1 TD Prediction

constant-$\alpha$ MC: $V(S_t) \gets V(S_t) + \alpha \underbrace{\left [ G_t - V(S_t) \right ]}_{= \sum_{k=t}^{T-1} \gamma^{k-1} \theta_k}$

\begin{align}
    v_\pi(s) &\doteq \mathbb{E}_\pi [ G_t \mid S_s = s] \qquad \text{Monte Carlo} \\
             &= \mathbb{E}_\pi [ R_{t+1} + \gamma \color{blue}{v_\pi(S_{t+1})} \mid S_t = s ] \quad \text{DP}
\end{align}

one-step TD, or TD(0): $V(S_t) \gets V(S_t) + \alpha \left [ \underbrace{R_{t+1} + \gamma \color{blue}{V(S_{t+1})} - V(S_t)}_{\text{TD error: } \theta_t} \right ]$

TD: samples the expected values and uses the current estimate $V$ instead of the true $v_\pi$.

```python
Image('./res/fig6_1.png')
```

```python
Image('./res/TD_0.png')
```

### 6.2 Advantages of TD Prediction Methods

TD: they learn a guess from a guess - they boostrap.

+ advantage:
  1. over DP: TD do not require a model of the environment, of its reward and next-state probability distributions.
  2. over Monte Carlo: TD are naturally implemented in an online, fully incremental fashion.

TD: guarantee convergence.

In practice, TD methods have usually been found that converge faster than constant-$\alpha$ MC methods on stochastic tasks.

### 6.3 Optimality of TD(0)

batch updating: updates are made only after processing each complete batch of training data until the value function converges.

+ Batch Monte Carlo methods: always find the estimates that minimize mean-squared error on the training set.
+ Batch TD(0): always find the estimates that would be exactly correct for the maximum-likelihood model of the Markov process.

### 6.4 Sarsa: On-policy TD Control

$Q(S_t, A_t) \gets Q(S_t, A_t) + \alpha \left [ R_{t+1} + \gamma Q(S_{t+1}, A_{t+1}) - Q(S_t,, A_t) \right ]$

```python
Image('./res/sarsa.png')
```

### 6.5 Q-learning: Off-policy TD Control

$Q(S_t, A_t) \gets Q(S_t, A_t) + \alpha \left [ R_{t+1} + \gamma \color{blue}{\max_a Q(S_{t+1}, a)} - Q(S_t,, A_t) \right ]$

```python
Image('./res/q_learn_off_policy.png')
```

### 6.6 Expected Sarsa

use expeteced value, how likely each action is under the current policy.

\begin{align}
    Q(S_t, A_t) & \gets Q(S_t, A_t) + \alpha \left [ R_{t+1} + \gamma \color{blue}{\mathbb{E}[Q(S_{t+1}, A_{t+1}) \mid S_{t+1}]} - Q(S_t,, A_t) \right ] \\
                & \gets Q(S_t, A_t) + \alpha \left [ R_{t+1} + \gamma \color{blue}{\sum_a \pi(a \mid S_{t+1}) Q(S_{t+1}, a)} - Q(S_t,, A_t) \right ]
\end{align}

+ con: additional computational cost.
+ pro: eliminate the variance due to the random seleciton of $A_{t+1}$.

### 6.7 Maximization Bias and Double Learning

maximization bias:
+ a maximum over estimated values => an estimate of the maximum value => significant positive bias.

root of problem: using the same samples (plays) both to determine the maximizing action and to estimate its value. => divide the plays in two sets ($Q_1, Q_2$) and use them to learn two indepedent estimates. (*double learning*)

$Q_1(S_t, A_t) \gets Q_1(S_t, A_t) + \alpha \left [ R_{t+1} + \gamma Q_2 \left( S_{t+1}, \operatorname{argmax}_a Q_1(S_{t+1}, a) \right) - Q_1(S_t,, A_t) \right ]$

```python
Image('./res/double_learn.png')
```

### 6.8 Games, Afterstates, and Other Special Cases

```python

```


# Textual entailment classifier using an MLP plus attention

In textual entailment,
the input is 2 sentences (premise and hypothesis), and the output
is a label, specifying if P entails H, P contradicts H, or neither.
(This is also called "natural language inference".)
We use attention to align hypothesis to premise and vice versa,
then compare the aligned words to estimate similarity between the sentences, and pass the weighted similarities to an MLP.

Based on sec 15.5 of http://d2l.ai/chapter_natural-language-processing-applications/natural-language-inference-attention.html

```python
!pip install -q flax
```

         |████████████████████████████████| 184 kB 12.2 MB/s
         |████████████████████████████████| 136 kB 50.2 MB/s
         |████████████████████████████████| 72 kB 709 kB/s

```python
import jax
import jax.numpy as jnp                # JAX NumPy

from flax import linen as nn           # The Linen API
from flax.training import train_state  # Useful dataclass to keep train state
import torch
from torch.utils import data           # For data

import numpy as np                     # Ordinary NumPy
import optax                           # Optimizers

import collections
import re
import os
import requests
import zipfile
import tarfile
import hashlib
import time
import functools
from typing import Any, Callable, Sequence, Tuple

import matplotlib.pyplot as plt
import pandas as pd
import math
from IPython import display

rng = jax.random.PRNGKey(0)
!mkdir figures # for saving plots
ModuleDef = Any
```

# Data

We use SNLI (Stanford Natural Language Inference) dataset described in sec 15.4 of http://d2l.ai/chapter_natural-language-processing-applications/natural-language-inference-and-dataset.html.

```python
# Required functions for downloading data

def download(name, cache_dir=os.path.join('..', 'data')):
    """Download a file inserted into DATA_HUB, return the local filename."""
    assert name in DATA_HUB, f"{name} does not exist in {DATA_HUB}."
    url, sha1_hash = DATA_HUB[name]
    os.makedirs(cache_dir, exist_ok=True)
    fname = os.path.join(cache_dir, url.split('/')[-1])
    if os.path.exists(fname):
        sha1 = hashlib.sha1()
        with open(fname, 'rb') as f:
            while True:
                data = f.read(1048576)
                if not data:
                    break
                sha1.update(data)
        if sha1.hexdigest() == sha1_hash:
            return fname  # Hit cache
    print(f'Downloading {fname} from {url}...')
    r = requests.get(url, stream=True, verify=True)
    with open(fname, 'wb') as f:
        f.write(r.content)
    return fname

def download_extract(name, folder=None):
    """Download and extract a zip/tar file."""
    fname = download(name)
    base_dir = os.path.dirname(fname)
    data_dir, ext = os.path.splitext(fname)
    if ext == '.zip':
        fp = zipfile.ZipFile(fname, 'r')
    elif ext in ('.tar', '.gz'):
        fp = tarfile.open(fname, 'r')
    else:
        assert False, 'Only zip/tar files can be extracted.'
    fp.extractall(base_dir)
    return os.path.join(base_dir, folder) if folder else data_dir
```

```python
DATA_HUB = dict()
DATA_HUB['SNLI'] = ('https://nlp.stanford.edu/projects/snli/snli_1.0.zip',
                        '9fcde07509c7e87ec61c640c1b2753d9041758e4')

data_dir = download_extract('SNLI')
```

    Downloading ../data/snli_1.0.zip from https://nlp.stanford.edu/projects/snli/snli_1.0.zip...

```python
def read_snli(data_dir, is_train):
    """Read the SNLI dataset into premises, hypotheses, and labels."""
    def extract_text(s):
        # Remove information that will not be used by us
        s = re.sub('\\(', '', s)
        s = re.sub('\\)', '', s)
        # Substitute two or more consecutive whitespace with space
        s = re.sub('\\s{2,}', ' ', s)
        return s.strip()

    label_set = {'entailment': 0, 'contradiction': 1, 'neutral': 2}
    file_name = os.path.join(
        data_dir, 'snli_1.0_train.txt' if is_train else 'snli_1.0_test.txt')
    with open(file_name, 'r') as f:
        rows = [row.split('\t') for row in f.readlines()[1:]]
    premises = [extract_text(row[1]) for row in rows if row[0] in label_set]
    hypotheses = [extract_text(row[2]) for row in rows if row[0] in label_set]
    labels = [label_set[row[0]] for row in rows if row[0] in label_set]
    return premises, hypotheses, labels
```

Show first 3 training examples and their labels (“0”, “1”, and “2” correspond to “entailment”, “contradiction”, and “neutral”, respectively ).

```python
train_data = read_snli(data_dir, is_train=True)
for x0, x1, y in zip(train_data[0][:3], train_data[1][:3], train_data[2][:3]):
    print('premise:', x0)
    print('hypothesis:', x1)
    print('label:', y)
```

    premise: A person on a horse jumps over a broken down airplane .
    hypothesis: A person is training his horse for a competition .
    label: 2
    premise: A person on a horse jumps over a broken down airplane .
    hypothesis: A person is at a diner , ordering an omelette .
    label: 1
    premise: A person on a horse jumps over a broken down airplane .
    hypothesis: A person is outdoors , on a horse .
    label: 0

```python
test_data = read_snli(data_dir, is_train=False)
for data in [train_data, test_data]:
    print([[row for row in data[2]].count(i) for i in range(3)])
```

    [183416, 183187, 182764]
    [3368, 3237, 3219]

```python
def tokenize(lines, token='word'):
    """Split text lines into word or character tokens."""
    if token == 'word':
        return [line.split() for line in lines]
    elif token == 'char':
        return [list(line) for line in lines]
    else:
        print('ERROR: unknown token type: ' + token)

class Vocab:
    """Vocabulary for text."""
    def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
        if tokens is None:
            tokens = []
        if reserved_tokens is None:
            reserved_tokens = []
        # Sort according to frequencies
        counter = count_corpus(tokens)
        self.token_freqs = sorted(counter.items(), key=lambda x: x[1],
                                  reverse=True)
        # The index for the unknown token is 0
        self.unk, uniq_tokens = 0, ['<unk>'] + reserved_tokens
        uniq_tokens += [
            token for token, freq in self.token_freqs
            if freq >= min_freq and token not in uniq_tokens]
        self.idx_to_token, self.token_to_idx = [], dict()
        for token in uniq_tokens:
            self.idx_to_token.append(token)
            self.token_to_idx[token] = len(self.idx_to_token) - 1

    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):
        if not isinstance(tokens, (list, tuple)):
            return self.token_to_idx.get(tokens, self.unk)
        return [self.__getitem__(token) for token in tokens]

    def to_tokens(self, indices):
        if not isinstance(indices, (list, tuple)):
            return self.idx_to_token[indices]
        return [self.idx_to_token[index] for index in indices]

def count_corpus(tokens):
    """Count token frequencies."""
    # Here `tokens` is a 1D list or 2D list
    if len(tokens) == 0 or isinstance(tokens[0], list):
        # Flatten a list of token lists into a list of tokens
        tokens = [token for line in tokens for token in line]
    return collections.Counter(tokens)

```

```python
class SNLIDataset(torch.utils.data.Dataset):
    """A customized dataset to load the SNLI dataset."""
    def __init__(self, dataset, num_steps, vocab=None):
        self.num_steps = num_steps
        all_premise_tokens = tokenize(dataset[0])
        all_hypothesis_tokens = tokenize(dataset[1])
        if vocab is None:
            self.vocab = Vocab(all_premise_tokens + all_hypothesis_tokens,
                                   min_freq=5, reserved_tokens=['<pad>'])
        else:
            self.vocab = vocab
        self.premises = self._pad(all_premise_tokens)
        self.hypotheses = self._pad(all_hypothesis_tokens)
        self.labels = torch.tensor(dataset[2])
        print('read ' + str(len(self.premises)) + ' examples')

    def _pad(self, lines):
        return torch.tensor([
            truncate_pad(self.vocab[line], self.num_steps,
                             self.vocab['<pad>']) for line in lines])

    def __getitem__(self, idx):
        return (self.premises[idx], self.hypotheses[idx]), self.labels[idx]

    def __len__(self):
        return len(self.premises)
```

```python
def load_data_snli(batch_size, num_steps=50):
    """Download the SNLI dataset and return data iterators and vocabulary."""
    num_workers = 2
    data_dir = download_extract('SNLI')
    train_data = read_snli(data_dir, True)
    test_data = read_snli(data_dir, False)
    train_set = SNLIDataset(train_data, num_steps)
    test_set = SNLIDataset(test_data, num_steps, train_set.vocab)
    train_iter = torch.utils.data.DataLoader(train_set, batch_size,
                                             shuffle=True,
                                             num_workers=num_workers)
    test_iter = torch.utils.data.DataLoader(test_set, batch_size,
                                            shuffle=False,
                                            num_workers=num_workers)
    return train_iter, test_iter, train_set.vocab

def truncate_pad(line, num_steps, padding_token):
    """Truncate or pad sequences."""
    if len(line) > num_steps:
        return line[:num_steps]  # Truncate
    return line + [padding_token] * (num_steps - len(line))
```

```python
train_iter, test_iter, vocab = load_data_snli(128, 50)
len(vocab)
```

    read 549367 examples
    read 9824 examples

    18678

# Model

The model is described in the book. Below we just give the code.

## Attending

We define attention weights
$$
e_{ij} = f(a_i)^T f(b_j)
$$
where $a_i \in R^E$ is the embedding of the $i$'th token from the premise,
$b_j \in R^E$ is the embedding of the $j$'th token from the hypothesis,
and $f: R^E \rightarrow R^H$ is an MLP that maps from the embedding space to another hidden space.

```python
class mlp(nn.Module):
    num_hiddens: int
    flatten: bool

    @nn.compact
    def __call__(self, X, train=True):
        X = nn.Dropout(rate=0.2, deterministic=not train)(X)
        X = nn.Dense(self.num_hiddens)(X)
        X = nn.relu(X)
        if self.flatten:
            X = X.reshape((X.shape[0], -1))  # flatten
        X = nn.Dropout(rate=0.2, deterministic=not train)(X)
        X = nn.Dense(self.num_hiddens)(X)
        X = nn.relu(X)
        if self.flatten:
            X = X.reshape((X.shape[0], -1))  # flatten
        return X
```

```python
class Attend(nn.Module):
    num_hiddens: int

    def setup(self):
        self.f = mlp(self.num_hiddens, False)

    @nn.compact
    def __call__(self, A, B, train=True):
        # Shape of `A`/`B`: (`batch_size`, no. of words in sequence A/B,
        # `embed_size`)
        # Shape of `f_A`/`f_B`: (`batch_size`, no. of words in sequence A/B,
        # `num_hiddens`)
        f_A = self.f(A, train)
        f_B = self.f(B, train)
        # Shape of `e`: (`batch_size`, no. of words in sequence A,
        # no. of words in sequence B)
        e = f_A@(f_B.transpose((0, 2, 1)))
        # Shape of `beta`: (`batch_size`, no. of words in sequence A,
        # `embed_size`), where sequence B is softly aligned with each word
        # (axis 1 of `beta`) in sequence A
        beta = nn.softmax(e, axis=-1)@B
        # Shape of `alpha`: (`batch_size`, no. of words in sequence B,
        # `embed_size`), where sequence A is softly aligned with each word
        # (axis 1 of `alpha`) in sequence B
        alpha = nn.softmax(e.transpose((0, 2, 1)) , axis=-1)@A
        return beta, alpha
```

## Comparing

We concatenate word $i$ in A, $a_i$, with its "soft counterpart" in B, $\beta_i$, and vice versa, and then pass this through another MLP $g$
to get a "comparison vector" for each input location.
$$
\begin{align}
  v_{A,i} &= g([a_i, \beta_i]), \; i=1,\ldots, m \\
  v_{B,j} &= g([b_j, \alpha_j]), \; j=1,\ldots, n
\end{align}
$$

```python
class Compare(nn.Module):
    num_hiddens: int

    def setup(self):
        self.g = mlp(self.num_hiddens, False)

    @nn.compact
    def __call__(self, A, B, beta, alpha, train=True):
        V_A = self.g(jnp.concatenate((A, beta), axis=2), train)
        V_B = self.g(jnp.concatenate((B, alpha), axis=2), train)
        return V_A, V_B
```

## Aggregation

We sum-pool the "comparison vectors" for each input sentence, and then pass the pair of poolings to yet another MLP $h$ to generate the final classification.

$$
\begin{align}
  v_A &= \sum_{i=1}^m v_{A,i} \\
  v_B &= \sum_{j=1}^n v_{B,j} \\
  \hat{y} &= h([v_A, v_B])
\end{align}
$$

```python
class Aggregate(nn.Module):
    num_hiddens: int
    num_outputs: int

    @nn.compact
    def __call__(self, V_A, V_B, train=True):
        # Sum up both sets of comparison vectors
        V_A = V_A.sum(axis=1)
        V_B = V_B.sum(axis=1)
        # Feed the concatenation of both summarization results into an MLP
        Y_hat = nn.Dense(self.num_outputs)(mlp(self.num_hiddens, True)(jnp.concatenate((V_A, V_B), axis=1) ,train))
        return Y_hat
```

## Putting it altogether

We use a pre-trained embedding of size E=100.
The $f$ (attend) function maps from $E=100$  to $H=200$ hiddens.
The $g$ (compare) function maps $2E=200$ to $H=200$.
The $h$ (aggregate) function maps $2H=400$ to 3 outputs.

```python
class DecomposableAttention(nn.Module):
    vocab: Any
    embed_size: int
    num_hiddens: int
    embed_init: Callable

    def setup(self):
        self.embedding = nn.Embed(len(self.vocab), self.embed_size, embedding_init=self.embed_init)
        self.attend = Attend(self.num_hiddens)
        self.compare = Compare(self.num_hiddens)
        # There are 3 possible outputs: entailment, contradiction, and neutral
        self.aggregate = Aggregate(self.num_hiddens, 3)

    def __call__(self, X, train=True):
        premises, hypotheses = X
        A = self.embedding(premises)
        B = self.embedding(hypotheses)
        beta, alpha = self.attend(A, B, train)
        V_A, V_B = self.compare(A, B, beta, alpha, train)
        Y_hat = self.aggregate(V_A, V_B, train)
        return Y_hat
```

```python
class TokenEmbedding:
    """Token Embedding."""
    def __init__(self, embedding_name):
        self.idx_to_token, self.idx_to_vec = self._load_embedding(
            embedding_name)
        self.unknown_idx = 0
        self.token_to_idx = {
            token: idx for idx, token in enumerate(self.idx_to_token)}

    def _load_embedding(self, embedding_name):
        idx_to_token, idx_to_vec = ['<unk>'], []
        data_dir = download_extract(embedding_name)
        # GloVe website: https://nlp.stanford.edu/projects/glove/
        # fastText website: https://fasttext.cc/
        with open(os.path.join(data_dir, 'vec.txt'), 'r') as f:
            for line in f:
                elems = line.rstrip().split(' ')
                token, elems = elems[0], [float(elem) for elem in elems[1:]]
                # Skip header information, such as the top row in fastText
                if len(elems) > 1:
                    idx_to_token.append(token)
                    idx_to_vec.append(elems)
        idx_to_vec = [[0] * len(idx_to_vec[0])] + idx_to_vec
        return idx_to_token, jnp.array(idx_to_vec)

    def __getitem__(self, tokens):
        indices = [
            self.token_to_idx.get(token, self.unknown_idx)
            for token in tokens]
        vecs = self.idx_to_vec[jnp.array(indices)]
        return vecs

    def __len__(self):
        return len(self.idx_to_token)

```

```python
def embedding_init(rng, shape, dtype):
    # get pre-trained GloVE embeddings of size 100
#     glove_embedding = TokenEmbedding('glove.6b.100d')
#     embeds = glove_embedding[vocab.idx_to_token]
    return embeds
```

```python
DATA_URL = 'http://d2l-data.s3-accelerate.amazonaws.com/glove.6B.100d.zip'
DATA_HUB['glove.6b.100d'] = (DATA_URL, 'cd43bfb07e44e6f27cbcc7bc9ae3d80284fdaf5a')

glove_embedding = TokenEmbedding('glove.6b.100d')
embeds = glove_embedding[vocab.idx_to_token]

embed_size, num_hiddens = 100, 200
AttentionNetwork = functools.partial(DecomposableAttention, vocab, embed_size, num_hiddens, embedding_init)
```

    Downloading ../data/glove.6B.100d.zip from http://d2l-data.s3-accelerate.amazonaws.com/glove.6B.100d.zip...

# Training

```python
class Animator:
    """For plotting data in animation."""
    def __init__(self, xlabel=None, ylabel=None, legend=None, xlim=None,
                 ylim=None, xscale='linear', yscale='linear',
                 fmts=('-', 'm--', 'g-.', 'r:'), nrows=1, ncols=1,
                 figsize=(3.5, 2.5)):
        # Incrementally plot multiple lines
        if legend is None:
            legend = []
        display.set_matplotlib_formats('svg')
        self.fig, self.axes = plt.subplots(nrows, ncols, figsize=figsize)
        if nrows * ncols == 1:
            self.axes = [self.axes,]
        # Use a lambda function to capture arguments
        self.config_axes = lambda: set_axes(self.axes[
            0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
        self.X, self.Y, self.fmts = None, None, fmts

    def add(self, x, y):
        # Add multiple data points into the figure
        if not hasattr(y, "__len__"):
            y = [y]
        n = len(y)
        if not hasattr(x, "__len__"):
            x = [x] * n
        if not self.X:
            self.X = [[] for _ in range(n)]
        if not self.Y:
            self.Y = [[] for _ in range(n)]
        for i, (a, b) in enumerate(zip(x, y)):
            if a is not None and b is not None:
                self.X[i].append(a)
                self.Y[i].append(b)
        self.axes[0].cla()
        for x, y, fmt in zip(self.X, self.Y, self.fmts):
            self.axes[0].plot(x, y, fmt)
        self.config_axes()
        display.display(self.fig)
        display.clear_output(wait=True)

class Timer:
    """Record multiple running times."""
    def __init__(self):
        self.times = []
        self.start()

    def start(self):
        """Start the timer."""
        self.tik = time.time()

    def stop(self):
        """Stop the timer and record the time in a list."""
        self.times.append(time.time() - self.tik)
        return self.times[-1]

    def avg(self):
        """Return the average time."""
        return sum(self.times) / len(self.times)

    def sum(self):
        """Return the sum of time."""
        return sum(self.times)

    def cumsum(self):
        """Return the accumulated time."""
        return np.array(self.times).cumsum().tolist()

class Accumulator:
    """For accumulating sums over `n` variables."""
    def __init__(self, n):
        self.data = [0.0] * n

    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0.0] * len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]
```

```python
def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):
    """Set the axes for matplotlib."""
    axes.set_xlabel(xlabel)
    axes.set_ylabel(ylabel)
    axes.set_xscale(xscale)
    axes.set_yscale(yscale)
    axes.set_xlim(xlim)
    axes.set_ylim(ylim)
    if legend:
        axes.legend(legend)
    axes.grid()
```

```python
def cross_entropy_loss(logits, labels) -> float:
    one_hot = jax.nn.one_hot(labels, num_classes=3)
    loss = optax.softmax_cross_entropy(logits=logits, labels=one_hot)
    return loss.sum()

def compute_metrics(logits, labels):
    """Computes metrics and returns them."""
    loss = cross_entropy_loss(logits, labels)
    accuracy = jnp.sum(jnp.argmax(logits, -1) == labels)
    metrics = {
        'loss': loss,
        'accuracy': accuracy,
    }
    return metrics
```

```python
def get_initial_params(model, rng):
    X = jnp.ones((2, 128, 50), dtype=jnp.int32)
    variables = model.init(jax.random.PRNGKey(0), X, False)
    return variables['params']

def get_train_state(rng, lr) -> train_state.TrainState:
    """Returns a train state."""
    model = AttentionNetwork()
    params = get_initial_params(model, rng)
    tx = optax.adam(lr)
    return train_state.TrainState.create(
        apply_fn=model.apply, params=params, tx=tx)
```

```python
def evaluate_accuracy(state: train_state.TrainState, data_iter):
    """Compute the accuracy for a model on a dataset using a GPU."""
    # No. of correct predictions, no. of predictions
    metric = Accumulator(2)
    for X, y in data_iter:
        X = [jnp.array(x) for x in X]
        y = jnp.array(y)
        logits = state.apply_fn({'params': state.params}, X, False)
        accuracy = jnp.sum(jnp.argmax(logits, -1) == y)
        metric.add(accuracy, y.size)
    return metric[0] / metric[1]
```

```python
@jax.jit
def train_step(state: train_state.TrainState, dropout_rng, features, labels):

    """Trains one step."""
    def loss_fn(params):
        logits = state.apply_fn({'params': params}, features, True, rngs={'dropout': dropout_rng})
        loss = cross_entropy_loss(logits, labels)
        return loss, logits

    grad_fn = jax.value_and_grad(loss_fn, has_aux=True)
    (_, logits), grads = grad_fn(state.params)
    state = state.apply_gradients(grads=grads)
    metrics = compute_metrics(logits, labels)

    return state, metrics
```

```python
def train(train_iter, test_iter, num_epochs, lr):
    key = jax.random.PRNGKey(42)

    state = get_train_state(key, lr)

    timer, num_batches = Timer(), len(train_iter)
    animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0, 1], legend=['train loss', 'train acc', 'test acc'])

    for epoch in range(num_epochs):
        # Store training_loss, training_accuracy, num_examples, num_features
        metric = Accumulator(4)
        for i, (features, labels) in enumerate(train_iter):
            features = [jnp.array(x) for x in features]
            labels = jnp.array(labels)
            timer.start()
            state, metrics = train_step(state, key, features, labels)
            l = metrics['loss']
            acc = metrics['accuracy']
            metric.add(l, acc, labels.shape[0], labels.size)
            timer.stop()
            if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
                animator.add(
                    epoch + (i + 1) / num_batches,
                    (metric[0] / metric[2], metric[1] / metric[3], None))
        # Calculate Test Accuracy
        test_acc = evaluate_accuracy(state, test_iter)
        animator.add(epoch + 1, (None, None, test_acc))
    device = jax.default_backend()
    print(f'loss {metric[0] / metric[2]:.3f}, train acc '
          f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on '
          f'{str(device)}')
    return state
```

```python
lr, num_epochs = 0.001, 4
state = train(train_iter, test_iter, num_epochs, lr)
```

    loss 0.500, train acc 0.802, test acc 0.811
    6486.3 examples/sec on gpu

# Testing

```python
def predict_snli(state, vocab, premise, hypothesis):
    model = AttentionNetwork()
    premise = jnp.array(vocab[premise])
    hypothesis = jnp.array(vocab[hypothesis])
    features = [premise.reshape((1, -1)), hypothesis.reshape((1, -1))]
    logits = state.apply_fn({'params': state.params}, features, False)
    label = jnp.argmax(logits)
    return 'entailment' if label == 0 else 'contradiction' if label == 1 \
            else 'neutral'
```

```python
predict_snli(state, vocab, ['he', 'is', 'good', '.'], ['he', 'is', 'bad', '.'])
```

    'contradiction'

```python
predict_snli(state, vocab, ['he', 'is', 'very', 'naughty', '.'], ['he', 'is', 'bad', '.'])
```

    'entailment'

```python
predict_snli(state, vocab, ['he', 'is', 'awful', '.'], ['he', 'is', 'bad', '.'])
```

    'entailment'

```python
predict_snli(state, vocab, ['he', 'is', 'handsome', '.'], ['he', 'is', 'bad', '.'])
```

    'contradiction'

## Examples from training set

```python
predict_snli(state, vocab,
             ['a', 'person', 'on', 'a', 'horse', 'jumps', 'over', 'a', 'log' '.'],
              ['a', 'person', 'is', 'outdoors', 'on', 'a', 'horse', '.'])
```

    'entailment'

```python
predict_snli(state, vocab,
             ['a', 'person', 'on', 'a', 'horse', 'jumps', 'over', 'a', 'log' '.'],
              ['a', 'person', 'is', 'at', 'a', 'diner', 'ordering', 'an', 'omelette', '.'])
```

    'contradiction'

```python
predict_snli(state, vocab,
             ['a', 'person', 'on', 'a', 'horse', 'jumps', 'over', 'a', 'log' '.'],
              ['a', 'person', 'is', 'training', 'a', 'horse', 'for', 'a', 'competition', '.'])
```

    'neutral'