## Data-driven Design and Analyses of Structures and Materials (3dasm)

## Lecture 8

### Miguel A. Bessa | M.A.Bessa@tudelft.nl  | Associate Professor

**What:** A lecture of the "3dasm" course

**Where:** This notebook comes from this [repository](https://github.com/bessagroup/3dasm_course)

**Reference for entire course:** Murphy, Kevin P. *Probabilistic machine learning: an introduction*. MIT press, 2022. Available online [here](https://probml.github.io/pml-book/book1.html)

**How:** We try to follow Murphy's book closely, but the sequence of Chapters and Sections is different. The intention is to use notebooks as an introduction to the topic and Murphy's book as a resource.
* If working offline: Go through this notebook and read the book.
* If attending class in person: listen to me (!) but also go through the notebook in your laptop at the same time. Read the book.
* If attending lectures remotely: listen to me (!) via Zoom and (ideally) use two screens where you have the notebook open in 1 screen and you see the lectures on the other. Read the book.

**Optional reference (the "bible" by the "bishop"... pun intended 😆) :** Bishop, Christopher M. *Pattern recognition and machine learning*. Springer Verlag, 2006.

**References/resources to create this notebook:**
* [Car figure](https://korkortonline.se/en/theory/reaction-braking-stopping/)

Apologies in advance if I missed some reference used in this notebook. Please contact me if that is the case, and I will gladly include it here.

## **OPTION 1**. Run this notebook **locally in your computer**:
1. Confirm that you have the 3dasm conda environment (see Lecture 1).

2. Go to the 3dasm_course folder in your computer and pull the last updates of the [repository](https://github.com/bessagroup/3dasm_course):
```
git pull
```
3. Open command window and load jupyter notebook (it will open in your internet browser):
```
conda activate 3dasm
jupyter notebook
```
4. Open notebook of this Lecture.

## **OPTION 2**. Use **Google's Colab** (no installation required, but times out if idle):

1. go to https://colab.research.google.com
2. login
3. File > Open notebook
4. click on Github (no need to login or authorize anything)
5. paste the git link: https://github.com/bessagroup/3dasm_course
6. click search and then click on the notebook for this Lecture.

```python
# Basic plotting tools needed in Python.

import matplotlib.pyplot as plt # import plotting tools to create figures
import numpy as np # import numpy to handle a lot of things!
from IPython.display import display, Math # to print with Latex math

%config InlineBackend.figure_format = "retina" # render higher resolution images in the notebook
plt.style.use("seaborn") # style for plotting that comes from seaborn
plt.rcParams["figure.figsize"] = (8,4) # rescale figure size appropriately for slides
```

## Outline for today

* Parameter estimation from training with data (model fitting)
    - Posterior approximation by Dirac delta "distribution"
    - Point estimates for the Dirac delta "distribution"
        * MAP: Maximum A Posterior estimate
        * MLE: Maximum Likelihood Estimation
    - Negative log likelihood (NLL)
* Why some people do not adopt a Bayesian (probabilistic) perspective of ML

**Reading material**: This notebook + Chapter 4

## Summary of past lectures

Bayesian inference:
* predicts a **quantity of interest** (e.g. $y$) while treating **unknown** information as rv's (e.g. $z$)

* it is based on establishing a model (observation distribution + prior) and evaluating it on data (joint likelihood normalized by marginal likelihood) to update our belief about the unknown (posterior)

* from the posterior, we can then predict a distribution for the quantity of interest (the PPD) that results from marginalizing (integrating out) the unknown

### The good and the bad

In short: Bayesian inference results from interpreting the unknown as rv's of a model, and then evaluating the impact of all possible values of the rv's (within the constraints imposed by the model!) by marginalizing them (integrating them out).

* **The good**: This is powerful because even if our assumptions are wrong, we can at least take different values for the rv's and their respective impact on the predictions. This alleviates problems such as overfitting and overconfidence, that we will encounter in the remaining of the course.

* **The bad**: Bayesian inference can be difficult. We solved one of the simplest problems in the last lectures, and we saw that those integrals are a bit ugly...
    - In most cases, the integrals (to compute the marginal likelihood, and the PPD) cannot even be solved analytically.
    - Numerical strategies exist to approximate the integration, but they tend to be **slow when accurate** or **fast but innacurate** (a dangerous generalization: forgive me Bayesians!)

**Very Important Question (VIQ)**: What if we don't calculate these integrals at all?

## Machine Learning without going fully Bayesian

Avoiding integration is possible by noting that:

1. Computing the PPD is trivial if the **posterior distribution becomes the Dirac delta**

2. The marginal likelihood is just a **constant**

Let's explore these two remarks.

### 1. PPD when the posterior is a Dirac delta

$$\require{color}
{\color{orange}p(y|\mathcal{D}_y)} = \int \underbrace{p(y|z)}_{\text{observation}\\ \text{distribution}} \overbrace{p(z|y=\mathcal{D}_y)}^{\text{posterior}} dz
$$

What happens if the posterior is the Dirac delta "distribution"?

$$
p(z|y=\mathcal{D}_y) = \delta(z-\hat{z})
$$

where $\hat{z}$ is our best estimate for the value that $z$ should have.

$$\require{color}
\begin{align}
{\color{orange}p(y|\mathcal{D}_y)} &= \int \underbrace{p(y|z)}_{\text{observation}\\ \text{distribution}} \overbrace{p(z|y=\mathcal{D}_y)}^{\text{posterior}} dz\\
&= \int p(y|z) \delta(z-\hat{z}) dz \\
&= p(y|z=\hat{z})
\end{align}
$$

**Conclusion**: The PPD becomes the **observation distribution** where the unknown $z$ becomes our **best estimate** $\hat{z}$ (in other words: $z = \hat{z} =$ const)

* But what is our "**best estimate**" $\hat{z}$?
    - There are different estimates and different strategies to get there!

### 2. Finding the "best estimate" $\hat{z}$ without computing the marginal likelihood

Remember: the Bayes' rule determines the posterior,

$\require{color}$
$$
{\color{green}p(z|y=\mathcal{D}_y)} = \frac{ {\color{blue}p(y=\mathcal{D}_y|z)}{\color{red}p(z)} } {p(y=\mathcal{D}_y)}
$$

and the marginal likelihood $p(y=\mathcal{D}_y)$ is just a constant.

If we want to reduce the posterior to the Dirac delta "distribution",

$$
p(z|y=\mathcal{D}_y) = \delta(z-\hat{z})
$$

what is the only parameter that we need to find?

* We just need to find $\hat{z}$ to completely characterize $\delta(z-\hat{z})$

Note that this is not the case if the posterior is a different distribution!

For example, we saw in the previous lectures that the posterior for the car stopping distance problem was a **Gaussian**.

* How many parameters do you need to characterize the Gaussian distribution?

Indeed... Two!

And if the posterior distribution is more complicated, you may need a lot more parameters! In some cases, the posterior does not even have an analytical description!

Anyway, the question still remains: what should be the value $\hat{z}$?

Let's go back to the two problems we have seen in Lecture 6 and Lecture 7.

Recall our reflection on the differences between the posterior for the two priors we used.

* When using the noninformative Uniform prior $p(z) = \frac{1}{C_z}$ (Lecture 6):

$$\require{color}\begin{align}
{\color{green}p(z|y=\mathcal{D}_y)}
&= \mathcal{N}(z|\mu, \sigma^2)
\end{align}
$$

* When using a Gaussian prior $p(z) = \mathcal{N}\left(z| \overset{\scriptscriptstyle <}{\mu}_z, \overset{\scriptscriptstyle <}{\sigma}_z^2\right)$ (Lecture 7):

$$\require{color}\begin{align}
{\color{green}p(z|y=\mathcal{D}_y)} &= \mathcal{N}\left(z| \overset{\scriptscriptstyle >}{\mu}_z, \overset{\scriptscriptstyle >}{\sigma}_z^2\right) = \mathcal{N}\left(z\left|\frac{1}{\frac{1}{\sigma^2} + \frac{1}{\overset{\scriptscriptstyle <}{\sigma}_z^2}} \left( \frac{\mu}{\sigma^2} + \frac{\overset{\scriptscriptstyle <}{\mu}_z}{\overset{\scriptscriptstyle <}{\sigma}_z^2}\right), \frac{1}{\frac{1}{\sigma^2} + \frac{1}{\overset{\scriptscriptstyle <}{\sigma}_z^2}}\right.\right)
\end{align}
$$

The posterior is still a Gaussian but its mean and variance have been updated by the influence of the prior!

Let's play a simple game:
* Choose where to place the Dirac delta "distribution" for those two posteriors we found before.

```python
# This cell is hidden during the presentation
from scipy.stats import norm # import the normal dist, as we learned before!
def samples_y_with_2rvs(N_samples,x): # observations/measurements/samples for car stop. dist. prob. with 2 rv's
    mu_z1 = 1.5; sigma_z1 = 0.5;
    mu_z2 = 0.1; sigma_z2 = 0.01;
    samples_z1 = norm.rvs(mu_z1, sigma_z1, size=N_samples) # randomly draw samples from the normal dist.
    samples_z2 = norm.rvs(mu_z2, sigma_z2, size=N_samples) # randomly draw samples from the normal dist.
    samples_y = samples_z1*x + samples_z2*x**2 # compute the stopping distance for samples of z_1 and z_2
    return samples_y # return samples of y
```

```python
# This cell is hidden during the presentation

# -------------------------------------------------------------------------------
# PARAMETERS YOU CAN CHANGE! PLAY A BIT WITH THIS ;)
x = 75 # keeping the car velocity constant at 75 m/s as we have done before
mu_z2 = 0.1; sigma_z2 = 0.01 # parameters of z_2 distribution
N_samples = 3 # Let's say our data is composed of 3 samples (empirical observations)
mu_prior_z = 3; sigma_prior_z = 2 # parameters of the Gaussian prior distribution (used only in case 2)
# -------------------------------------------------------------------------------

empirical_y = samples_y_with_2rvs(N_samples, x) # Our data (empirical measurements of N_samples at x=75)

# Compute all the constants needed to plot the posterior for Lecture 6 and for Lecture 7
w = x
b = mu_z2*x**2
sigma_yGIVENz = np.sqrt((x**2*sigma_z2)**2) # sigma_y|z (comes from the stochastic influence of the z_2 rv)
# Empirical mean and std directly calculated from observations:
empirical_mu_y = np.mean(empirical_y); empirical_sigma_y = np.std(empirical_y);
#
# Parameters of the likelihood function (not a distribution because it is not normalized):
sigma = np.sqrt(sigma_yGIVENz**2/(w**2*N_samples)) # std arising from the likelihood
mu = empirical_mu_y/w - b/w # mean arising from the likelihood (product of Gaussian densities for the data)
# -------------------------------------------------------------------------------
# Case 1: using a noninformative Uniform prior (Lecture 6):
# Posterior parameters:
#    These parameters are obvious in this case but I just want to highlight that the mean and std of this posterior
#    are the same as the parameters of the likelihood because posterior = likelihood / const )
sigma_posterior_UniformPrior = sigma # std of posterior (same as likelihood)
mu_posterior_UniformPrior = mu # mean of posterior (same as likelihood)
#
# PPD parameters:
PPD_mu_y_UniformPrior = mu*w + b # same result if using: np.mean(empirical_y)
PPD_sigma_y_UniformPrior = np.sqrt(w**2*sigma**2+sigma_yGIVENz**2) # same as: np.sqrt((x**2*sigma_z2)**2*(1/N_samples + 1))

# z values for plot of case 1:
zrange_case1 = np.linspace(-3*sigma_posterior_UniformPrior+mu_posterior_UniformPrior,
                            3*sigma_posterior_UniformPrior+mu_posterior_UniformPrior, 200)
# Posterior values for plot of case 1:
posterior_pdf_values_case1 = norm.pdf(zrange_case1, mu, sigma)
# Probability density of posterior at the mean for case 1:
pdf_at_mean_case1 =  norm.pdf(mu_posterior_UniformPrior,mu_posterior_UniformPrior,sigma_posterior_UniformPrior)
# MAP estimate (maximum a posterior estimate) is the same as MLE (maximum likelihood estimation) for case 1:
pdf_at_mode_case1 = pdf_at_mean_case1 # in this case it's the same as mean (no calculation needed)
# -------------------------------------------------------------------------------
#
# -------------------------------------------------------------------------------
# CASE 2: using a Gaussian prior (Lecture 7):
# Posterior parameters:
sigma_posterior_GaussianPrior = np.sqrt( (sigma_prior_z**2*sigma**2)/(sigma_prior_z**2+sigma**2) )# std of posterior
mu_posterior_GaussianPrior = sigma_posterior_GaussianPrior**2*(mu/(sigma**2)+mu_prior_z/(sigma_prior_z**2)) # mean of posterior
# PPD parameters:
PPD_mu_y_GaussianPrior = mu_posterior_GaussianPrior*w + b
PPD_sigma_y_GaussianPrior = np.sqrt(w**2*sigma_posterior_GaussianPrior**2+sigma_yGIVENz**2)
#
# z values for plot:
zrange_case2 = np.linspace(-3*sigma_posterior_GaussianPrior+mu_posterior_GaussianPrior,
                            3*sigma_posterior_GaussianPrior+mu_posterior_GaussianPrior, 200)
# Posterior values for plot of case 2:
posterior_pdf_values_case2 = norm.pdf(zrange_case2, mu_posterior_GaussianPrior,
                                      sigma_posterior_GaussianPrior) # values of posterior for plotting
# Probability density of posterior at the mean for case 2:
pdf_at_mean_case2 = norm.pdf(mu_posterior_GaussianPrior,mu_posterior_GaussianPrior,sigma_posterior_GaussianPrior)
# MAP estimate (maximum a posterior estimate) for case 2:
pdf_at_mode_case2 = pdf_at_mean_case2 # in this case it's the same as mean (no calculation needed)
# -------------------------------------------------------------------------------

# Plot the posteriors that we calculate above and the Dirac delta at different z_hat
def Posteriors_and_Dirac_delta(z_hat_case1=mu_posterior_UniformPrior-2*sigma_posterior_UniformPrior,
                               z_hat_case2=mu_posterior_GaussianPrior-2*sigma_posterior_GaussianPrior):
    fig_Dirac, (ax_case1, ax_case2) = plt.subplots(1,2)
    #
    ax_case1.plot(zrange_case1, posterior_pdf_values_case1,
                  label=r"Posterior: $p(z|\mathcal{D}_y) = \mathcal{N}\left(z| \mu, \sigma^2\right)$")
    ax_case1.set_ylim(0, 1.3*pdf_at_mode_case1)
    ax_case1.plot(mu_posterior_UniformPrior, pdf_at_mean_case1,
                  'g^', markersize=25, linewidth=2,
                  label=r'mode: $\underset{z}{\mathrm{argmax}}\; p(z|\mathcal{D}_y)=\mu$')
    ax_case1.plot(mu_posterior_UniformPrior, pdf_at_mode_case1,
                  'k*', markersize=20, linewidth=2,
                  label=r'mean: $\mathbb{E}[z|\mathcal{D}_y]=\mu$')
    ax_case1.annotate("",
            xy=(z_hat_case1, 0), xycoords='data',
            xytext=(z_hat_case1, 1.3*pdf_at_mode_case1), textcoords='data',
            arrowprops=dict(arrowstyle="<-",
                            connectionstyle="arc3", color='r', lw=2),
            )
    ax_case1.text(z_hat_case1, pdf_at_mode_case1*1.05, 'Dirac $\delta$', rotation = -90, fontsize = 15)
    ax_case1.text(z_hat_case1, 0, ('$\hat{z}=%1.2f$' % z_hat_case1), fontsize = 15)
    ax_case1.set_xlabel("z", fontsize=20)
    ax_case1.set_ylabel("probability density", fontsize=20)
    ax_case1.legend(loc='center right', fontsize=12)
    ax_case1.set_title("Posterior using noninformative Uniform prior (Lecture 6)", fontsize=20)
    #
    ax_case2.plot(zrange_case2, posterior_pdf_values_case2,
                  label=r"Posterior: $p(z|\mathcal{D}_y) = \mathcal{N}\left(z| \overset{>}{\mu}_z, \overset{>}{\sigma}_z^2\right)$")

    ax_case2.set_ylim(0, 1.3*pdf_at_mode_case2)
    ax_case2.plot(mu_posterior_GaussianPrior, pdf_at_mean_case2,
                  'g^', markersize=25, linewidth=2,
                  label=r'mode: $\underset{z}{\mathrm{argmax}}\; p(z|\mathcal{D}_y)=\overset{>}{\mu}_z$')
    ax_case2.plot(mu_posterior_GaussianPrior, pdf_at_mean_case2,
                  'k*', markersize=20, linewidth=2,
                  label=r'mean: $\mathbb{E}[z|\mathcal{D}_y]=\overset{>}{\mu}_z$')
    ax_case2.annotate("",
            xy=(z_hat_case2, 0), xycoords='data',
            xytext=(z_hat_case2, 1.3*pdf_at_mode_case2), textcoords='data',
            arrowprops=dict(arrowstyle="<-",
                            connectionstyle="arc3", color='r', lw=2),
            )
    ax_case2.text(z_hat_case2, pdf_at_mode_case2*1.05, 'Dirac $\delta$', rotation = -90, fontsize = 15)
    ax_case2.text(z_hat_case2, 0, ('$\hat{z}=%1.2f$' % z_hat_case2), fontsize = 15)
    ax_case2.set_xlabel("z", fontsize=20)
    ax_case2.set_ylabel("probability density", fontsize=20)
    ax_case2.legend(loc='center right', fontsize=12)
    ax_case2.set_title("Posterior using Gaussian prior (Lecture 7)", fontsize=20)
    fig_Dirac.set_size_inches(15, 6) # scale figure to be wider (since there are 2 subplots)
```

```python
# Static plot (I skip this cell in presentations, but use it when printing slides to PDF)
Posteriors_and_Dirac_delta(z_hat_case1=mu_posterior_UniformPrior-2*sigma_posterior_UniformPrior,
                           z_hat_case2=mu_posterior_GaussianPrior-2*sigma_posterior_GaussianPrior)
```

```python
# Showing posteriors and Dirac delta with interactive plot. Code is hidden in presentation.
from ipywidgets import interactive # so that we can interact with the plot
interactive_plot = interactive(Posteriors_and_Dirac_delta,
                       z_hat_case1=(min(zrange_case1), max(zrange_case1), 6/10*sigma_posterior_UniformPrior),
                       z_hat_case2=(min(zrange_case2), max(zrange_case2), 6/10*sigma_posterior_GaussianPrior) )
interactive_plot
```

    interactive(children=(FloatSlider(value=0.2645198973023184, description='z_hat_case1', max=2.429583406763415, …

Probably you didn't hesitate to place the Dirac delta "distribution" at the mean or mode (they are the same for a Gaussian distribution)!

What if the Posterior distribution is something else? For example, a Gamma distribution

```python
# This cell is hidden during the presentation

# You may recall that we plotted the Gamma distribution in Lecture 1
# -------------------------------------------------------------------------------
# PARAMETERS YOU CAN CHANGE! PLAY A BIT WITH THIS ;)
x = 75 # keeping the car velocity constant at 75 m/s as we have done before
mu_z2 = 0.1; sigma_z2 = 0.01 # parameters of z_2 distribution
N_samples = 3 # Let's say our data is composed of 3 samples (empirical observations)
mu_prior_z = 3; sigma_prior_z = 2 # parameters of the Gaussian prior distribution (used only in case 2)
# -------------------------------------------------------------------------------

from scipy.stats import gamma # import from scipy.stats the Gamma distribution
from scipy.optimize import minimize # import minimizer to calculate mode

a = 2.0 # this is the only input parameter needed for this distribution

# Define the support of the distribution (its domain) by using the
# inverse of the cdf (called ppf) to get the lowest z of the plot that
# corresponds to Pr = 0.01 and the highest z of the plot that corresponds
# to Pr = 0.99:
zrange_min = gamma.ppf(0.01, a)
zrange_max = gamma.ppf(0.99, a)
zrange = np.linspace(zrange_min, zrange_max, 200)

mu_posterior, var_posterior = gamma.stats(2.0, moments='mv') # This computes the mean and variance of the pdf

posterior_pdf_values = gamma.pdf(zrange, a)

pdf_at_mean = gamma.pdf(mu_posterior, a)

# Finding the maximum of a function can be done by minimizing
# the negative gamma pdf. So, we create a function that outputs
# the negative of the gamma pdf given the parameter a=2.0:
def neg_gamma_given_a(z): return -gamma.pdf(z,a)

# Use the default optimizer of scipy (L-BFGS) to find the
# maximum (by minimizing the negative gamma pdf). Note
# that we need to give an initial guess for the value of z,
# so we can use, for example, z=mu_z:
mode_posterior = minimize(neg_gamma_given_a,mu_posterior).x # in general this is a vector, but for Gamma it's just a scalar

pdf_at_mode = gamma.pdf(mode_posterior, a) # in general this is a vector, but for Gamma is just a scalar

# Plot the posteriors that we calculate above and the Dirac delta at different z_hat
def Gamma_Posterior_and_Dirac_delta(z_hat=0.5):
    fig_Gamma, ax = plt.subplots()
    ax.plot(zrange, posterior_pdf_values, label=r"Posterior: $p(z|\mathcal{D}_y) = \Gamma(z|a)$")

    ax.plot(mu_posterior, pdf_at_mean, 'r*', markersize=15, linewidth=2,
            label=r'Posterior mean: $\mathbb{E}[z|\mathcal{D}_y]$')

    ax.plot(mode_posterior, pdf_at_mode[0],'g^', markersize=15,
            linewidth=2,label=r'Posterior mode: $\underset{z}{\mathrm{argmax}}\; p(z|\mathcal{D}_y)$')
    ax.annotate("",
            xy=(z_hat, 0), xycoords='data',
            xytext=(z_hat, 1.3*pdf_at_mode[0]), textcoords='data',
            arrowprops=dict(arrowstyle="<-",
                            connectionstyle="arc3", color='r', lw=2),
            )
    ax.text(z_hat, pdf_at_mode[0]*1.05, 'Dirac $\delta$', rotation = -90, fontsize = 15)
    ax.text(z_hat, 0, ('$\hat{z}=%1.2f$' % z_hat) , fontsize = 15)
    ax.set_ylim(0, 1.3*pdf_at_mode[0])
    ax.set_xlabel("z", fontsize=20)
    ax.set_ylabel("probability density", fontsize=20)
    ax.legend(loc='upper right', fontsize=15)
    ax.set_title("Posterior being a Gamma pdf for $a=2.0$", fontsize=20)
```

```python
# Static plot (I skip this cell in presentations, but use it when printing slides to PDF)
Gamma_Posterior_and_Dirac_delta(z_hat=0.5)
```

```python
# Showing posteriors and Dirac delta with interactive plot. Code is hidden in presentation.
interactive_plot = interactive(Gamma_Posterior_and_Dirac_delta,z_hat=(0.5, 6.5, 0.5 ) )
interactive_plot
```

    interactive(children=(FloatSlider(value=0.5, description='z_hat', max=6.5, min=0.5, step=0.5), Output()), _dom…

Maybe now you are hesitating where to place it?

Both are used in practice! And there are other estimates...

These are called **point estimates**.

* They reduce each unknown rv $z$ to a point $\hat{z}$ (transforming the posterior distribution into the Dirac delta "distribution").

Of course, as everything in life, some choices are better than others...

Common point estimates for determining $\hat{z}$:
* Maximum Likelihood Estimation (MLE):
    - You choose the mode (the maximum) of the posterior but you used a Uniform prior
* Maximum A Posterior (MAP) estimate:
    - You choose the mode (the maximum) of the posterior (and your prior is **not** Uniform)
* Posterior mean estimate (no accronym!):
    - You choose the mean of the posterior.
* ... and so on

Calculating the **Posterior mean estimate** is not new to us (see Lecture 1):

$$
\mathbb{E}[z|\mathcal{D}]= \int_{\mathcal{Z}}z p(z|\mathcal{D}) dz
$$

But I told you that today we are all about avoiding integrals!

So, let's focus on two very common point estimates: **MAP** and **MLE**.

Both are obtained by finding the **mode** of the posterior (i.e. maximum location in the posterior):

$$
\require{color}\hat{\mathbf{z}} = \underset{z}{\mathrm{argmax}}\; {\color{green}p(z|\mathcal{D})}
$$

In other words, we need to solve an optimization problem.

But finding the mode of the posterior involves a few simple "tricks"...

$$\require{color}
{\color{green}p(z|y=\mathcal{D}_y)} = \frac{ {\color{blue}p(y=\mathcal{D}_y|z)}{\color{red}p(z)} } {p(y=\mathcal{D}_y)}
$$

#### Calculating the mode of posterior: Trick 1 (taking the $\log$)

We can separate the three terms of the posterior if we work with its $\log$:

$$
\log{{\color{green}p(z|y=\mathcal{D}_y)}} = \log{{\color{blue}p(y=\mathcal{D}_y|z)}} + \log{{\color{red}p(z)}} - \log{p(y=\mathcal{D}_y)}
$$

* Note: $\log$ is a monotone function, so the $\mathrm{argmax}$ of a function is the same as the $\mathrm{argmax}$ of the $\log$ of the function! Mathematically:

$$
\require{color}\hat{\mathbf{z}} = \underset{z}{\mathrm{argmax}}\; {\color{green}p(z|\mathcal{D})} = \underset{z}{\mathrm{argmax}}\; \log{{\color{green}p(z|\mathcal{D})}}
$$

#### Calculating the mode: Trick 2 (maximizing by minimizing the negative $\log$)

**Maximizing a function** is the same as **minimizing the negative of a function** (flipping the sign in the end).

Mathematically:

$$
\require{color}\hat{\mathbf{z}} = \underset{z}{\mathrm{argmax}}\; \log{{\color{green}p(z|\mathcal{D})}} = \underset{z}{\mathrm{argmin}} \left[-\log{{\color{green}p(z|\mathcal{D})}}\right]
$$

* In numerical optimization, this is very common practice!
    - Most optimization algorithms are designed to *minimize* functions.
    - In general, when we are optimizing (whether maximizing or minimizing) functions we call them "**objective**" functions. Yet, in particular:
        * when we are *minimizing* functions we call them "**loss**" or "cost" functions.
        * when we are *maximizing* functions we call them "**reward**" or "score" functions.

#### Calculating the mode: focusing on each $\log$ term

$$\require{color}
\begin{align}
\hat{\mathbf{z}} &= \underset{z}{\mathrm{argmax}}\left[\log{{\color{green}p(z|y=\mathcal{D}_y)}}\right] \\
&= \underset{z}{\mathrm{argmin}}\left[-\log{{\color{green}p(z|y=\mathcal{D}_y)}}\right] \\
&= \underset{z}{\mathrm{argmin}}\left[-\log{{\color{blue}p(y=\mathcal{D}_y|z)}} - \log{{\color{red}p(z)}} + \log{p(y=\mathcal{D}_y)}\right]\\
&= \underset{z}{\mathrm{argmin}}\left[-\log{{\color{blue}p(y=\mathcal{D}_y|z)}} - \log{{\color{red}p(z)}}+\text{constant}\right]\\
\end{align}
$$

* The last line can be further simplified because a constant does not change the location of the minimum.

So, we get: $\require{color}
\hat{\mathbf{z}} = \underset{z}{\mathrm{argmin}}\left[-\log{{\color{blue}p(y=\mathcal{D}_y|z)}} - \log{{\color{red}p(z)}}\right]$

At this point, recall that the likelihood is usually calculated assuming the training examples (observations) are sampled independently from the observation distribution $p(y|z)$:

$$
p(y=\mathcal{D}_y | z) = \prod_{i=1}^{N} p(y=y_i|z)
$$

which is known as the **i.i.d.** assumption (independent and identically distributed).

This means that the $\log$ likelihood usually has a very convenient form:

$$
\mathrm{LL}(z) = \log{{\color{blue}p(y=\mathcal{D}_y|z)}} = \sum_{i=1}^{N} \log{p(y=y_i|z)}
$$

which decomposed into a sum of terms, one per example (observation).

**In summary**, the mode of the posterior is calculated as:

$\require{color}
\hat{\mathbf{z}} = \underset{z}{\mathrm{argmin}}\left[-\log{{\color{blue}p(y=\mathcal{D}_y|z)}} - \log{{\color{red}p(z)}}\right]$

where the first term is called **negative log likelihood**:

$\mathrm{NLL}(z) = -\mathrm{LL}(z) = -\log{{\color{blue}p(y=\mathcal{D}_y|z)}}=-\sum_{i=1}^{N} \log{p(y=y_i|z)}$

#### Maximum A Posterior (MAP) estimate

If we choose any prior distribution **except** the Uniform distribution, then the estimate is called MAP:

$\require{color}
\hat{\mathbf{z}}_{\text{map}} = \underset{z}{\mathrm{argmin}}\left[-\log{{\color{blue}p(y=\mathcal{D}_y|z)}} - \log{{\color{red}p(z)}}\right]$

where $p(z)$ is **not** the Uniform distribution.

#### Maximum Likelihood Estimation (MLE)

In the special case of choosing the prior to be a **Uniform distribution**, $p(z) \propto 1$, then the mode of the posterior becomes the same as the mode of the (log) likelihood:

$$\require{color}
\hat{\mathbf{z}} = \underset{z}{\mathrm{argmin}}\left[-\log{{\color{blue}p(y=\mathcal{D}_y|z)}} - \log{{\color{red}p(z)}}\right] = \underset{z}{\mathrm{argmin}}\left[-\log{{\color{blue}p(y=\mathcal{D}_y|z)}}\right]
$$

and we say that we are using the Maximum Likelihood Estimation (MLE) for the unknown $z$:

$$
\hat{\mathbf{z}}_{\text{mle}} = \underset{z}{\mathrm{argmin}}\left[-\log{{\color{blue}p(y=\mathcal{D}_y|z)}}\right] = \underset{z}{\mathrm{argmin}}\left[-\sum_{i=1}^{N}\log{ p(y=y_i|z)}\right]
$$

where, again, the argument of this expression is called the **negative log likelihood** $\mathrm{NLL}(z)$.

## Summary of Machine Learning without going fully Bayesian

1. Approximate posterior by a **Dirac delta** "distribution" $\delta(z-\hat{z})$ where $\hat{z}$ is a chosen **Point estimate**:
    * MLE: $\hat{\mathbf{z}}_{\text{mle}} = \underset{z}{\mathrm{argmin}}\left[-\sum_{i=1}^{N}\log{ p(y=y_i|z)}\right]$
    * MAP: $\hat{\mathbf{z}}_{\text{map}} = \underset{z}{\mathrm{argmin}}\left[-\sum_{i=1}^{N}\log{ p(y=y_i|z)}- \log{p(z)}\right] $
    * etc.

2. Compute the PPD using the Point estimate $\hat{z}$ and without calculating any integrals:
$$\require{color}
{\color{orange}p(y|\mathcal{D}_y)} = \int p(y|z) \delta(z-\hat{z}) dz = p(y|z=\hat{z})
$$

# HOMEWORK

1. Using the MLE point estimate, predict the PPD for the car stopping distance problem (Lecture 6).

2. Using the MAP estimate, predict the PPD for the car stopping distance problem considering the Gaussian prior of Lecture 7.

3. Create a plot of the two PPD's and compare them with the PPD's obtained in Lecture 6 and Lecture 7.
    * Note: create these plots of the PPD's such that the abscissa (horizontal) axis is the $y$ rv and the ordinate (vertical axis) is the probability density.

"Teaser": PPD obtained with the MLE **versus** PPD obtained in Lecture 6 (Uniform prior)

```python
# This cell is hidden during presentation. It's just to define a function to plot the governing model of
# the car stopping distance problem. Defining a function that creates a plot allows to repeatedly run
# this function on cells used in this notebook.
def car_fig_2rvs(ax):
    x = np.linspace(3, 83, 1000)
    mu_z1 = 1.5; sigma_z1 = 0.5;  # parameters of the "true" p(z_1)
    mu_z2 = 0.1; sigma_z2 = 0.01; # parameters of the "true" p(z_2)
    mu_y = mu_z1*x + mu_z2*x**2 # From Homework of Lecture 4
    sigma_y = np.sqrt( (x*sigma_z1)**2 + (x**2*sigma_z2)**2 ) # From Homework of Lecture 4
    ax.set_xlabel("x (m/s)", fontsize=20) # create x-axis label with font size 20
    ax.set_ylabel("y (m)", fontsize=20) # create y-axis label with font size 20
    ax.set_title("Car stopping distance problem with two rv's", fontsize=20); # create title with font size 20
    ax.plot(x, mu_y, 'k:', label="Governing model $\mu_y$")
    ax.fill_between(x, mu_y - 1.9600 * sigma_y,
                    mu_y + 1.9600 * sigma_y,
                    color='k', alpha=0.2,
                    label='95% confidence interval ($\mu_y \pm 1.96\sigma_y$)') # plot 95% credence interval
    ax.legend(fontsize=15)
```

```python
# This cell is hidden during presentation
def MLE_versus_Bayesian_PPD_for_UniformPrior(N_samples):
    fig_car_PPD_UniformPrior, ax_car_PPD_UniformPrior = plt.subplots(1,2)
    x = 75
    mu_z2 = 0.1; sigma_z2 = 0.01
    # Observation of N_samples from the true data:
    empirical_y = samples_y_with_2rvs(N_samples, x) # Empirical measurements of N_samples at x=75
    # Empirical mean and std directly calculated from observations:
    empirical_mu_y = np.mean(empirical_y); empirical_sigma_y = np.std(empirical_y);
    #
    # --------------------------------------------------------------------------------------------
    # PPD calculated in Lecture 6 (Uniform prior)
    # Now define all the constants needed in the calculation of the PPD's obtained with each prior.
    w = x
    b = mu_z2*x**2
    sigma_yGIVENz = np.sqrt((x**2*sigma_z2)**2) # sigma_y|z (comes from the stochastic influence of the z_2 rv)
    sigma = np.sqrt(sigma_yGIVENz**2/(w**2*N_samples)) # std arising from the likelihood
    mu = empirical_mu_y/w - b/w # mean arising from the likelihood (product of Gaussian densities for the data)
    #
    # Now, calculate PPD when using a UNIFORM prior (Lecture 6):
    PPD_mu_y_UniformPrior = mu*w + b # same result if using: np.mean(empirical_y)
    PPD_sigma_y_UniformPrior = np.sqrt(w**2*sigma**2+sigma_yGIVENz**2) # same as: np.sqrt((x**2*sigma_z2)**2*(1/N_samples + 1))
    # --------------------------------------------------------------------------------------------

    # --------------------------------------------------------------------------------------------
    # MLE:
    z_mle = mu # in this case it also coincides with the mean of the likelihood.
    PPD_mu_y_mle = w*z_mle + b # same as empirical mean (also same as mean of Bayesian PPD for Uniform prior)
    PPD_sigma_y_mle = sigma_yGIVENz # NOT the same as Bayesian PPD for Uniform prior (only in the limit)
    # --------------------------------------------------------------------------------------------

    car_fig_2rvs(ax_car_PPD_UniformPrior[0]) # a function I created to include the background plot of the governing model
    for i in range(2): # create two plots (one is zooming in on the error bar)
        ax_car_PPD_UniformPrior[i].errorbar(x , empirical_mu_y,yerr=1.96*empirical_sigma_y, fmt='m*',
                               markersize=30, elinewidth=9);
        ax_car_PPD_UniformPrior[i].errorbar(x , PPD_mu_y_UniformPrior,yerr=1.96*PPD_sigma_y_UniformPrior,
                               color='#F39C12', fmt='*', markersize=15, elinewidth=6);
        ax_car_PPD_UniformPrior[i].errorbar(x , PPD_mu_y_mle,yerr=1.96*PPD_sigma_y_mle,
                               fmt='w*', markersize=10, elinewidth=3);
        ax_car_PPD_UniformPrior[i].scatter(x*np.ones_like(empirical_y),empirical_y, s=150,facecolors='none',
                              edgecolors='k', linewidths=2.0)
    print("Ground truth                   : mean[y] = 675    & std[y] = 67.6")
    print("Empirical values (purple)      : mean[y] = %.2f & std[y] = %.2f" % (empirical_mu_y,empirical_sigma_y) )
    print("PPD with Uniform Prior (orange): mean[y] = %.2f & std[y] = %.2f" % (PPD_mu_y_UniformPrior, PPD_sigma_y_UniformPrior))
    print("PPD from MLE (white)           : mean[y] = %.2f & std[y] = %.2f" % (PPD_mu_y_mle,PPD_sigma_y_mle))
    fig_car_PPD_UniformPrior.set_size_inches(15, 6) # scale figure to be wider (since there are 2 subplots)
```

```python
MLE_versus_Bayesian_PPD_for_UniformPrior(N_samples=2)
```

"Teaser": PPD obtained with the MLE **versus** PPD obtained in Lecture 7 (Gaussian prior)

```python
# This cell is hidden during presentation
def MAP_versus_Bayesian_PPD_for_GaussianPrior(N_samples):
    fig_car_PPD_GaussianPrior, ax_car_PPD_GaussianPrior = plt.subplots(1,2)
    x = 75
    mu_z2 = 0.1; sigma_z2 = 0.01
    # Observation of N_samples from the true data:
    empirical_y = samples_y_with_2rvs(N_samples, x) # Empirical measurements of N_samples at x=75
    # Empirical mean and std directly calculated from observations:
    empirical_mu_y = np.mean(empirical_y); empirical_sigma_y = np.std(empirical_y);
    #
    # --------------------------------------------------------------------------------------------
    # PPD calculated in Lecture 7 (Gaussian prior)
    w = x
    b = mu_z2*x**2
    sigma_yGIVENz = np.sqrt((x**2*sigma_z2)**2) # sigma_y|z (comes from the stochastic influence of the z_2 rv)
    sigma = np.sqrt(sigma_yGIVENz**2/(w**2*N_samples)) # std arising from the likelihood
    mu = empirical_mu_y/w - b/w # mean arising from the likelihood (product of Gaussian densities for the data)
    #
    mu_prior_z = 3; sigma_prior_z = 2 # parameters of the Gaussian prior distribution
    sigma_posterior_z = np.sqrt( (sigma_prior_z**2*sigma**2)/(sigma_prior_z**2+sigma**2) )# std of posterior
    mu_posterior_z = sigma_posterior_z**2*( mu/(sigma**2) + mu_prior_z/(sigma_prior_z**2) ) # mean of posterior
    PPD_mu_y_GaussianPrior = mu_posterior_z*w + b
    PPD_sigma_y_GaussianPrior = np.sqrt(w**2*sigma_posterior_z**2+sigma_yGIVENz**2)
    # --------------------------------------------------------------------------------------------

    # --------------------------------------------------------------------------------------------
    # MAP:
    z_map = mu_posterior_z # in this case it also coincides with the mean of the posterior
    PPD_mu_y_map = w*z_map + b # same as empirical mean (also same as mean of Bayesian PPD for Uniform prior)
    PPD_sigma_y_map = sigma_yGIVENz # NOT the same as Bayesian PPD for Uniform prior (only in the limit)
    # --------------------------------------------------------------------------------------------

    #
    car_fig_2rvs(ax_car_PPD_GaussianPrior[0]) # a function I created to include the background plot of the governing model
    for i in range(2): # create two plots (one is zooming in on the error bar)
        ax_car_PPD_GaussianPrior[i].errorbar(x , empirical_mu_y,yerr=1.96*empirical_sigma_y, fmt='m*',
                               markersize=30, elinewidth=9);
        ax_car_PPD_GaussianPrior[i].errorbar(x , PPD_mu_y_GaussianPrior,yerr=1.96*PPD_sigma_y_GaussianPrior,
                               fmt='b*', markersize=15, elinewidth=6);
        ax_car_PPD_GaussianPrior[i].errorbar(x , PPD_mu_y_map,yerr=1.96*PPD_sigma_y_map,
                               fmt='c*', markersize=10, elinewidth=3);
        ax_car_PPD_GaussianPrior[i].scatter(x*np.ones_like(empirical_y),empirical_y, s=150,facecolors='none',
                              edgecolors='k', linewidths=2.0)
    print("Ground truth                  : mean[y] = 675    & std[y] = 67.6")
    print("Empirical values (purple)     : mean[y] = %.2f & std[y] = %.2f" % (empirical_mu_y,empirical_sigma_y) )
    print("PPD with Gaussian Prior (blue): mean[y] = %.2f & std[y] = %.2f" % (PPD_mu_y_GaussianPrior,PPD_sigma_y_GaussianPrior))
    print("PPD from MAP (cyan)           : mean[y] = %.2f & std[y] = %.2f" % (PPD_mu_y_map, PPD_sigma_y_map))
    fig_car_PPD_GaussianPrior.set_size_inches(15, 6) # scale figure to be wider (since there are 2 subplots)
```

```python
MAP_versus_Bayesian_PPD_for_GaussianPrior(N_samples=3)
```

## Final reflection: what strategy should we choose?

Approximating the PPD using a Point estimate is usually much simpler and faster than marginalizing unknown rv's such as $z$ (integrals!).

This is true analytically as well as numerically.

This explains why many ML practitioners choose Point estimates like MLE or MAP.

But in general the predictions of the PPD have different robustness:

* PPD calculated from Posterior distribution > PPD from Point estimates
    - We can also say that within the Point estimates: Posterior mean estimate > MAP > MLE

We will see evidence in favor of this in the remaining of the course.

## Final reflection: Bayesian versus non-Bayesian perspective on ML

We can do one last simplification (but it can **mislead** us into believing that ML is not probabilistic!)

When the PPD is approximated by the observation distribution for a Point estimate $\hat{z}$,

$$\require{color}
{\color{orange}p(y|\mathcal{D}_y)} = p(y|z=\hat{z})
$$

we can decide to focus on only making a prediction for the **mean** of the PPD and even forget that it is a distribution (we forget uncertainties!).

This is very common in ML literature! But, I think it's advantageous not to think about it that way...

## Solution to the Homework of this Lecture

1. The PPD using the MLE for problem in Lecture 6 is:

$$
p(y\mid y=\mathcal{D}_y) = p(y \mid z=\hat{z}_{\text{mle}}) = \mathcal{N}\left(y| \mu_{y|z}=w \hat{z}_{\text{mle}}+b,\, \sigma_{y|z}^2 \right)
$$

where the MLE of $z$ is obtained as follows:

$$
\begin{align}
\hat{\mathbf{z}}_{\text{mle}} &= \underset{z}{\mathrm{argmin}}\left[-\sum_{i=1}^{N}\log{ p(y=y_i|z)}\right] \\
&= \underset{z}{\mathrm{argmin}}\left[-\sum_{i=1}^{N}\log{\left( \frac{1}{|w|}\frac{1}{\sqrt{2\pi \left(\frac{\sigma_{y|z}}{w}\right)^2}} \exp\left\{ -\frac{1}{2\left(\frac{\sigma_{y|z}}{w}\right)^2}\left[z-\left(\frac{y_i-b}{w}\right)\right]^2\right\}\right)}\right]\\
&= \underset{z}{\mathrm{argmin}}\left[-\sum_{i=1}^{N}\left(\log{\left( \frac{1}{|w|}\frac{1}{\sqrt{2\pi \left(\frac{\sigma_{y|z}}{w}\right)^2}}\right)} -\frac{1}{2\left(\frac{\sigma_{y|z}}{w}\right)^2}\left[z-\left(\frac{y_i-b}{w}\right)\right]^2\right)\right] \\
&= \underset{z}{\mathrm{argmin}}\left[\frac{1}{2\left(\frac{\sigma_{y|z}}{w}\right)^2}\sum_{i=1}^{N} \left[z-\left(\frac{y_i-b}{w}\right)\right]^2\right] \\
\end{align}
$$

To find the minimum location we need to take the derivative wrt $z$ and equal it to zero:

$$
\frac{1}{\left(\frac{\sigma_{y|z}}{w}\right)^2}\sum_{i=1}^{N} \left[z-\left(\frac{y_i-b}{w}\right)\right] = 0
$$

$$
N z - \sum_{i=1}^{N} \frac{y_i-b}{w}=0
$$

$$
z = \frac{1}{N} \sum_{i=1}^{N} \frac{y_i-b}{w}
$$

So, we conclude that:

$$
\hat{z}_{\text{mle}} = \frac{1}{N} \sum_{i=1}^{N} \frac{y_i-b}{w} = \mu
$$

**This result should be very familiar to you**!

Here's why: Remember that in Lecture 5 (and 6) we already calculated the likelihood to be ${\color{blue}p(y=\mathcal{D}_y | z)} = \frac{1}{|w|^N} \cdot C \cdot \frac{1}{\sqrt{2\pi \sigma^2}} \exp\left[ -\frac{1}{2\sigma^2}(z-\mu)^2\right]$, so it is obvious that this is maximized at the mean value of $z=\mu$ because the mode of a Gaussian is the same as the mean. This is why in the code above, I already knew the result without doing any calculation 😉

2. The PPD using the MAP for the Gaussian prior of Lecture 7 is:

$$
p(y\mid y=\mathcal{D}_y) = p(y \mid z=\hat{z}_{\text{map}}) = \mathcal{N}\left(y| \mu_{y|z}=w \hat{z}_{\text{map}}+b,\, \sigma_{y|z}^2 \right)
$$

where the MAP of $z$ is obtained as follows:

$$
\begin{align}
\hat{\mathbf{z}}_{\text{map}} &= \underset{z}{\mathrm{argmin}}\left[-\sum_{i=1}^{N}\log{ p(y=y_i|z)}-\log{p(z)}\right] \\
&= \underset{z}{\mathrm{argmin}}\left[\frac{1}{2\left(\frac{\sigma_{y|z}}{w}\right)^2}\sum_{i=1}^{N} \left[z-\left(\frac{y_i-b}{w}\right)\right]^2 - \log{\frac{1}{\sqrt{2\pi \overset{\scriptscriptstyle <}{\sigma}_z^2}}\exp\left\{-\frac{1}{2\overset{\scriptscriptstyle <}{\sigma}_z^2}\left(z-\overset{\scriptscriptstyle <}{\mu}_z\right)^2\right\}}\right] \\
&= \underset{z}{\mathrm{argmin}}\left[\frac{1}{2\left(\frac{\sigma_{y|z}}{w}\right)^2}\sum_{i=1}^{N} \left[z-\left(\frac{y_i-b}{w}\right)\right]^2+\frac{1}{2\overset{\scriptscriptstyle <}{\sigma}_z^2}\left(z-\overset{\scriptscriptstyle <}{\mu}_z\right)^2\right] \\
\end{align}
$$

Similarly, to find the minimum location we need to take the derivative wrt $z$ and equal it to zero:

$$
\frac{1}{\left(\frac{\sigma_{y|z}}{w}\right)^2}\sum_{i=1}^{N} \left[z-\left(\frac{y_i-b}{w}\right)\right] + \frac{1}{\overset{\scriptscriptstyle <}{\sigma}_z^2}\left(z-\overset{\scriptscriptstyle <}{\mu}_z\right)= 0
$$

$$
\frac{N w^2}{\sigma_{y|z}^2} z - \frac{w^2}{\sigma_{y|z}^2} \sum_{i=1}^{N} \frac{y_i-b}{w} + \frac{1}{\overset{\scriptscriptstyle <}{\sigma}_z^2} z - \frac{1}{\overset{\scriptscriptstyle <}{\sigma}_z^2}\overset{\scriptscriptstyle <}{\mu}_z =0
$$

Noting that $\sum_{i=1}^{N} \frac{y_i-b}{w} = N\mu$ and that $\frac{N w^2}{\sigma_{y|z}^2} = \frac{1}{\sigma^2}$,

$$
\frac{1}{\sigma^2} z - \frac{1}{\sigma^2}\mu + \frac{1}{\overset{\scriptscriptstyle <}{\sigma}_z^2} z - \frac{1}{\overset{\scriptscriptstyle <}{\sigma}_z^2}\overset{\scriptscriptstyle <}{\mu}_z =0
$$

$$
z = \frac{1}{\frac{1}{\sigma^2}+\frac{1}{\overset{\scriptscriptstyle <}{\sigma}_z^2}} \left( \frac{\mu}{\sigma^2}+\frac{\overset{\scriptscriptstyle <}{\mu}_z}{\overset{\scriptscriptstyle <}{\sigma}_z^2}\right)
$$

So, we conclude that:

$$
\hat{z}_{\text{map}} = \frac{1}{\frac{1}{\sigma^2}+\frac{1}{\overset{\scriptscriptstyle <}{\sigma}_z^2}} \left( \frac{\mu}{\sigma^2}+\frac{\overset{\scriptscriptstyle <}{\mu}_z}{\overset{\scriptscriptstyle <}{\sigma}_z^2}\right)
$$

Once again, **this result should be very familiar to you**!

Here's why: Remember that in Lecture 7 we already calculated the posterior to be ${\color{green}p(z|y=\mathcal{D}_y)} = \mathcal{N}\left(z| \overset{\scriptscriptstyle >}{\mu}_z, \overset{\scriptscriptstyle >}{\sigma}_z^2\right) = \mathcal{N}\left(z\left|\frac{1}{\frac{1}{\sigma^2} + \frac{1}{\overset{\scriptscriptstyle <}{\sigma}_z^2}} \left( \frac{\mu}{\sigma^2} + \frac{\overset{\scriptscriptstyle <}{\mu}_z}{\overset{\scriptscriptstyle <}{\sigma}_z^2}\right), \frac{1}{\frac{1}{\sigma^2} + \frac{1}{\overset{\scriptscriptstyle <}{\sigma}_z^2}}\right.\right)$, so it is obvious that this is maximized at the mean value of $z=\overset{\scriptscriptstyle >}{\mu}_z$ because the mode of a Gaussian is the same as the mean. Again, this is why in the code above, I already knew the result without doing any calculation 😉

3. Finally, we can plot the PPD's for question 1 and 2 and compare them with what we obtained in Lecture 7.

We already have computed the parameters in the last 2 plots of this lecture. This question is just asking to show these distributions in a different way.

```python
fig_HW, ax_HW = plt.subplots()

N_samples=3
x = 75
mu_z2 = 0.1; sigma_z2 = 0.01
#
# Observation of N_samples from the true data:
empirical_y = samples_y_with_2rvs(N_samples, x) # Empirical measurements of N_samples at x=75
# Empirical mean and std directly calculated from observations:
empirical_mu_y = np.mean(empirical_y); empirical_sigma_y = np.std(empirical_y);
#
# --------------------------------------------------------------------------------------------
# PPD calculated in Lecture 6 (Uniform prior)
# Now define all the constants needed in the calculation of the PPD's obtained with each prior.
w = x
b = mu_z2*x**2
sigma_yGIVENz = np.sqrt((x**2*sigma_z2)**2) # sigma_y|z (comes from the stochastic influence of the z_2 rv)
sigma = np.sqrt(sigma_yGIVENz**2/(w**2*N_samples)) # std arising from the likelihood
mu = empirical_mu_y/w - b/w # mean arising from the likelihood (product of Gaussian densities for the data)
#
# Now, calculate PPD when using a UNIFORM prior (Lecture 6):
PPD_mu_y_UniformPrior = mu*w + b # same result if using: np.mean(empirical_y)
PPD_sigma_y_UniformPrior = np.sqrt(w**2*sigma**2+sigma_yGIVENz**2) # same as: np.sqrt((x**2*sigma_z2)**2*(1/N_samples + 1))
# --------------------------------------------------------------------------------------------

# --------------------------------------------------------------------------------------------
# MLE:
z_mle = mu # in this case it also coincides with the mean of the likelihood.
PPD_mu_y_mle = w*z_mle + b # same as empirical mean (also same as mean of Bayesian PPD for Uniform prior)
PPD_sigma_y_mle = sigma_yGIVENz # NOT the same as Bayesian PPD for Uniform prior (only in the limit)
# --------------------------------------------------------------------------------------------

# --------------------------------------------------------------------------------------------
# PPD calculated in Lecture 7 (Gaussian prior)
w = x
b = mu_z2*x**2
sigma_yGIVENz = np.sqrt((x**2*sigma_z2)**2) # sigma_y|z (comes from the stochastic influence of the z_2 rv)
sigma = np.sqrt(sigma_yGIVENz**2/(w**2*N_samples)) # std arising from the likelihood
mu = empirical_mu_y/w - b/w # mean arising from the likelihood (product of Gaussian densities for the data)
#
mu_prior_z = 3; sigma_prior_z = 2 # parameters of the Gaussian prior distribution
sigma_posterior_z = np.sqrt( (sigma_prior_z**2*sigma**2)/(sigma_prior_z**2+sigma**2) )# std of posterior
mu_posterior_z = sigma_posterior_z**2*( mu/(sigma**2) + mu_prior_z/(sigma_prior_z**2) ) # mean of posterior
PPD_mu_y_GaussianPrior = mu_posterior_z*w + b
PPD_sigma_y_GaussianPrior = np.sqrt(w**2*sigma_posterior_z**2+sigma_yGIVENz**2)
# --------------------------------------------------------------------------------------------

# --------------------------------------------------------------------------------------------
# MAP:
z_map = mu_posterior_z # in this case it also coincides with the mean of the posterior
PPD_mu_y_map = w*z_map + b # same as empirical mean (also same as mean of Bayesian PPD for Uniform prior)
PPD_sigma_y_map = sigma_yGIVENz # NOT the same as Bayesian PPD for Uniform prior (only in the limit)
# --------------------------------------------------------------------------------------------

# --------------------------------------------------------------------------------------------
# I will also include the real distribution p(y)
# Note: we found this in Homework of Lecture 4 (solution shown in Lecture 5)
mu_z1 = 1.5
sigma_z1 = 0.5
real_mu_y = x*mu_z1 + mu_z2*x**2
real_sigma_y = np.sqrt((x*sigma_z1)**2 + (x**2*sigma_z2)**2)
# --------------------------------------------------------------------------------------------

# We can establish the limits of the plot based on the real distribution:
ymin = real_mu_y - 3*real_sigma_y
ymax = real_mu_y + 3*real_sigma_y
yrange = np.linspace(ymin, ymax, 200) # to plot

ax_HW.plot(yrange, norm.pdf(yrange, PPD_mu_y_UniformPrior, PPD_sigma_y_UniformPrior),
               '--', linewidth = 3, color='#F39C12', label='Bayesian PPD for Uniform prior')
ax_HW.plot(yrange, norm.pdf(yrange, PPD_mu_y_mle, PPD_sigma_y_mle),
               '-', linewidth = 3, color='#F39C12', label='PPD using MLE')
ax_HW.plot(yrange, norm.pdf(yrange, PPD_mu_y_GaussianPrior, PPD_sigma_y_GaussianPrior),
               'b--', linewidth = 3, label='Bayesian PPD for Gaussian prior')
ax_HW.plot(yrange, norm.pdf(yrange, PPD_mu_y_map, PPD_sigma_y_map),
               'b-', linewidth = 3, label='PPD using MAP')
ax_HW.plot(yrange, norm.pdf(yrange, real_mu_y, real_sigma_y),
               'k-', linewidth = 3, label='Real distribution $p(y)$')
ax_HW.set_xlabel("y", fontsize=20)
ax_HW.set_ylabel("probability density", fontsize=20)
ax_HW.legend(fontsize=15, loc='upper left');
fig_HW.set_size_inches(15, 6)
```

### See you next class

Have fun **but do your HOMEWORK**!


```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.

```python
# The below IFrame displays Page 162 of your textbook:
# Shultis, J. K. (2016). Fundamentals of Nuclear Science and Engineering Third Edition,
# 3rd Edition. [Vitalsource]. Retrieved from https://bookshelf.vitalsource.com/#/books/9781498769303/

from IPython.display import IFrame
IFrame("https://bookshelf.vitalsource.com/books/9781498769303/pageid/162", width=1000, height=500)

```

## 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)^*$?

### An Aside on Nuclear Energy in the Media

Prof. Huff's first job was at the LANSCE ICE HOUSE, 2003 & 2004

This image linked above is a screenshot from Spiderman 2, in 2004.

It is owned and copyright 2004 by Marvel comics.

It shows Dr. Octopus and the fuel for his fusion reactor.

#### 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))
```

    Q:  -10.401892923148063

#### 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.

##### Question: How do we handle the case when the incident and target nuclei both have initial velocity?

##### Harder Question: How do we handle the case when the incident and target nuclei are accelerating with respect to each other?

If $E_i$ is the kinetic energy of the $i^{th}$ nucleus.

\begin{align}
Ex = Ey + EY − Q.
\end{align}

```python
# The below IFrame displays Page 162 of your textbook:
# Shultis, J. K. (2016). Fundamentals of Nuclear Science and Engineering Third Edition,
# 3rd Edition. [Vitalsource]. Retrieved from https://bookshelf.vitalsource.com/#/books/9781498769303/

from IPython.display import IFrame
IFrame("https://bookshelf.vitalsource.com/books/9781498769303/pageid/162", width=1000, height=500)
```

# 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)
```

    Q:  -1.8505028887843764

```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)
```

    E_k_th:  2.0573975230549033
    E_k_th (simplified):  2.0574431294953586

```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)
```

    E_c_th:  1.558399146177754

```python
## Which one is higher?

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

    Total threshold:  2.0573975230549033

# 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.
Prof. Huff's first job was at the LANSCE ICE HOUSE, 2003 & 2004

## 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}

Prof. Huff 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))
```

    Max (4MeV alpha):  0.002192755433424931

## 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$)")

```

```python
# The below IFrame displays Page 172 of your textbook:
# Shultis, J. K. (2016). Fundamentals of Nuclear Science and Engineering Third Edition,
# 3rd Edition. [Vitalsource]. Retrieved from https://bookshelf.vitalsource.com/#/books/9781498769303/

from IPython.display import IFrame
IFrame("https://bookshelf.vitalsource.com/books/9781498769303/pageid/172", width=1000, height=500)
```

## 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}

### 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")
```

```python
#### Questions about this plot:

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

```

    Object `energy` not found.
    Object `energy` not found.

```python
- Can you write an equation for the average neutron energy
```

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

    0.358102702287 0.358102702287

#### 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

```python
# The below IFrame displays Page 183 of your textbook:
# Shultis, J. K. (2016). Fundamentals of Nuclear Science and Engineering Third Edition,
# 3rd Edition. [Vitalsource]. Retrieved from https://bookshelf.vitalsource.com/#/books/9781498769303/

from IPython.display import IFrame
IFrame("https://bookshelf.vitalsource.com/books/9781498769303/pageid/183", width=1000, height=500)

```

### 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
### Running in Google Colab? You'll want to uncomment and run these cell once each time you start this notebook.

"""
!pip uninstall cftime --yes
!pip install cftime==1.2.1
!pip install nc-time-axis
!pip install netcdf4
!wget https://github.com/psheehan/CIERA-HS-Program/blob/master/Projects/EarthsClimateModel/tas_Amon_CESM1-WACCM_rcp85_r2i1p1_200601-209912.nc?raw=true
!mv tas_Amon_CESM1-WACCM_rcp85_r2i1p1_200601-209912.nc?raw=true tas_Amon_CESM1-WACCM_rcp85_r2i1p1_200601-209912.nc
!apt-get install libproj-dev proj-data proj-bin
!apt-get install libgeos-dev
!pip uninstall shapely cartopy --yes
!pip install shapely cartopy --no-binary shapely --no-binary cartopy
"""
```

# _Welcome to Earth_

## In this module, we'll take the space exploration a little bit closer to home: Earth.

image from NASA (https://explorer1.jpl.nasa.gov/galleries/earth-from-space/#gallery-16)

# What makes the Earth unique?

The easiest answer? Us.

Humankind has perfectly evolved to thrive in this climate. Arguably, the main components that support this life is the atmospheric composition and the temperatures we experience because of it.

# Atmospheric Composition

The Earth has this unique atmosphere that allows for life to thrive. The air we breathe is made up of many elements, mainly: nitrogen (78.09%) and oxygen (20.95%). The remaining >1% of the atmosphere is made up of other trace gases like carbon dioxide ($CO_2$), methane ($CH_4$), and water vapor ($H_2O_v$).

You've probably heard of these molecules before -- water, $CO_2$, and $CH_4$ are famous greenhouse gases. Though you've probably heard of greenhouse gases, the definition is:

**Greenhouse gas:** A gas that absorbs infared energy thus trapping heat.

These greenhouse gases are actually necessary for life -- without these gases trapping heat in our atmosphere, we'd experience immensely cooler temperatures. This can be explained with some basic physics principles.

1. Above 0 Kelvin (or absolute zero), molecules in an object are moving. Therefore, all objects above 0 Kelvin emit radiation. This includes the Sun and the Earth!

2. The radiation ($E$) an object gives off is proportional to its absolute temperature ($T$), as described in the Stefan-Boltzman Law:
    $$E = \sigma T^4 $$ where $\sigma$ is a constant such that: $\sigma = 5.67*10^{-8} W/m^2$.
    If an object completely absorbs and emits radiation, it's what we call a **black body.**

3. The relationship between the object's temperature ($T$) and wavelength ($\lambda$) is called Wien's Law:
    $$ \lambda = c/T $$
    where $c$ is a constant such that: $c = 2897 \mu m * K$. The wavelength an object emits is what we percieve as the color of the object. The sun emits at a high temperature, so we see the the incoming light in the visible spectrum -- the sun glows yellow, so let's say it's emitting at 570 $nm$. The Earth emits radiation at a lower temperature in the infrared (50 $\mu m$), so we can't actually see the outgoing radiation.

After rearranging the Stefan-Boltzmann law, we can rearrange the equation to relate stars and black body planets temperatures to:

$$ T_{planet} = T_{star} * \sqrt { R_{sun}/{(2*a_{star-planet}) }}$$

With the sun radius of 696,340 km ($696*10^6 m$) and a = 149,598,000 km ($149*10^9 m)$

**Question 1: what is the temperature of the Sun's surface? What is the temperature of the Earth's surface?**

Hint: rearrange Stefan-Boltzman and Wien's law with the information you know about the sun and Earth's peak spectra

Hint 2: If you're still struggling -- Google stefan-boltzmann surface temperature of the sun! (My advice for all problems you can't figure out)

```python

```

**Question 2: Look up the average temperature of the Earth's surface. Do your answers match? Why or why not?**

```python

```

So....

These were leading questions obvioulsy to make you think about the power of that >1% atmosphere. Greenhouse gases are essential to making these temperatures habitable on Earth! What you just solved for was the 1-dimensional average temperature of the Earth if we did not have an atmosphere. But we do ... so it gets more complicated.

image from NASA (https://explorer1.jpl.nasa.gov/galleries/earth-from-space/#gallery-6)

# One-dimensional to three

The previous exercise gave you a 1-D view of the Earth. But we know that the Earth is a sphere which causes all sorts of fun problems, like the uneven distribution of sunlight as compared to the equators and the north pole.

From: http://www.geo.mtu.edu/KeweenawGeoheritage/Lake/Temperature_files/latitude_and_sunlight_large.jpg

Solar warming is generally greater at the equator where the sun shines directly and much less at the poles where the sun is low in the sky.  Surfaces that are perpendicular to the sun’s ray path heat faster than those at an angle.  This differential heating is passed on to the air above by conduction which causes air expansion and changes in pressure. Wind is the result of pressure changes in the atmosphere.  Any shoreline is a wind machine, because of solar heating effects.

**Question 3: Using your background knowledge, how would you describe the weather at the equator? (More than just temperatures ...)**

This differing temperature thus forces air parcels to move, and then the Earth's rotation creates a deflection on these parcels, giving us atmospheric circulation patterns.

The basic principle comes from the ideal gas law:
$$ PV = nRT $$
Where P = Pressure (atm), V = volume (liters), n = number moles of gas, T = temperature of gas (Kelvin), R = constant, 0.0821 L * atm * K^-1 * mol^-1

From: https://history.aip.org/climate/xGenCirc.htm

**Here is a video explainer of this figure if you do not understand the figure, the last 2 minutes are particularly useful for the next two questions:**

https://www.youtube.com/watch?v=ebjKyoQ6YoE

**Question 4: Looking at the figure above, how does the atmospheric circulation converge at the equator (0 degrees)? Think about where the air is going ... what it's bringing ...**

**Question 5: How about at 30 degrees? Looking at a map, what significant features are found on the 30th latitude? (Hint look to Africa....) Can you explain why this may be happening, given what you know about the way the ideal gas law works and the atmospheric circulation patterns?**

Unfortunately this perfect circulation model does not work across the entire planet -- think about the temperature of Ireland and Canada -- they're found on the same latitude, but one is so much warmer than the other!

Looking at the Earth as a simple cell does not perfectly explain our climate situation -- it would if we were an Aquaplanet without land (orthographic) scale features and a uniform depth in the ocean -- but obviously, our planet is way more complicated than that. And that's why we can't just use the Stefan-Boltzmann and ideal gas law to explain our climate! They are very useful and provide a part of the story about our climate, however, our mathematical models need to integrate the nuance of oceanic circulation, orthographic features, and clouds! As a result, we go look at far more complicated models -- climate models -- in order to better estimate what's happening in our environment.

# Intro to Climate Models

To reiterate, the energy on Earth comes from the sun, so incoming light -- or radiation, as I'll call it for the rest of this -- is key to controlling temperature. The ultimate amount of warming potential found in the earth is controlled by three knobs:

> 1. Incoming radiation: Amount of sunlight reaching Earth -- influenced by sun spots, the distance of the sun to the Earth

> 2. Albedo: Amount of reflectivity on Earth's surface -- what kind of landcover is on the planet

> 3. Gases that absorb longwave radiation: levels of greenhouse gas concentrations in the atmosphere  trap heat

If you change any of these three knobs, you change the climate because you change the radiation balance that we're in!

Climate models use this radiation balance to solve the energy balance, but there's also a mass balance that it must solve. This is stolved through the Navier-Stokes equation ... and it's not pretty.

\begin{equation}
\frac{\partial (\rho u_{i})}{\partial t} + \frac{\partial[\rho u_{i}u_{j}]}{\partial x_{j}} = -\frac{\partial p}{\partial x_{i}} + \frac{\partial \tau_{ij}}{\partial x_{j}} + \rho f_{i} \end{equation}

Basically, we're balancing the flow of fluid motion and balancing the speed, pressure, temperature and density of the gases in the atmosphere and the water in the ocean.

BUT DON'T WORRY -- I'm not having you solve for this equation. I have model data which has already done this for you! A climate model simultaneously solves for the energy and mass balance over a sphere, giving us an incredible tool to study the atmosphere and climate.

## Making Plots with Climate Model Output

For this assignment, you are tasked with creating a Python plot of Climate Model Intercomparison Project CMIP data. CMIP is a standard experimental framework for studying the output of coupled atmosphere-ocean general circulation models. This facilitates assessment of the strengths and weaknesses of climate models which can enhance and focus the development of future models. For example, if the models indicate a wide range of values either regionally or globally, then scientists may be able to determine the cause(s) of this uncertainty.

## Task:  Your assignment is to plot the average global temperature in the year 2066 using whatver map projection you'd like to use.

The CMIP file is called: tas_Amon_CESM1-WACCM_rcp85_r2i1p1_200601-209912.nc

Recommended libraries: xarray, netCDF4, cartopy, matplotlib.pyplot, numpy, pandas

```python
# Import libraries
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import xarray as xr
import nc_time_axis
import math
import numpy as np

from netCDF4 import Dataset
```

1. Read in the file using xarray

```python
file_in = # fill in: filename

DS_netcdf = Dataset(file_in)
DS=xr.open_dataset(file_in)
```

I guess we should talk about Xarray before we continue. Climate data file formats are typically netCDF files, a binary file format that makes these huge datasets more portable across different machines but not possible to open with something like excel. Really, the data are stored in a folder...  Xarray is a great tool to open these files up. But it takes a second to get used to this kinda format.

```python
print(DS)
```

Ok so attributes are not necessary for you all to know, but it's basically the metadata attached to the file! Most important part of the file is the data variables. **tas** is the variable we're more interested in -- it's temperature!

2. Select the 2066 time slice from the array

```python
startday= #complete
endday= #complete
feb66=DS.sel(time=slice(startday,endday))
```

3. Average the data over time! (http://xarray.pydata.org/en/stable/generated/xarray.Dataset.mean.html)

```python
feb66_avg=feb66.mean(dim= ) #complete
print(feb66_avg)
```

4. Now let's map it to make more sense of it...

```python
#Projection for plot
projection= ccrs.PlateCarree(central_longitude=255);
# Data projection
data_crs = ccrs.PlateCarree()

# Open figure object
plt.figure(figsize=(10, 6))
ax = plt.axes(projection=projection)
ax.set_global()
ax.coastlines()

# Make a contour plot
lon= # pull the data from the feb66_avg variable!
lat= # complete
data= # complete
cf=ax.contourf(lon, lat, data, levels=60, transform=data_crs, cbar_kwargs={'label': DS.tas.units})
plt.colorbar(cf)
ax.set_label(DS.tas.units)

# Add the gridlines, title, colorbar
ax.gridlines(crs=projection,linestyle="dotted", draw_labels=True)
plt.title("Temperature")

#Save our figure, show our figure! (Must save before showing)
plt.savefig('assignment1_feb66.png')
plt.show()
```

5. So there you have it! Your first climate plot. Look at your plot and ponder, what is happening in the world? Where is it warm? Where is it cool? Is this expected?

# Taking off the training wheels! Make your own plots now.

Now that you know how to make a plot, now I want to see:

(a) A map of average temperature from 2006-2016

```python
# Code here... don't worry, it's basically written above -- you can do this!

## End code
```

(b) A map of average temperature from 2089-2099

```python

```

(c) The difference between the temperatures in (a) and (b)

```python

```

# Time series plot

Now we can see how the temperature changes from 2006-2099. In this next plot we'll look at making the global average monthly temperatures from 2006-2099 (an x-y plot).

```python
# This will help you later....
def weighted_mean(data_da, dim, weights):
    r"""Computes the weighted mean.

    We can only do the actual weighted mean over the dimensions that
    ``data_da`` and ``weights`` share, so for dimensions in ``dim`` that aren't
    included in ``weights`` we must take the unweighted mean.

    This functions skips NaNs, i.e. Data points that are NaN have corresponding
    NaN weights.

    Args:
        data_da (xarray.DataArray):
            Data to compute a weighted mean for.
        dim (str | list[str]):
            dimension(s) of the dataarray to reduce over
        weights (xarray.DataArray):
            a 1-D dataarray the same length as the weighted dim, with dimension
            name equal to that of the weighted dim. Must be nonnegative.
    Returns:
        (xarray.DataArray):
            The mean over the given dimension. So it will contain all
            dimensions of the input that are not in ``dim``.
    Raises:
        (IndexError):
            If ``weights.dims`` is not a subset of ``dim``.
        (ValueError):
            If ``weights`` has values that are negative or infinite.
    """
    if isinstance(dim, str):
        dim = [dim]
    else:
        dim = list(dim)

    if not set(weights.dims) <= set(dim):
        dim_err_msg = (
            "`weights.dims` must be a subset of `dim`. {} are dimensions in "
            "`weights`, but not in `dim`."
        ).format(set(weights.dims) - set(dim))
        raise IndexError(dim_err_msg)
    else:
        pass  # `weights.dims` is a subset of `dim`

    if (weights < 0).any() or xr.ufuncs.isinf(weights).any():
        negative_weight_err_msg = "Weight must be nonnegative and finite"
        raise ValueError(negative_weight_err_msg)
    else:
        pass  # `weights` are nonnegative

    weight_dims = [
        weight_dim for weight_dim in dim if weight_dim in weights.dims
    ]

    if np.isnan(data_da).any():
        expanded_weights, _ = xr.broadcast(weights, data_da)
        weights_with_nans = expanded_weights.where(~np.isnan(data_da))
    else:
        weights_with_nans = weights

    mean_da = ((data_da * weights_with_nans).sum(weight_dims, skipna=True)
               / weights_with_nans.sum(weight_dims))
    other_dims = list(set(dim) - set(weight_dims))
    return mean_da.mean(other_dims, skipna=True)
```

1. Pull time period of interest

```python
data_2006_2099=DS.sel(time=slice('2006-01-01','2099-12-31')) # 94 years
```

2. Assign contents into variables

```python
lat = # latitude
lon = # longitude
tas = # near-surface air temperature
```

3. Calculate the weights according to latitude. There are multiple ways to do this (see here). In this example, we use the cosine of latitudes. Now we use the function defined above to calculate the weighted mean over the latitude, passing the correct parameters

```python
rad  = 4.*math.atan(1.)/180.
weights   = np.cos(lat*rad)
wmean_lat = weighted_mean(, dim='lat', weights=weights) #complete
```

4. Since we want a single average value for the year, we also average over the longitudes. This time it does not have to be weighted.

```python
wmean = wmean_lat.mean(dim='lon')
```

5. Plot time and data from wmean

```python
plt.figure(figsize=(10,3))
plt.plot(, , linestyle='-', color='black', linewidth=0.5) #complete
plt.xlabel('Time')
plt.ylabel('Temperature (K)')
plt.title('Global Monthly Mean Temperatures', loc='left')
```

6. Yay! Another plot. Again -- what is happening? Any observations? What's weird about this plot?

# Time series plot of temperature anomolies

The annual temperature has all of the seasonal cyclicity ... while it's clear the temperature is rising, we can remove the seasonal signal by subtracting a baseline. We can call 2006-2030 temperatures a baseline.

1. Get the annual temperatures and the baseline

```python
annual_wmean = wmean.groupby('time.year').mean(dim='time')

baseline = wmean.sel(time=slice('2006-01-01','2035-12-31')).mean(dim='time')
```

2. We then subtract the baseline from the annual weighted mean, to get the anomaly

```python
anomaly = # complete
```

3. Now plot the anomoly like you did above!

```python
# complate
```

Now we have global average temperature anomolies. Is it what you expected? Is this climate change?

# BONUS
If you've made it all the way here and still want more to do ...
Plot the global temperature anomoly as a map! This is a combination of the past 3 plots put all together... Good luck.

```python
#bonus
```