# ***Introduction to Radar Using Python and MATLAB***
## Andy Harrison - Copyright (C) 2019 Artech House
<br/>

# Hertzian Dipole
***

Referring to Section 4.1, the electric field radiated by a Hertzian dipole is written as (Equation 4.8)

\begin{align}
\vec{E} = 2\eta\,I_o\, \Delta{z} \, \frac{e^{-jkr}}{4 \pi r}\,\cos{\theta}\left[\frac{1}{r} - \frac{j}{kr^2} \right]\,\hat{r} + j\eta k\, I_o\, \Delta{z}\,\frac{e^{-jkr}}{4 \pi r}\,\sin{\theta}\,\left[1+\frac{1}{jkr} - \frac{1}{(kr)^2} \right]\hat{\theta}\hspace{0.5in} \text{(V/m)}
\end{align}
***

Begin by getting the library path

```python
import lib_path
```

Import the routines for the Hertzian dipole

```python
from Libs.radar_range.hertzian_dipole import *
```

For plotting the antenna patterns, the angle will be set from 0 to 2 $\pi$ using the `linspace` routine from `scipy`

```python
from numpy import linspace
```

```python
theta = linspace(0, 2.0 * pi, 256)
```

Set the relative permittivity, relative permeability, frequency (Hz), current (A), length (m), and distance (m)

```python
relative_permittivity = 1.0

relative_permeability = 1.0

frequency = 1e6

current = 1

length = 0.1

r = 1e6
```

Set up the keyword args

```python
kwargs = {'relative_permittivity': relative_permittivity,

          'relative_permeability': relative_permeability,

          'frequency': frequency,

          'current': current,

          'length': length,

          'r': r,

          'theta': theta}
```

Calculate the electric field (V/m)

```python
electric_field = electric_field(**kwargs)
```

Import the `matplotlib` routines

```python
from matplotlib import pyplot as plt
```

```python
# Set the figure size

plt.rcParams["figure.figsize"] = (15, 10)

# Display the results

plt.polar(theta, abs(electric_field))

# Set the plot title and labels

plt.title('Hertzian Dipole Electric Field (V/m)', size=14)

# Set the tick label size

plt.tick_params(labelsize=12)

# Turn on the grid

plt.grid(linestyle=':', linewidth=0.5)
```

The magnetic field radiated by a Hertzian dipole is written as (Equation 4.6)

\begin{equation}
    \vec{H} =  I_o \,\Delta z \, \sin{\theta}\, \frac{e^{-jkr}}{4 \pi r} \left[ jk + \frac{1}{r} \right] \, \hat{\phi}\hspace{0.5in} \text{(A/m)}
\end{equation}

Keyword args are slightly different for the magnetic field

```python
kwargsH = {'frequency': frequency,

           'current': current,

           'length': length,

           'r': r,

           'theta': theta}
```

Calculate the magnetic field (A/m)

```python
magnetic_field = magnetic_field(**kwargsH)
```

`matplotlib` is imported so just create the plot

```python
# Set the figure size

plt.rcParams["figure.figsize"] = (15, 10)

 # Display the results

plt.polar(theta, abs(magnetic_field))

# Set the plot title and labels

plt.title('Hertzian Dipole Magnetic Field (A/m)', size=14)

# Set the tick label size

plt.tick_params(labelsize=12)

# Turn on the grid

plt.grid(linestyle=':', linewidth=0.5)
```

The power density radiated by the Hertzian dipole is written as (Equation 4.12)

\begin{equation}\label{eq:hertzian_poynting_vector2}
    \vec{P}_d = \frac{\eta}{2}\, \Bigg[ \frac{k\, I_o\, \Delta{z}}{4\pi r} \sin{\theta}\Bigg]^2\, \hat{r} \hspace{0.5in} \text{(W/m\textsuperscript{2})}
\end{equation}

Calculate the power density (Watts/m^2)

```python
power_density = power_density(**kwargs)
```

Display the results using `matplotlib`

```python
# Set the figure size

plt.rcParams["figure.figsize"] = (15, 10)

# Display the results

plt.polar(theta, power_density)

# Set the plot title and labels

plt.title('Hertzian Dipole Power Density (W/m$^2$)', size=14)

# Set the tick label size

plt.tick_params(labelsize=12)

# Turn on the grid

plt.grid(linestyle=':', linewidth=0.5)
```

Set up the keyword args

```python
kwargs = {'relative_permittivity': relative_permittivity,

          'relative_permeability': relative_permeability,

          'frequency': frequency,

          'current': current,

          'length': length,

          'theta': theta}
```

The radiation intensity is written as (Equation 4.17)

\begin{equation}
    U(\theta,\phi) = \frac{\eta}{2}\,\left[ \frac{k\,I_o\,\Delta{z}}{4\pi}\,\sin{\theta}\right]^2  \hspace{0.5in} \text{(W/sr)}
\end{equation}

Calculate the radiation intensity (W/sr)

```python
radiation_intensity = radiation_intensity(**kwargs)
```

Display the results using `matplotlib`

```python
# Set the figure size

plt.rcParams["figure.figsize"] = (15, 10)

# Display the results

plt.polar(theta, radiation_intensity, '')

# Set the plot title and labels

plt.title('Hertzian Dipole Radiation Intensity', size=14)

# Set the tick label size

plt.tick_params(labelsize=12)

# Turn on the grid

plt.grid(linestyle=':', linewidth=0.5)
```

The directivity of a Hertzian dipole is written as (Equation 4.18)

\begin{equation}
    D(\theta,\phi) = \frac{4\pi\,U(\theta,\phi)}{P_{rad}} = \frac{3}{2}\,\sin^2{\theta}
\end{equation}

Calculate the directivity

```python
directivity = directivity(**kwargs)
```

Display the results using `matplotlib`

```python
# Set the figure size

plt.rcParams["figure.figsize"] = (15, 10)

# Display the results

plt.polar(theta, directivity, '')

# Set the plot title and labels

plt.title('Hertzian Dipole Directivity', size=14)

# Set the tick label size

plt.tick_params(labelsize=12)

# Turn on the grid

plt.grid(linestyle=':', linewidth=0.5)
```

The total power radiated by the Hertzian dipole is written as (Equation 4.13)

\begin{equation}\label{eq:hertzian_power_density}
    P_{rad} = \int\limits_{0}^{\pi}\int\limits_{0}^{2\pi} \vec{P}_d\cdot r^2 \sin{\theta}\,d\theta\,d\phi\,\hat{r}\hspace{0.5in} \text{(W)}
\end{equation}

Calculate and display the total radiated power (W)

Set up the keyword args

```python
kwargs = {'relative_permittivity': relative_permittivity,

          'relative_permeability': relative_permeability,

          'frequency': frequency,

          'current': current,

          'length': length}
```

```python
total_radiated_power = total_radiated_power(**kwargs)

print('{:.3e}'.format(total_radiated_power))
```

    4.390e-05


# Modelo de Vasicek

## Ecuaciones del Modelo

El modelo de Vasicek para la tasa instantánea en la medida histórica es:

$$
\begin{equation}
dr_t=\gamma\left(\overline{r}-r_t\right)dt+\sigma dX_t
\end{equation}
$$

Este modelo también supone que $\lambda$ el *market price of interest rate risk* es constante y por lo tanto, en la medida ajustada por riesgo:

$$
\begin{equation}
dr_t=\gamma^*\left(\overline{r}^*-r_t\right)dt+\sigma dX^*_t
\end{equation}
$$

donde $\gamma^*=\gamma$ y $\overline{r}^*=\overline{r}-\lambda\sigma\gamma^{-1}$.

La solución de esta ecuación es:

$$
\begin{equation}
r_t=r_0 e^{-\gamma^* t}+\overline{r}^*\left(1-e^{-\gamma^* t}\right)+\sigma e^{-\gamma^* t}\int_{0}^{t}e^{\gamma^* s}dX_s
\end{equation}
$$

Y además tenemos que:

$$
\begin{equation}
\mathbb{E}_s\left[r_t\right]=r_0 e^{-\gamma^* \left(t-s\right)}+\overline{r}^*\left(1-e^{-\gamma^* (t-s)}\right) \\
\end{equation}
$$

$$
\begin{equation}
Var_s\left[r_t\right]=\frac{\sigma^2}{2\gamma^*}\left(1-e^{-2\gamma^* (t-s)}\right)
\end{equation}
$$

El modelo de Vasicek admite una fórmula explícita para el valor de un bono cupón cero que vence en $T$:

$$
\begin{equation}
Z\left(r_t,t,T\right)=\exp\left[A\left(t,T\right)-B\left(t,T\right)r_t\right] \\
Z\left(r_T,T,T\right)=1
\end{equation}
$$

Donde:

$$
\begin{equation}
B\left(t,T\right)=\frac{1}{\gamma^*}\left\{1-\exp\left[-\gamma^*\left(T-t\right)\right]\right\}
\end{equation}
$$

$$
\begin{equation}
A\left(t,T\right)=\left[B\left(t,T\right)-\left(T-t\right)\right]\left[\overline{r}^*-\frac{\sigma^2}{2{\gamma^*}^2}\right]-\frac{\sigma^2{B\left(t,T\right)}^2}{4\gamma^*}
\end{equation}
$$

### Ejercicio

¿Qué distribución tiene el bono cupón cero $Z$ en el modelo de Vasicek?

Lognormal, porque $r_t$ es normal y $Z=\exp(A-B\cdot r)$ lo que implica que $\log Z$ es normal.

## Simulación del Modelo

Vamos a trabajar primero con parámetros inventados. Haremos una simulación. Luego usando la fórmula para el bono cupón cero estudiaremos el ajuste del modelo a un set de datos reales.

```python
from typing import List, Tuple
import scipy.optimize as opt
import plotly.express as px
import pandas as pd
import numpy as np
import math
```

```python
frmt = {'tasa': '{:.4%}', 'df': '{:.6%}'}
```

```python
# type hints
def func(x: float, y: float) -> float:
    return x + y
```

Damos de alta los parámetros del modelo (hay que interpretarlos como los parámetros en la medida ajustada por riesgo):

```python
gamma = 1
sigma = .01
r_ = .01
r0 = .01 # tasa corta a día de hoy
```

Veamos que forma tienen sus trayectorias:

```python
def vasicek_path(
        r0: float,
        gamma: float,
        r_: float,
        sigma: float,
        num_dias: int = 263,
        dias_agno: int = 264,
        pasos_dia: int = 1) -> List[Tuple[float, float]]:
    """
    Retorna un camino de simulación del modelo de Vasicek usando el esquema de Euler.

    r(t+dt) = r(t) + gamma * (r_ - r(t)) * dt + sigma * raiz(dt) * eps(t)
    r(t + dt) = gamma * r_ * dt + r(t) *(1 - gamma * dt) + sigma * raiz(dt) * eps(t)

    donde eps(t) distibuye N(0,1).

    params:

    - r0: tasa inicial (t = 0) de la simulación
    - Parámetros del modelo:
      - gamma: velocidad de reversión
      - r_: tasa de largo plazo
      - sigma: volatilidad
    - num_dias: número de días en la trayectoria, incluyendo el instante t = 0
    - dias_agno: número de días hábiles por año
    - pasos_dia: número de pasos de simulación en 1 día

    return:

    - Un `list` donde cada elemento es una `tuple` con los valores del tiempo y la tasa simulada.
    """
    dt = 1 / (dias_agno * pasos_dia)
    dt_gamma_r_ = dt * gamma * r_
    sigma_sqdt = sigma * math.sqrt(dt)
    result = [(0, r0), ]
    gamma_dt = gamma * dt

    r = r0
    for i in range(1, (num_dias + 1) * pasos_dia):
        r = dt_gamma_r_ + (1 - gamma_dt) * r + sigma_sqdt * \
            np.random.normal()  # Discretización de Euler
        result.append((i * dt, r))
    return result
```

```python
sim = vasicek_path(r0, gamma, r_, sigma, num_dias=1000)
df_sim = pd.DataFrame(sim, columns=['t', 'tasa'])
fig = px.line(
    df_sim,
    x='t',
    y='tasa',
    title=f'Simulación Modelo Vasicek. r0={r0:.2%}, gamma={gamma}, r_={r_:.2%}, sigma={sigma:.2%}'
)
fig.update_layout(yaxis_range=[-.04,.04])
fig.show()
```

## Estimación de Parámetros

Se utilizará la curva OIS que se construyó en clase (notebook 5):

```python
df_curva = pd.read_excel('data/20201012_built_sofr_zero.xlsx')
```

```python
df_curva.head().style.format(frmt)
```

        	        plazo	        tasa	        df

                        0
                        1
                        0.0811%
                        99.999778%

                        1
                        7
                        0.0841%
                        99.998388%

                        2
                        14
                        0.0780%
                        99.997010%

                        3
                        21
                        0.0774%
                        99.995549%

                        4
                        33
                        0.0781%
                        99.992942%

```python
# zcc = aux.get_curve_from_dataframe(Qcf.QCAct365(),Qcf.QCCompoundWf(), df_curva)
```

```python
fig = px.line(df_curva, x='plazo', y='tasa', title='Curva de Mercado')
fig.show()
```

Se definen las funciones asociadas a la fórmula para obtener un factor de descuento o precio de un bono cero cupón con el modelo de Vasicek.

```python
def b_vasicek(
        r: float,
        r_: float,
        gamma: float,
        t: float,
        T: float
) -> float:
    """
    Implementa la función B que interviene en el cálculo del valor de un bono cupón
    cero con el modelo de Vasicek.

    Z(t, T) = exp(A(t, T) - B(t, T) * r(t))

    params:

    - r: valor de la tasa corta
    - Parámetros del Modelo:
      - r_: tasa de largo plazo
      - gamma: velocidad de reversión
    - t: tiempo de cálculo
    - T: tiempo de vencimiento del bono (debe ser T >= t)

    return:

    - valor de la función B
    """
    return 1 / gamma * (1 - math.exp(-gamma * (T - t)))

def a_vasicek(
        r: float,
        r_: float,
        gamma: float,
        sigma: float,
        t: float,
        T: float
) -> float:
    """
    Implementa la función A que interviene en el cálculo del valor de un bono cupón
    cero con el modelo de Vasicek.

    Z(t, T) = exp(A(t, T) - B(t, T) * r(t))

    params:

    - r: valor de la tasa corta
    - Parámetros del Modelo:
      - r_: tasa de largo plazo
      - gamma: velocidad de reversión
    - t: tiempo de cálculo
    - T: tiempo de vencimiento del bono (debe ser T >= t)

    return:

    - valor de la función A
    """
    b = b_vasicek(r, r_, gamma, t, T)
    sigma2 = sigma ** 2
    return (b - (T - t)) * (r_ - sigma2 / (2 * gamma ** 2)) - (sigma2 * b ** 2) / (4.0 * gamma)

def zero_vasicek(
    r: float,
    r_: float,
    gamma: float,
    sigma: float,
    t: float,
    T: float
) -> float:
    """
    Implementa la fórmula para el cálculo del valor de un bono cupón cero con el modelo de Vasicek.

    Z(t, T) = exp(A(t, T) - B(t, T) * r(t))

    params:

    - r: valor de la tasa corta
    - Parámetros del Modelo:
      - r_: tasa de largo plazo
      - gamma: velocidad de reversión
    - t: tiempo de cálculo
    - T: tiempo de vencimiento del bono (debe ser T >= t)

    return:

    - valor de la función A
    """
    return math.exp(a_vasicek(r, r_, gamma, sigma, t, T) - b_vasicek(r, r_, gamma, t, T) * r)
```

```python
# Probar la función
df = zero_vasicek(r0, r_, gamma, sigma, 0, 1.5)
print(f'df: {df:.8%}')
```

    df: 98.51326945%

Queremos estimar los parámetros $\gamma^*$, $\overline{r}^*$ y $\sigma$ minimizando el error cuadrático medio entre los factores de descuento obtenidos con la fórmula del modelo y los factores de descuento de la curva de mercado (en `df_curva` más arriba).

Se define la función objetivo para encontrar los parámetros:

```python
# x = [r_, gamma, sigma] valor inicial
def objective(x: List[float], *args) -> float:
    """
    Calcula el error cuadrático total entre los valores de la curva de mercado y los obtenidos
    con la fórmula del modelo de Vasicek y los parámetros x.
    """
    error = 0.0
    # Supongo que el DataFrame con las tasas es el primer elemento de args
    for row in args[0].itertuples():
        # args[1] es la tasa corta de hoy
        error += (row.df - zero_vasicek(args[1], x[0], x[1], x[2], 0, row.plazo / 365)) ** 2
    return error
```

```python
objective(
    [r_, gamma, sigma], df_curva, r0)
```

    0.01708607452228135

```python
print(r_, gamma, sigma)
```

    0.01 1 0.01

Se encuentran los parámetros usando scipy:

```python
x0 = [.03, 0.5, .01]
r0 = df_curva.iloc[0]['tasa']
bnds = ((0, None), (0, None), (0, None))
result = opt.minimize(objective, x0, args=(df_curva, r0), bounds=bnds, tol=.00000001)
print(result)
optimo = result.x
```

          fun: 0.003306986381573132
     hess_inv: <3x3 LbfgsInvHessProduct with dtype=float64>
          jac: array([6.94039444e-05, 6.28577052e-07, 5.20417043e-09])
      message: b'CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH'
         nfev: 216
          nit: 38
         njev: 54
       status: 0
      success: True
            x: array([0.01031726, 0.22319265, 0.        ])

```python
print(f'r_: {optimo[0]:.4%}')
print(f'gamma: {optimo[1]:.4f}')
print(f'sigma: {optimo[2]:.4%}')
```

    r_: 1.0317%
    gamma: 0.2232
    sigma: 0.0000%

Z(0,T) = E(exp(-int(0,T)r(s) x ds))

¿Porqué $\sigma$ da 0?

Comparemos la curva real con la aproximación:

```python
v_curva = []
for row in df_curva.itertuples():
    v_curva.append((
        row.plazo / 365.0,
        row.df,
        zero_vasicek(r0, optimo[0], optimo[1], optimo[2], 0, row.plazo / 365)
    ))
compara_dfs = pd.DataFrame(v_curva, columns=['plazo', 'df_mkt', 'df_vasicek'])
fig = px.line(compara_dfs, x='plazo', y=['df_mkt', 'df_vasicek'], title='Compara Vasicek vs Mercado (plazo en años)')
fig.show()
```

Hagamos la misma comparación, pero en tasa:

```python
def tasa_comp(df: float, plazo: float) -> float:
    return (1 / df) ** (1 / plazo) - 1

compara_dfs['tasa_vasicek'] = compara_dfs.apply(lambda row: tasa_comp(row.df_vasicek, row.plazo), axis=1)
compara_dfs['tasa_mkt'] = compara_dfs.apply(lambda row: tasa_comp(row.df_mkt, row.plazo), axis=1)
```

```python
compara_dfs.head()
```

      plazo
      df_mkt
      df_vasicek
      tasa_vasicek
      tasa_mkt

      0
      0.002740
      0.999998
      0.999998
      0.000814
      0.000811

      1
      0.019178
      0.999984
      0.999984
      0.000832
      0.000841

      2
      0.038356
      0.999970
      0.999967
      0.000852
      0.000780

      3
      0.057534
      0.999955
      0.999950
      0.000872
      0.000774

      4
      0.090411
      0.999929
      0.999918
      0.000907
      0.000781

```python
fig = px.line(compara_dfs, x='plazo', y=['tasa_mkt', 'tasa_vasicek'], title='Compara Vasicek vs Mercado')
fig.show()
```

Veamos como queda la simulación:

```python
sim = vasicek_path(r0, optimo[0], optimo[1], optimo[2])
df_sim = pd.DataFrame(sim, columns=['t', 'tasa'])
fig = px.line(
    df_sim,
    x='t',
    y='tasa',
    title=f'Simulación Modelo Vasicek. r0={r0:.2%}, gamma={optimo[1]:.4f}, \
    r_={optimo[0]:.2%}, sigma={optimo[2]:.2%}'
)
fig.show()
```

```python
# x = [r_, gamma] valor inicial
def objective2(x: List[float], *args) -> float:
    """
    Calcula el error cuadrático total entre los valores de la curva de mercado y los obtenidos
    con la fórmula del modelo de Vasicek y los parámetros x.
    """
    r_ = x[0]
    gamma = x[1]

    df_tasas = args[0]
    r0 = args[1]
    sigma0 = args[2]

    t0 = 0

    error = 0.0
    for row in df_tasas.itertuples():
        # 0 es que estoy calibrando la curva a t = 0
        error += (row.df - zero_vasicek(r0, r_, gamma, sigma0, t0, row.plazo / 365)) ** 2
    return error
```

```python
x00 = [.03, 1]
sigma0 = .02
bnds = ((0, None), (0, None))
result = opt.minimize(objective2, x00, args=(df_curva, r0, sigma0), bounds=bnds)
print(result)
optimo2 = result.x
```

          fun: 0.0034946069945208007
     hess_inv: <2x2 LbfgsInvHessProduct with dtype=float64>
          jac: array([-1.74033964e-05, -3.93222781e-06])
      message: b'CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH'
         nfev: 219
          nit: 55
         njev: 73
       status: 0
      success: True
            x: array([0.02472949, 0.11358796])

```python
print(f'r_: {optimo2[0]:.4%}')
print(f'gamma: {optimo2[1]:.4f}')
print(f'sigma: {sigma0:.4%}')
```

    r_: 2.4729%
    gamma: 0.1136
    sigma: 2.0000%

```python
v_curva = []
for row in df_curva.itertuples():
    v_curva.append((
        row.plazo / 365.0,
        row.df,
        zero_vasicek(r0, optimo2[0], optimo2[1], sigma0, 0, row.plazo / 365)
    ))
compara_dfs_2 = pd.DataFrame(v_curva, columns=['plazo', 'df_mkt', 'df_vasicek'])
fig = px.line(compara_dfs_2, x='plazo', y=['df_mkt', 'df_vasicek'], title='Compara Vasicek vs Mercado')
fig.show()
```

```python
compara_dfs_2['tasa_mkt'] = compara_dfs_2.apply(lambda row: tasa_comp(row.df_mkt, row.plazo), axis=1)
compara_dfs_2['tasa_vasicek'] = compara_dfs_2.apply(lambda row: tasa_comp(row.df_vasicek, row.plazo), axis=1)
```

```python
fig = px.line(compara_dfs_2, x='plazo', y=['tasa_mkt', 'tasa_vasicek'], title='Compara Vasicek vs Mercado')
fig.show()
```

```python
sim = vasicek_path(r0, optimo2[0], optimo2[1], sigma0)
df_sim = pd.DataFrame(sim, columns=['t', 'tasa'])
fig = px.line(
    df_sim, x='t',
    y='tasa',
    title=f'Simulación Modelo Vasicek. r0={r0:.2%}, gamma={optimo2[1]:.4f}, r_={optimo2[0]:.2%}, sigma={sigma0:.2%}'
)
fig.show()
```

## Fórmulas para Calls y Puts

Consideremos una opción con el siguiente payoff (una opción call sobre un bono cero cupón):

$$
\begin{equation}
Call\left(r_{T_O},T_O\right)=\max\left[Z\left(r_{T_O},T_O,T_B\right)-K,0\right]
\end{equation}
$$

donde $T_O$ representa la fecha de vencimiento de la opción, $T_B$ la fecha de vencimiento del bono y $r_{T_O}$ es la tasa instantánea al vencimiento de la opción.

Tenemos que, bajo el modelo de Vasicek, el valor de esta call cuando $t=0$ está dado por:

$$
\begin{equation}
Call\left(r_0,0\right)=Z\left(r_0,0,T_O\right)\left[\frac{Z\left(r_0,0,T_B\right)}{Z\left(r_0,0,T_O\right)}N\left(d_1\right)-KN\left(d_2\right)\right]
\end{equation}
$$

$$
\begin{equation}
d_1=\frac{\log\left(\frac{Z\left(r_0,0,T_B\right)}{KZ\left(r_0,0,T_O\right)}\right)+\frac{1}{2}S_Z\left(T_O\right)^2}{\sqrt{S_Z\left(T_O\right)}}
\end{equation}
$$

$$
\begin{equation}
d_2=d_1-S_Z\left(T_O\right)
\end{equation}
$$

$$
\begin{equation}
S_Z\left(T_O\right)=B\left(T_O,T_B\right)\sqrt{\frac{\sigma^2}{2\gamma^*}\left(1-\exp\left(-2\gamma^* T_O\right)\right)}
\end{equation}
$$

d1 = (ln(F/K) + 1/2 sigma^2 * T) / (sigma x raiz(T))

INT(0, inf) max(S-K,0) dS

Como se calcula la INT, recordando que S es lognormal y por lo tanto tiene una densidad que se mete dentro de la INT y se resuelve.

### Ejercicio

¿Qué es $S_Z\left(T_O\right)$?

Es la volatilidad del retorno (logarítimico) del bono cupón cero que vence en $T_B$ desde $t=0$ hasta $t=T_O$.

Análogamente para una opción put sobre un bono cero cupón tenemos que:

$$
\begin{equation}
Put\left(r_{T_O},T_O\right)=\max\left[K-Z\left(r_{T_O},T_O,T_B\right),0\right]
\end{equation}
$$

Y su valor según el modelo de Vasicek está dado por:

$$
\begin{equation}
Put\left(r_0,0\right)=Z\left(r_0,0,T_O\right)\left[KN\left(-d_2\right)-\frac{Z\left(r_0,0,T_B\right)}{Z\left(r_0,0,T_O\right)}N\left(-d_1\right)\right]
\end{equation}
$$

y los mismos valores anteriores para $d_1$ y $d_2$.

## Fórmulas para Caps y Floors

Veamos como podemos utilizar la fórmula para valorizar opciones sobre bonos cupón cero para valorizar caps y floors.

Sea $L\left(T-\Delta,T\right)$ la tasa Libor fijada en $T-\Delta$ y que vence en $T$. El valor presente del payoff de un caplet que cuyo fixing se produce en $T-\Delta$ está dado por ($N$ es el nocional del caplet):

$$
\begin{equation}
PV_{T_\Delta}\left(payoff\right)=\frac{N}{1+L\left(T-\Delta,T\right)\Delta}\Delta\max\left[L\left(T-\Delta,T\right)-L_K,0\right] \\
=\frac{N}{1+L\left(T-\Delta,T\right)\Delta}\max\left[\left(1+L\left(T-\Delta,T\right)\Delta\right)-\left(1+L_K\Delta\right),0\right]
\end{equation} \\
=N\max\left[1-\frac{1+L_K\Delta}{1+L\left(T-\Delta,T\right)\Delta}\right] \\
=N\left(1+L_K\Delta\right)\max\left[\frac{1}{1+L_K\Delta}-\frac{1}{1+L\left(T-\Delta,T\right)\Delta}\right]
$$

Notamos entonces que el caplet es equivalente a una **put** sobre el bono $Z\left(T−\Delta,T\right)$ con strike igual a $\frac{1}{\left(1+L_K\Delta\right)}$ y nocional igual a $N\left(1+L_K\Delta\right)$. Por lo tanto, en el modelo de Vasicek, un caplet de strike $L_K$ , vencimiento $T-\Delta$ y nocional igual a $N$ está dado por:

$$
\begin{equation}
V\left(r_0,0)\right)=MZ\left(r_0,0,T-\Delta\right)\left[KN\left(-d_2\right)-\frac{Z\left(r_0,0,T\right)}{Z\left(r_0,0,T-\Delta\right)}N\left(-d_1\right)\right]
\end{equation}
$$

$$
\begin{equation}
M=N\left(1+L_{K}\Delta\right) \\
K=\frac{1}{1+L_{K}\Delta}
\end{equation}
$$

$$
\begin{equation}
d_1=\frac{\log\left(\frac{Z\left(r_0,0,T\right)}{KZ\left(r_0,0,T-\Delta\right)}\right)+\frac{1}{2}S_Z\left(T-\Delta\right)^2}{\sqrt{S_Z\left(T-\Delta\right)}}
\end{equation}
$$

$$
\begin{equation}
d_2=d_1-S_Z\left(T-\Delta\right)
\end{equation}
$$

$$
\begin{equation}
S_Z\left(T-\Delta\right)=B\left(T-\Delta,T\right)\sqrt{\frac{\sigma^2}{2\gamma^*}\left[1-\exp\left(-2\gamma^* \left(T-\Delta\right)\right)\right]}
\end{equation}
$$


# Electrode State of Health

This notebook demonstrates some utilities to work with electrode State of Health (also sometimes called electrode stoichiometry), using the algorithm from Mohtat et al [1]

[1] Mohtat, P., Lee, S., Siegel, J. B., & Stefanopoulou, A. G. (2019). Towards better estimability of electrode-specific state of health: Decoding the cell expansion. Journal of Power Sources, 427, 101-111.

```python
%pip install pybamm -q
import pybamm
import matplotlib.pyplot as plt
import numpy as np
```

    WARNING: You are using pip version 21.0.1; however, version 21.1 is available.
    You should consider upgrading via the '/Users/vsulzer/Documents/Energy_storage/PyBaMM/.tox/dev/bin/python -m pip install --upgrade pip' command.
    Note: you may need to restart the kernel to use updated packages.

## Create and solve model

```python
spm = pybamm.lithium_ion.SPM()
experiment = pybamm.Experiment([
    "Charge at 1C until 4.2V",
    "Hold at 4.2V until C/50",
    "Discharge at 1C until 2.8V",
    "Hold at 2.8V until C/50",
])
parameter_values = pybamm.ParameterValues(chemistry=pybamm.parameter_sets.Mohtat2020)

sim = pybamm.Simulation(spm, experiment=experiment, parameter_values=parameter_values)
spm_sol = sim.solve()
spm_sol.plot([
    "Terminal voltage [V]",
    "Current [A]",
    "Negative electrode SOC",
    "Positive electrode SOC",
])
```

    interactive(children=(FloatSlider(value=0.0, description='t', max=2.324130562944902, step=0.02324130562944902)…

    <pybamm.plotting.quick_plot.QuickPlot at 0x146bb7580>

## Solve for electrode SOH variables

Given a total amount of lithium, $n_{Li}$, electrode capacities, $C_n$ and $C_p$, and voltage limits, $V_{min}$ and $V_{max}$, we can solve for the min and max electrode SOCs, $x_0$, $x_{100}$, $y_0$, and $y_{100}$,  and the cell capacity, $C$, using the algorithm from Mohtat et al [1].
First, we find $x_{100}$ and $y_{100}$ using
$$
\begin{align}
n_{Li} &= \frac{3600}{F}(y_{100}C_p + x_{100}C_n),
\\
V_{max} &= U_p(y_{100}) - U_n(x_{100}).
\end{align}
$$
Then, we find $C$ using
$$
\begin{align}
V_{min} &= U_p(y_{0}) - U_n(x_{0})
\\
&= U_p\left(y_{100} + \frac{C}{C_p}\right) - U_n\left(x_{100} - \frac{C}{C_n}\right)
\end{align}
$$
Finally, $x_0$ and $y_0$ are simply defined as
$$
\begin{align}
x_0 &= x_{100} - \frac{C}{C_n},
\\
y_0 &= y_{100} + \frac{C}{C_p}.
\end{align}
$$
We implement this in pybamm as an algebraic model.

```python
param = pybamm.LithiumIonParameters()

Vmin = 2.8
Vmax = 4.2
Cn = parameter_values.evaluate(param.C_n_init)
Cp = parameter_values.evaluate(param.C_p_init)
n_Li = parameter_values.evaluate(param.n_Li_particles_init)

Un = param.U_n_dimensional
Up = param.U_p_dimensional
T_ref = param.T_ref
```

```python
model = pybamm.BaseModel()
x100 = pybamm.Variable("x100")
C = pybamm.Variable("C")

y100 = (n_Li * param.F / 3600 - x100 * Cn) / Cp
x0 = x100 - C/Cn
y0 = y100 + C/Cp

model.algebraic = {
    x100: Up(y100, T_ref) - Un(x100, T_ref) - Vmax,
    C: Up(y0, T_ref) - Un(x0, T_ref) - Vmin,
}
model.initial_conditions = {
    x100: 0.9,
    C: Cp,
}
model.variables = {
    "x_100": x100,
    "y_100": y100,
    "C": C,
    "x_0": x0,
    "y_0": y0,
}

sim = pybamm.Simulation(model, parameter_values=parameter_values)
sol = sim.solve([0])
for var in ["x_100", "y_100", "C", "x_0", "y_0"]:
    print(var, ":", sol[var].data[0])
```

    x_100 : 0.8334162315444249
    y_100 : 0.03350230659015535
    C : 4.969342722024421
    x_0 : 0.0014851546252120373
    y_0 : 0.8909223888800355

This model is implemented in PyBaMM as the `ElectrodeSOH` model

```python
model = pybamm.lithium_ion.ElectrodeSOH()
sim = pybamm.Simulation(model, parameter_values=parameter_values)
esoh_sol = sim.solve(
    [0],
    inputs={ "V_min": Vmin, "V_max": Vmax, "C_n": Cn, "C_p": Cp, "n_Li": n_Li},
)
for var in ["x_100", "y_100", "C", "x_0", "y_0"]:
    print(var, ":", esoh_sol[var].data[0])
```

    x_100 : 0.8334162315444252
    y_100 : 0.033502306590155045
    C : 4.969342721609116
    x_0 : 0.0014851546947397543
    y_0 : 0.8909223888083776

## Check against simulations

Plotting the SPM simulations against the eSOH calculations validates the min/max stoichiometry calculations

```python
t = spm_sol["Time [h]"].data
x_spm = spm_sol["Negative electrode SOC"].data
y_spm = spm_sol["Positive electrode SOC"].data

x_0 = esoh_sol["x_0"].data * np.ones_like(t)
y_0 = esoh_sol["y_0"].data * np.ones_like(t)
x_100 = esoh_sol["x_100"].data * np.ones_like(t)
y_100 = esoh_sol["y_100"].data * np.ones_like(t)

fig, axes = plt.subplots(1,2)

axes[0].plot(t, x_spm, "b")
axes[0].plot(t, x_0, "k:")
axes[0].plot(t, x_100, "k:")
axes[0].set_ylabel("x")

axes[1].plot(t, y_spm, "r")
axes[1].plot(t, y_0, "k:")
axes[1].plot(t, y_100, "k:")
axes[1].set_ylabel("y")

for k in range(2):
    axes[k].set_xlim([t[0],t[-1]])
    axes[k].set_ylim([0,1])
    axes[k].set_xlabel("Time [h]")

fig.tight_layout()

```

## References

The relevant papers for this notebook are:

```python
pybamm.print_citations()
```

    [1] Joel A. E. Andersson, Joris Gillis, Greg Horn, James B. Rawlings, and Moritz Diehl. CasADi – A software framework for nonlinear optimization and optimal control. Mathematical Programming Computation, 11(1):1–36, 2019. doi:10.1007/s12532-018-0139-4.
    [2] Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, and others. Array programming with NumPy. Nature, 585(7825):357–362, 2020. doi:10.1038/s41586-020-2649-2.
    [3] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. An asymptotic derivation of a single particle model with electrolyte. Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019. doi:10.1149/2.0341915jes.
    [4] Peyman Mohtat, Suhak Lee, Jason B Siegel, and Anna G Stefanopoulou. Towards better estimability of electrode-specific state of health: decoding the cell expansion. Journal of Power Sources, 427:101–111, 2019.
    [5] Peyman Mohtat, Suhak Lee, Valentin Sulzer, Jason B. Siegel, and Anna G. Stefanopoulou. Differential Expansion and Voltage Model for Li-ion Batteries at Practical Charging Rates. Journal of The Electrochemical Society, 167(11):110561, 2020. doi:10.1149/1945-7111/aba5d1.
    [6] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.
    [7] Pauli Virtanen, Ralf Gommers, Travis E. Oliphant, Matt Haberland, Tyler Reddy, David Cournapeau, Evgeni Burovski, Pearu Peterson, Warren Weckesser, Jonathan Bright, and others. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods, 17(3):261–272, 2020. doi:10.1038/s41592-019-0686-2.

```python

```


**Ritz-Galerkin method** - Visualisation of approximate solutions given different sets of approach functions
T. Mirbach, 15.06.2020

```python
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

from sympy.parsing.sympy_parser import parse_expr
from sympy.plotting import plot
from sympy import symbols
from sympy import init_printing
init_printing()
from sympy import *

import rg_algorithm as rg
```

Enter DEQ here: add right hand side f in between the quotes. You might use exp(x), sin(x), cos(x)

For the boundary conditions, np.exp(x) etc.
If condition i is on u, p_i is False. Its True for a condition on u'.

```python
#inhomogeneous part
f = parse_expr("74*cos(3*x) + x**2")
#Lu = a2*u'' + a1*u' + a0*u, (a2 = 1)
a2 = 4
a1 = 0
a0 = -1
#boundary conditions: False - on u; True - on u'
x0 = -np.pi
y0 = 10
p0 = False
x1 = np.pi
y1 = 10
p1 = False
```

Enter approach functions v_i and sets of them V_i
p_i are the desired plots.

Labels of the plot are rounded on two digits.

```python
#choice of approach functions
v0 = parse_expr("x**2 - pi**2")
v1 = parse_expr("cos(3*x)+1")
v2 = parse_expr("x")
v3 = parse_expr("x**2-x")
v4 = parse_expr("cos(2*x)-1")
v5 = parse_expr("x-5")
v6 = parse_expr("sin(x)")
v7 = parse_expr("x**3")
v8 = parse_expr("x**2")
v9 = parse_expr("cos(x)-1")
V0 = [v0]
V = [v1]
V1 = [v1,v0]
V2 = [v2]
V3 = [v3]
V4 = [v2,v3,v7]
V5 = [v8]
V6 = [v2,v8,v9]

plt.figure(figsize=(18,5))

#find exact solution
u = Function('u')
x = Symbol('x')
real_sol = rg.exact(u,x,a2*u(x).diff(x,2)+a1*u(x).diff(x,1) + a0*u(x)-f, x0,y0,p0,x1,y1,p1,a2)
pr = plot(real_sol, (x,x0,x1),show = False, line_color = 'green', legend = True)
pr[0].label = "$"+latex(rg.pretty_label(real_sol))+"$ (exact)"

f_1,p_1 = rg.ritz_galerkin(x,a1,a0,x0,y0,p0,x1,y1,p1,f,V2,a2=a2)
#f_2,p_2 = rg.ritz_galerkin(x,a1,a0,x0,y0,p0,x1,y1,p1,f,V,col ='blue',a2=a2)
#f_3,p_3 = rg.ritz_galerkin(x,a1,a0,x0,y0,p0,x1,y1,p1,f,V1,col ='yellow',a2=a2)

pr.append(p_1[0])
#pr.append(p_2[0])
#pr.append(p_3[0])

pr.show() # show plot with everything inside

```

```python
#Example 3.2.3
#inhomogeneous part
f = parse_expr("5+8*x - 2*x**2")
#Lu = a2*u'' + a1*u' + a0*u, (a2 = 1)
a2 = 1
a1 = 2
a0 = -1
#boundary conditions: False - on u; True - on u'
x0 = 0
y0 = -1
p0 = False
x1 = 1
y1 = 1
p1 = False
```

```python
#Exercise 3.2.4
#inhomogeneous part
f = parse_expr("2*exp(x)")
#Lu = a2*u'' + a1*u' + a0*u, (a2 = 1)
a2 = 1
a1 = 0
a0 = 1
#boundary conditions: False - on u; True - on u'
x0 = 0
y0 = 1
p0 = False
x1 = 2
y1 = np.exp(2)
p1 = True
```

    7.38905609893065

```python

```

```python

```

```python

```


# `DSML Workshop 07` - Introduction to ML

In this workshop we start with hands-on machine learning focusing mostly on the linear regression example covererd previously in the lecture.

Machine learning is a fast-growing field. Recent popularity of machine learning as a topic can largely be explained due to a combination of three different elements:
1. Gains in computing power (machine learning, as it grew out of computer science, has always been fundamentally concerned with computational algorithms)
1. Massive amounts of available data (the "raw materials" for machine learning methods)
1. Some notable algorithmic advances that have occurred over the last 30 years.

To start, though, we will cover some of the most basic algorithms to codify the underlying principles.

### Example: predicting peak electrical power

Let's consider the above problem a bit more formally, starting with a simple
example.  Suppose you want to predict what the peak electricity demand will be
during the day tomorrow for some area (we'll consider data from the area
surrounding Pittsburgh, PA).  This is actually a very important problem from a
logitics planning perspective: electricity generators, which for the most part
are based upon boiling water to move turbines, cannot turn on instantly, so in
order to guarantee that we have enough power to supply a given area, a system
operator typically needs to have some excess generation always waiting in the
wings.  The better we can forecast future demand, the smaller our excess margin
can be, leading to increased efficiency of the entire electrical grid.
The power consumption tomorrow depends on many factors: temperature, day of
week, season, holiday events, etc, not to mention some inherrent randomness
that we don't expect to even predict with perfect accuracy.  However, even for
someone working in the area, it would be very
difficult to come up with a model for electrical demand based soley upon "first
principles", thinking about the nature of electricity consumption or the devices
people may use, in an attempt to predict future consumption.

What _is_ easy, however, is simply to collect lots of data about past energy
consumption (the system operator serving the Pittsburgh region, PJM, maintains
a data set available [here](https://dataminer2.pjm.com/feed/hrl_load_metered/definition))
as well as the past factors that affect consumption, like the past weather for
the area. The files we are loading are the raw files we downloaded from this source. The final input data for our code is `Pittsburgh_load_data.csv`.

```python
import numpy as np
import matplotlib.pyplot as plt
from pandas.plotting import register_matplotlib_converters
register_matplotlib_converters()
import seaborn as sns
import pandas as pd
from sklearn.linear_model import LinearRegression
import datetime
```

**Exercise** Import the data and have a first look.

If you have time left, also convert the 'Date' column to datetime.
Hint: the pandas function `to_datetime()` might be useful

```python
## Your Code goes here
```

```python
## Target solution 5 min
df = pd.read_csv("Pittsburgh_load_data.csv")
df["Date"] = pd.to_datetime(df["Date"],format="%d.%m.%Y")
df = df.sort_values("Date")
df.describe()
```

      AVG
      MAX
      MIN
      Total
      High_temp
      Avg_temp

      count
      1825.000000
      1825.000000
      1825.000000
      1825.000000
      1825.000000
      1825.000000

      mean
      1.625500
      1.864475
      0.001625
      39.007871
      16.247726
      11.322263

      std
      0.222458
      0.312973
      0.000222
      5.348285
      10.898529
      10.452796

      min
      1.187175
      1.316033
      0.001187
      28.494714
      -15.600000
      -40.790000

      25%
      1.455030
      1.628122
      0.001455
      34.917959
      7.800000
      3.260000

      50%
      1.586174
      1.798936
      0.001586
      38.071560
      18.300000
      13.100000

      75%
      1.773888
      2.048079
      0.001774
      42.577061
      25.600000
      20.330000

      max
      2.465262
      2.951464
      0.002465
      59.171705
      33.900000
      27.610000

Today we want to build a simple ML model to forecast peak demand.
Using the converted Date column, we can have a look at the interaction between the date and the peak demand, which can be found under the 'MAX' column.

```python
## Create a plot, plotting the date on the x-axis and the peak demand on the y-axis
```

```python
## Target solution 5 min
fig, ax = plt.subplots(figsize = (16,9))

# Plotting the data
ax.plot(df["Date"],df[["MAX"]])
ax.set_xlabel("Date")
ax.set_ylabel("Peak Demand (GW)")
plt.show()
```

**Exercise**: In the next steps we want to look at the summer months only. Create a `df_summer` which only contains the months of June, July and August. Hint: use the `dt.month` function to extract the month from the datetime object.

```python
## Create a dataframe df_summer which only contains the months of June, July and August

```

```python
## Target solution 5 min

# create month feature
df["Month"] = df["Date"].apply(lambda dt: dt.month)
test = df['Date'].dt.month

# create df_summer
summer_month=[6,7,8]
df_summer = df[df["Month"].isin(summer_month)==True]
df_summer["Month"].max()
```

    8

```python
# let us define our dependent (y) and independent (x) variables for peak electricity load prediction

xp = df_summer['High_temp']

yp = df_summer['MAX']

#min_H_temp, max_H_temp = xp.min(), xp.max()
```

```python
fig, ax = plt.subplots(figsize = (8,6))
# Plotting the data
ax.scatter(xp, yp, marker='x',)
ax.set_xlabel("High Temperature (°C)")
ax.set_ylabel("Peak Demand (GW)")

plt.show()
#saving figures (You can comment-out this line inorder not to save figures)
#plt.savefig('summer data_peak demand.pdf')
```

As it turns out, electricity consumption in the summer in Pittsburgh is largely
driven by air conditioning, so with increasing high temperature comes increasing
electrical demand.  Thus, we may hypothesize that we can form a fairly good prediction of the peak demand using a linear model: that is, we hypothesize that
\begin{equation}
\mathrm{PeakDemand} \approx \theta_1 \cdot \mathrm{HighTemperature} + \theta_2
\end{equation}
where $\theta_1$ is the slope of the line and $\theta_2$ is the intercept term (together called the _parameters_ of the model).

For example, just eyeballing the data we might guess that the slope for peak demand data and average demand data is approximately $\theta_1 = 0.07$ (we get an increase of 0.07 GW per degree C, which we just arrived at by seeing that the power increased, very approximately, by noticing a total of ~1 GW increase in the range of 14 to 28 degree).  If we further suppose that the average peak demand is about 2.1GW at 26 degrees C, then we can solve for the intercept term by $0.07*26 + \theta_2 = 2$, or $\theta_2 = 0.28$.  Note: if this seems ad-hoc to you, don't worry, it is! We will shortly decribe to actually find good values for these parameters in a disciplined way.

```python
# define theta vector
theta = np.array([0.1, -0.5])

# set up plot
fig, ax = plt.subplots(figsize = (8,6))

# add objects to axis
ax.scatter(xp, yp, marker='x')
xlimp, ylimp =(plt.gca().get_xlim(), plt.gca().get_ylim())  #gets limity of x and y
ax.plot(xlimp, [theta[0]*xlimp[0]+theta[1], theta[0]*xlimp[1]+theta[1]], 'C1')
ax.set_xlim(xlimp)
ax.set_ylim(ylimp)
ax.set_xlabel("High Temperature (°C)")
ax.set_ylabel("Peak Demand (GW)")
plt.show()
#plt.savefig('summer data_peak demand_line.pdf')
```

Naturally, this model won't fit the data exactly (we can see from the chart that the figure doesn't lie precisely on an exact line), but if we can find slope and intercept terms that fit the data well, then for example, if we want to know what the peak demand will be tomorrow we can simply plug in the forecasted high temperature into the equation above get an estimate of the peak demand tomorrow (ignore the fact, for now that the high temperature tomorrow is also a prediction, we'll assume we just get this from a reliable source, and domains like weather forecasting are extremely well-studied in practice). This is of course equivalent to just "finding a point on the line".

### Finding good parameters

The question, of course, is how we find "good" values for $\theta_1$ and $\theta_2$ that fit this data well, i.e., so that the line fits the data as "closely" as possible.  The method we will describe for doing this (which is called _gradient descent_) is probably not the simplest algorithm for finding this fit.  In fact, as we will see, there is a very simple closed-form expression that will immediately give us the same solution for the framework we consider here.  But gradient descent is an _extremely_ powerful and general algorithm (and _is_ actually quite simple compared to some alternative approaches), and it is no exaggeration to say that gradient descent underlies virtually all modern machine learning.  So, with these caveats in place, let's dive in to understanding how we find "good" parameters $\theta_1$ and $\theta_2$ in some disciplined manner.

**Objective functions**  In order to find good values for the parameters, we need to formally define what "good" means in this setting.  This will actually be one of the key questions for machine learning algorithms in general, and difference notions of goodness lead to different algorithms.  Fortunately, there are some very well-studied definitions in this context, and so we have some "standard" options that we can try.  The notion that we will consider here captures the idea of the "squared error" between the prediction and the actual values.  That is, we consider all the days in the plot above, where $\mathrm{HighTemperature}^{(i)}$ denotes the high temperature and $\mathrm{PeakDemand}^{(i)}$ denotes the peak demand on day $i$.  Since _predicted_ peak demand for day $i$ is equal to
\begin{equation}
\theta_1 \cdot \mathrm{HighTemperature} + \theta_2
\end{equation}
we want to make this quantity as close as possible, averaged over all the days, to the true $\mathrm{PeakDemand}^{(i)}$.  We're going to measure this closeness in terms of the squared difference between the predicted and actual more.  More formally, we would like to minimize the quantity:
\begin{equation}
\frac{1}{\# \mathrm{days}} \sum_{i \in \mathrm{days}} \left ( \theta_1 \cdot \mathrm{HighTemperature}^{(i)} + \theta_2 - \mathrm{PeakDemand}^{(i)} \right )^2 \equiv E(\theta)
\end{equation}
which we abbreviate as $E(\theta)$ to emphasize the fact that we are going to be minimizing this error by tuning our $\theta$ variables.  This is known as the _objective function_ that we are trying to minimize.  A natural question that you may be asking is: why did we choose to measure closeness using this squared difference?  Why not use the average of absolute difference?  Or the maximum absolute difference?  These are good questions, and we'll defer answering them for now, except to say that we will definitely consider other possibilities later.  The squared error is simply a very common choice, mainly for reasons of mathematical convenience.

**Data scaling**  We'll shortly see what the gradient descent procedure looks like in our example above.  Before we apply the algorithm, though, we're going to make one small modification to our problem, and _normalize_ the data (both the inputs and the output) before we attempt to run the gradient descent algorithm.  We will see the reason for this more clearly shortly, but the brief reason is that if our slope and intercept terms are on very different "scales" (which would be the case here, because of the relatively large input values (units of degrees Celsius) compared to the output values (units of gigawatts)), then we would actually need to take very different step sizes in the two parameters $\theta_1$ and $\theta_2$.  This is possible to manually tune in our case, but when we start having many more parameters, it's not feasible.  We thus make our life much easier if we scale all the input and output data to be in the same rough range _before_ running gradient descent (Note: in class we talked about only scaling the input, which also happens to work fine here, but I think it may be easier conceptually to consider the case where we just normalize all the inputs and outputs in the same manner).

We can re-scale data in a number of ways, but a simple strategy is just to translate and scale the coordinates such that the values vary between zero and one in our dataset.  This can be easily achieved by the transformation
\begin{equation}
\tilde{x}^{(i)} = \frac{x^{(i)} - \min_i x^{(i)}}{\max_i x^{(i)} - \min_i x^{(i)}}
\end{equation}
and similarly for $\tilde{y}^{(i)}$.

**Exercise:** Re-scale input and output features using the formula shown above and plot the data. Call the normalized data vectors `x_nor` and `y_nor`.

```python
# YOUR CODE HERE

# 0-1 scaling: substract min, divide by range

x_nor = (xp-min(xp))/(max(xp)-min(xp))

y_nor = (yp-min(yp))/(max(yp)-min(yp))

```

```python
# set up plot
fig, axes = plt.subplots(1,2,figsize = (16,9))

axes[0].scatter(xp, yp, marker='x')
axes[0].set_xlabel("Re-scaled Temperature")
axes[0].set_ylabel("Re-scaled Demand")

axes[1].scatter(x_nor, y_nor, marker = 'x')
axes[1].set_xlabel("Re-scaled Temperature")
axes[1].set_ylabel("Re-scaled Demand")

plt.show()
```

This look identical to the previous plot, of course, except that the units no longer correspond to traditional quantities like degrees Celsius or gigawatts, but just some linear transformation of these units.

### Visualizing gradient descent

Now let's look at the gradient descent algorithm, which we have derived mathematically in previous lectures.  This will initialize $\theta_1$ and $\theta_2$ to zero and repeatedly update them according to the partial derivative rules.  We will use the step size (also known as learning rate) $\alpha=1$, and print out the value of $\theta$ per each iteration.

\begin{equation}
\theta_j  := \theta_j  − \alpha \sum_{j=1}^m (\sum_{i=1}^n \theta_j x_j^{(i)} - y^{(i)})x_j^{(i)}
\end{equation}

Note: The above simplified equation omits the number of days (n) and the *2 term which results from partial differentiation and groups them into the alpha term for easier representation.

```python
theta = np.array([0., 0.])

alpha = 1.0

for t in range(20):
    print("Iteration {}: ".format(t), theta)

    # partial derivative theta1: 2*sum(((theta1*x + theta2)-y)*x)
    # term 2/N added here (usully included in alpha term), this is just a constant so could also be omitted
    theta[0] -= alpha/len(xp) * 2 * sum((theta[0] * x_nor + theta[1] - y_nor)*x_nor)

    # partial derivative theta2: 2*sum(((theta1*x + theta2)-y))
    # term 2/N added here (usully included in alpha term), this is just a constant so could also be omitted
    theta[1] -= alpha/len(xp) * 2 * sum((theta[0] * x_nor + theta[1] - y_nor) ) # this is the intercept with a slightly different partial derivative
```

    Iteration 0:  [0. 0.]
    Iteration 1:  [0.63963085 0.14465722]
    Iteration 2:  [0.57350762 0.08188867]
    Iteration 3:  [0.63955769 0.06285916]
    Iteration 4:  [0.67479548 0.0382493 ]
    Iteration 5:  [0.71149952 0.01740394]
    Iteration 6:  [ 0.74380061 -0.00175318]
    Iteration 7:  [ 0.77323293 -0.01904579]
    Iteration 8:  [ 0.79984928 -0.03471553]
    Iteration 9:  [ 0.82395824 -0.04890293]
    Iteration 10:  [ 0.84578837 -0.06175052]
    Iteration 11:  [ 0.86555657 -0.07338436]
    Iteration 12:  [ 0.88345729 -0.08391922]
    Iteration 13:  [ 0.89966702 -0.0934589 ]
    Iteration 14:  [ 0.91434548 -0.1020974 ]
    Iteration 15:  [ 0.92763732 -0.10991985]
    Iteration 16:  [ 0.93967354 -0.11700336]
    Iteration 17:  [ 0.95057275 -0.12341771]
    Iteration 18:  [ 0.96044236 -0.12922612]
    Iteration 19:  [ 0.96937963 -0.13448584]

Let's visualize what this looks like in a couple different ways.  First, let's look at what our line looks like during different iterations of gradient descent.  For this purpose, we'll wrap the above in a simple function that takes `iters` iterations of gradient descent (note that we can of course get all these plots within a single run of gradient descent, but we'll just use multiple calls to this function for illustration purposes).

```python
def gradient_descent(iters):
    theta = np.array([0., 0.])
    alpha = 1.0
    for t in range(iters):
        theta[0] -= alpha/len(xp) * 2 * sum((theta[0] * x_nor + theta[1] - y_nor)*x_nor)
        theta[1] -= alpha/len(xp) * 2 * sum((theta[0] * x_nor + theta[1] - y_nor) )
    return theta

def plot_fit(theta):

    Error = sum((theta[0]*x_nor + theta[1] - y_nor)**2) # simple least squares error, which underlies our OLS example

    # compute partial derivative (i.e. gradient) for theta 1 and 2
    def_theta1 = sum((theta[0] * x_nor + theta[1] - y_nor)*x_nor)
    def_theta2 = sum((theta[0] * x_nor + theta[1] - y_nor))

    # plot

    plt.figure(figsize = (8,6))
    plt.scatter(x_nor, y_nor, marker = 'x')
    plt.xlabel("Nomalized Temperature")
    plt.ylabel("Normalized Demand")
    xlim, ylim =(plt.gca().get_xlim(), plt.gca().get_ylim())

    plt.plot(xlim, [theta[0]*xlim[0]+theta[1], theta[0]*xlim[1]+theta[1]], 'C1')
    plt.xlim(xlim)
    plt.ylim(ylim)
    print('Theta = ', theta, 'Error = ',Error,'def_theta1 = ',def_theta1, 'def_theta2 = ', def_theta2 )
    #plt.savefig('gradient decent '+str(theta[0])+'.png', dpi = 300)
```

```python
plot_fit(gradient_descent(0))
```

```python
plot_fit(gradient_descent(1))
```

```python
plot_fit(gradient_descent(2))
```

```python
plot_fit(gradient_descent(3))
```

```python
plot_fit(gradient_descent(4))
```

```python
plot_fit(gradient_descent(5))
```

```python
plot_fit(gradient_descent(10))
```

```python
plot_fit(gradient_descent(100))
```

```python
plot_fit(gradient_descent(1000))
```

### Error versus iteration

We can also look at the average error versus iteration.

```python
def gradient_descent_err(iters):
    err = []
    theta = np.array([0., 0.])
    alpha = 1.0
    for t in range(iters):
        err.append(np.mean((theta[0] * x_nor + theta[1] - y_nor)**2))
        theta[0] -= alpha/len(xp) * 2 * sum((theta[0] * x_nor + theta[1] - y_nor)*x_nor)
        theta[1] -= alpha/len(xp) * 2 * sum((theta[0] * x_nor + theta[1] - y_nor) )
    return np.array(err)
plt.figure(figsize = (8,6))
plt.plot(np.arange(0,50), gradient_descent_err(50))
plt.xlabel("Iteration")
plt.ylabel("Average error")
plt.show()
```

### Getting the answer back in the original coordinates

Fortunately, we don't need to resort to solving the system in the original coordinates, we can simply solve on our normalized data and then find the corresponding equations for the original data.  Specifically, since our model gives the approximation
\begin{equation}
\begin{split}
\tilde{y} & \approx \tilde{x} \cdot \theta_1 + \theta_2\\
\Longrightarrow \;\; \frac{y-a}{b} & \approx \frac{x-c}{d} \cdot \theta_1 + \theta_2 \\
\Longrightarrow \;\; y-a & \approx ((x-c)\cdot \theta_1)/d)\cdot b + b \theta_2 \\
\Longrightarrow \;\; y & \approx x \cdot (b \theta_1/d) + b \theta_2 + a - c b \theta_1/d\\
\Longrightarrow \;\; y & \approx x \cdot \hat{\theta}_1 + \hat{\theta}_2\\
\end{split}
\end{equation}
where
\begin{equation}
a = \min_i y^{(i)}, \;\; b = \max_i y^{(i)} - \min_i y^{(i)}, \;\; c = \min_i x^{(i)}, \;\; d = \max_i x^{(i)} - \min_i x^{(i)},
\end{equation}
and where we define
\begin{equation}
\begin{split}
\hat{\theta}_1 & = b \theta_1/d \\
\hat{\theta}_2 & = b \theta_2 + a - c \cdot(b \theta_1/d).
\end{split}
\end{equation}
That might seem like a lot, but all it's saying is that there is an easy formula to convert between the solution we get for the normalized data and the unnormalized data.

### Getting the answer back in the original coordinates

Fortunately, we don't need to resort to solving the system in the original coordinates, we can simply solve on our normalized data and then find the corresponding equations for the original data.  Specifically, since our model gives the approximation
\begin{equation}
\begin{split}
\tilde{y} & \approx \tilde{x} \cdot \theta_1 + \theta_2\\
\Longrightarrow \;\; \frac{y-a}{b} & \approx \frac{x-c}{d} \cdot \theta_1 + \theta_2 \\
\Longrightarrow \;\; y-a & \approx ((x-c)\cdot \theta_1)/d)\cdot b + b \theta_2 \\
\Longrightarrow \;\; y & \approx x \cdot (b \theta_1/d) + b \theta_2 + a - c b \theta_1/d\\
\Longrightarrow \;\; y & \approx x \cdot \hat{\theta}_1 + \hat{\theta}_2\\
\end{split}
\end{equation}
where
\begin{equation}
a = \min_i y^{(i)}, \;\; b = \max_i y^{(i)} - \min_i y^{(i)}, \;\; c = \min_i x^{(i)}, \;\; d = \max_i x^{(i)} - \min_i x^{(i)},
\end{equation}
and where we define
\begin{equation}
\begin{split}
\hat{\theta}_1 & = b \theta_1/d \\
\hat{\theta}_2 & = b \theta_2 + a - c \cdot(b \theta_1/d).
\end{split}
\end{equation}
That might seem like a lot, but all it's saying is that there is an easy formula to convert between the solution we get for the normalized data and the unnormalized data.

```python
theta = gradient_descent(1000)
```

```python
theta
```

    array([ 1.05505079, -0.18490465])

**Exercise**: Using the above expression, re-scale the theta parameters to the original scale by creating a new array entitled `theta_hat`.

```python
theta[0]
```

    1.0550507949763712

```python
# YOUR CODE HERE

# define a, b, c, d

a= min(yp)
b= max(yp)-min(yp)
c= min(xp)
d= max(xp) - min(xp)

# use formula from above to re-scale

theta_1= b*theta[0]/d
theta_2= b*theta[1] + a -c*(b*theta[0]/d)

#combine into single array
theta_hat = np.array([theta_1,theta_2])

```

```python
theta_hat  # not that
```

    array([ 0.08385936, -0.08024741])

Finally we plot the derived linear regression line

```python
fig,ax = plt.subplots(figsize = (8,6))

ax.scatter(xp, yp, marker='x')

xlim, ylim =(plt.gca().get_xlim(), plt.gca().get_ylim())
ax.plot(xlim, [theta_hat[0]*xlim[0]+theta_hat[1], theta_hat[0]*xlim[1]+theta_hat[1]], 'C1')

ax.set_xlim(xlim)
ax.set_ylim(ylim)
ax.set_xlabel("High Temperature (°C)")
ax.set_ylabel("Peak Demand (GW)")
print(theta, theta_hat)
```

## `Libraries for machine learning in Python`

Finally, we conclude with some information about the types of libraries we will use to run machine learning algorithms in Python.  Although there are a number of machine learning packages available, by far the most popular Python library for general-purpose "classical" machine learning (this is in contrast to packages focused specficially on deep learning, such as [TensorFlow](http://www.tensorflow.org) and [Keras](https://keras.io/getting_started/)) is the [scikit-learn](http://scikit-learn.org/) library.  Scikit Learn is general purpose machine learning library with a number of common machine learning algorithms built in.

One important note, however, is that (despite some ongoing efforts to make it more scalable), scikit-learn is still best suited for small to medium-scale problems (say with ~10,000s of examples and ~1,000s of features).  For these size problems, most of the algorithms contained in the library will work reasonably fast, and the library has the advantage that one can train many different types of algorithms all with the same interface.  However, if you have data that is much bigger than this, then the algorithms start to get fairly slow compared to other more specialized libraries, and you are likely better off using an alternative library.

Another important caveat, and this is one that sadly often gets ignored, is that unlikely other software libraries, you _need_ to have some (even just basic) understanding of what the algorithms do in order to use scikit-learn effectively.  This is because virtually all algorithms will have some substantial number of hyperparameters, settings to the algorithm that can drastically effect performance (and really, affect _all_ the underlying aspects of the algorithm itself, the hypothesis, loss, and optimization problem).  Sadly, a surprisingly large number of the statements people make about data science techniques seem less about the actual algorithms and more about whatever default settings scikit-learn happens to have for each algorithm.  This is why you get people saying things like "support vector machines worked better than neural networks for this problem", which is a completely meaningless statement unless you know _what sort_ of support vector machine, and _what architecture_ neural network.  Maybe in 10 years we will be at a place where the ML algorithms truly are "self-contained", and practitioners don't need to know anything about the underlying algorithms to get good performance (certainly, some researchers and companies are attempting to develop tools that move in this direction).  But for the vast majority of tasks, we are still a lot ways away from this point, and you do absolutely need to understand the algorithms to get reasonable performance.

### Linear regression in scikit-learn

Let's look at how we specify a model, fit it to data, and make predictions in scikit-learn.  These three tasks form the common usage pattern for most interation with scikit-learn.  Let's first prepare our data.  Note that scikit-learn by default will fit a separate intercept term for linear regression models, so we don't include the all-ones entry in our features.

```python
X = df_summer["High_temp"].values.reshape((-1,1)) # if we pass a 1-feature array we need to re-shape it! This is not required for multi-dimenisonal arrays
y = df_summer["MAX"]
```

```python
df_summer["High_temp"].values.shape
```

    (459,)

Now we can import and initialize our model. In general, scikit-learn has a different class for each different type of learning algorithm. In this case, we are importing the LinearRegression class. When we initialize the class, we pass various parameters to the constructor. In this case, we are specifying that we will fit an intercept term (i.e., we will not pass it as an explicit feature), and that we will not normalize the data. These are in fact the default parameters of the linear regression class, but we include them explicitly just for illustration.

Note that `scikit-learn` also has a pre-implemented scaling functionality such as the `StandardScaler` class ([see here](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.StandardScaler.html)). For convenience we will not scale the data here. Gradient descent is still able to find an optimal solution. For higher dimensional inputs, however, scaling is highly recommended. We will cover this in future workshops!

```python
# initialize model
linear_model = LinearRegression(fit_intercept=True, normalize=False)
```

```python
linear_model
```

    LinearRegression(normalize=False)

Note that when we create this class we haven't actually passed any data to the system.  This is the standard interface for scikit-learn classes: the constructor just initializes the hyperparameters of the model, and when we actually want to fit it to data, we call the `model.fit()` function.

```python
# train model
linear_model.fit(X, y)
print(linear_model.coef_, linear_model.intercept_)
```

    [0.08385936] -0.08024740699315469

    /Users/philipppeter/Repo/DSML_2022/env/lib/python3.9/site-packages/sklearn/linear_model/_base.py:148: FutureWarning: 'normalize' was deprecated in version 1.0 and will be removed in 1.2. Please leave the normalize parameter to its default value to silence this warning. The default behavior of this estimator is to not do any normalization. If normalization is needed please use sklearn.preprocessing.StandardScaler instead.
      warnings.warn(

```python
# compare to own implementation (at 1000 iteration of gradient descent)
theta_hat
```

    array([ 0.08385936, -0.08024741])

Finally, when we want to make a prediction on a new data point, we call the model.predict() function, passing in the feature values for the new points we want to predict. In the following example, we would be predicting what the peak demand would be given a 25 degree peak temperature.

```python
linear_model.intercept_
```

    -0.0802474069931498

```python
# predict on new data
Xnew = np.array([[40]]) #predict peak load at 25 deg c

print(linear_model.predict(Xnew))
```

    [3.2741269]

Note that we can pass multiple points to predict at once.

```python
Xnew = np.array([[25],[23]])
print(linear_model.predict(Xnew))
```

    [2.01623653 1.84851782]

Using Scikit-Learn Library for fitting linear regression line:

```python
lr = LinearRegression()
lr.fit(xp.values.reshape(-1,1), yp)
model_pred = lr.predict(xp.values.reshape(-1,1))

fig,ax = plt.subplots(figsize = (8,6))

ax.scatter(xp, yp, marker="x")
ax.set_xlabel("High Temperature (°C)")
ax.set_ylabel("Peak Demand (GW)")

ax.plot(xp, model_pred, c='C2')
ax.legend(['Squared loss fit','Observed days'])
#plt.savefig('summer data_peak demand_line.pdf')
print(lr.coef_, lr.intercept_)
```

## `Regression Model Evaluation`

### Metrics

```python
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
```

```python
y_true = df_summer["MAX"]
y_pred = linear_model.predict(df_summer["High_temp"].values.reshape((-1,1)))
```

#### (Root) Mean Squared Error ((R)MSE)
The MSE calculates the mean of the squared residuals. As the fitting of the linear regression model is (typically) carried out by minimizing the squared error (see loss function in lecture slides), the MSE corresponds directly to the model optimization. The unit of the MSE is the target unit squared, i.e. in the above example using energy consumption in GW, the unit of the MSE is $(GW)^2$. As the residuals (the difference of prediction and observed values) are squared, large deviations are penalized stronger than small deviations. Therefore, the weight of outliers increases using MSE as a metric. This is useful in applications, where small prediction errors are not important, but large errors have to be avoided.

As the squared unit is hard to interpret, the RMSE can be used instead of the MSE. The RMSE is just the square root of the MSE, meaning that it is monotonic with respect to the MSE - a bigger MSE leads to a bigger RMSE and vice versa.

```python
print("Mean Squared Error:",mean_squared_error(y_pred, y_true),"(GW)^2")
print("Root Mean Squared Error:",mean_squared_error(y_pred, y_true)**0.5,"GW")
```

    Mean Squared Error: 0.02457070921042353 (GW)^2
    Root Mean Squared Error: 0.15675046797513406 GW

The RMSE can be easily interpreted as it can be set into relation with the c. 1.8 GW average demand mean. Generally, MSE and RMSE have always to be assessed given the model under evaluation. A MSE or RMSE with a certain value has no meaning unless the order of magnitude of the dataset is known (with an exception of the very unlikely case that the MSE is zero).

#### Mean Absolute Error (MAE)
Just as the RSME, the MAE is easy to interpret. It is just the mean absolute value of the error term. When there are no or little major outliers, RMSE and MAE often have the same order of magnitude, with the MAE always being smaller than the RMSE. While a linear regression is typically fitted using the least squares method, the MAE can still be a valuable metric. It is applicable when large errors are not disproportionately worse than smaller errors. For example, when prediciting monetary values, an error of 100 USD might always be twice as bad as an error of 50 USD and so on. If this is the case, the MAE can actually be the more suitable error metric. As the RSME (and the MAE for that matter), the MAE does only make sense when knowing the order of magnitude of the predicted values. On its own, it does not imply good or bad model fit.

```python
print("Mean Absolute Error:",mean_absolute_error(y_pred, y_true),"GW")
```

    Mean Absolute Error: 0.1259610051963046 GW

#### Coefficient of Determination ($R^2$)
The coefficient of determination (typically pronunced as "R squared"), is a metric of how well a model explains variance seen in the data. $R^2$ indicates the ratio of the explained and the overall variance (given some assumptions that we will not discuss here). Its value is always between 0 and 1. It can therefore be used as a means of comparison not only between regression methods, but also between completely different datasets - or even without knowing the data at all. For example, a value of 0.9 is always good, a value of 0.1 is always bad. Still, what threshold you would define as a good model fit depends on the domain of application. If you except a high degree of randomness in your data, it is harder to explain your variance using a predictor. If you expect your data to be highly deterministic, then it should be easily explainable using suitable features and prediction methods.

```python
print("Coefficient of determination:",r2_score(y_pred, y_true))
```

    Coefficient of determination: 0.6303586800437209

#### Other metrics
Scikit-learn does include other metrics that are not used as often as the ones explained above. To read up on those, just visit Scikit-learn's website. You can also define functions to calculate your own error metrics - depending on the application and with sound reasoning. E.g., it could make sense to weigh negative deviations higher than positive deviations, or the relative error instead of a form of absolute error could be decisive.

### References
[Model Evaluation](http://scikit-learn.org/stable/modules/model_evaluation.html)

__Note that the metrcis calculated above represent the training loss! They do not say anything about predictive performance!__

## End of Class Exercise

**Exercise 1**:

- Fit a linear regression by using `scikit-learn` library for average temperature and average demand data.
- Plot the data and fitted line. Annotate your graph and size it appropriately.
- Present relevant test metrics to quantify the __training__ loss

```python
# YOUR CODE HERE

# define target and feature vectors

Xavg = np.array(df_summer["Avg_temp"]).reshape(-1,1)
yavg = np.array(df_summer["AVG"])

# initialize lin model

lin_model = LinearRegression()

# fit model

lin_model.fit(Xavg,yavg)

# predict training data

y_pred_avg = lin_model.predict(Xavg)

# define y true

y_true_avg = yavg
```

```python
#yavg
```

```python
plt.figure(figsize = (12,8))

plt.scatter(Xavg, yavg, marker="x", label='Observerd Days')
plt.xlabel("Average Temperature (°C)")
plt.ylabel("Average Demand (GW)")

plt.plot(Xavg, y_pred_avg, c='C2', label='Squared loss fit')

xlim, ylim =(plt.gca().get_xlim(), plt.gca().get_ylim())
plt.xlim(xlim)
plt.ylim(ylim)
plt.legend()
plt.show()
```

```python
# Present test_metrics
print("Mean Absolute Error:",mean_absolute_error(y_pred_avg, y_true_avg),"GW")
print("Root Mean Squared Error:",mean_squared_error(y_pred_avg, y_true_avg)**0.5,"GW")
print("Coefficient of determination:",r2_score(y_pred_avg, y_true_avg))
```

    Mean Absolute Error: 0.09209366431417638 GW
    Root Mean Squared Error: 0.11341922005934792 GW
    Coefficient of determination: 0.6130420522714961

**Exercise 2**:

(we will cover this in detail in next weeks tutorial)

- Create an `IsWeekday` feature and examine its effect on peak demand and high temperature data.
- Using `Scikit learn` build a linear model that incorporates `IsWeekday` as an additional feature.
- Plot the data and fitted line. Annotate your graph and size it appropriately.
- Provide appropriate test metrics

```python
# YOUR CODE HERE

# see WS08 for solution

```

---


# Model selection using GpABC

## Problem formulation

This notebook illustrates how to perform [model selection](https://tanhevg.github.io/GpABC.jl/latest/overview-ms/) using the algorithm described in (Toni et al, 2009). The example here is adapted from the SIR (susceptible-infected-recovered) model in Section 3.3 of the [paper](https://royalsocietypublishing.org/doi/10.1098/rsif.2008.0172#d3e1991).

The example gives two models that describe the evolution of the number of of susceptible ($S$), infected ($I$) and recovered ($R$) individuals in a population (the number of dead individuals is not included in the model).

There are two possible models, both of which are described below. Given some observed data on the number of individuals in each disease state over time we wish to determine which of the two models is better supported by these data. The data was generated using the first model so the algorithm should reflect this.

### Model 1

A basic SIR model assumes that each individual can be infected once and is then able to infect other individuals immediately.

The model is given by

\begin{align}
    \dot{S} &= \alpha - \gamma S I - d S, \\
    \dot{I} &= \gamma S I - v I - d I, \\
    \dot{R} &= v I - d R \, ,
\end{align}

where the notation $\dot{x}$ denotes $\text{d}x/\text{d}t$.

The paramters of this model are the birth rate $\alpha$, the infection rate $\gamma$, the recovery rate $v$ and the death rate $d$. Note that the death rate is constant for all three disease states.

### Model 2

A slightly more sophisticated model includes a time delay between an individual becoming infected and being able to infect others by introducing an additional latent state ($L$). This model is given by

\begin{align}
    \dot{S} &= \alpha - \gamma S I - d S , \\
    \dot{L} &= \gamma S I - \delta L - d L , \\
    \dot{I} &= \delta L - v I - d I , \\
    \dot{R} &= v I - d R \, ,
\end{align}

where $\delta$ is the transition rate from latent to infected.

### References
- Toni, T., Welch, D., Strelkowa, N., Ipsen, A. and Stumpf, M.P., 2009. Approximate Bayesian computation scheme for parameter inference and model selection in dynamical systems. Journal of the Royal Society Interface, 6(31), pp.187-202. https://doi.org/10.1098/rsif.2008.0172

# To run this notebook locally:
* Make sure that [Jupyter](https://jupyter.org/) is installed, along with [Julia](https://www.julialang.org/) and its [Jupyter Kernel](https://github.com/JuliaLang/IJulia.jl).
* Clone or download [GpABC.jl](https://github.com/tanhevg/GpABC.jl) to your machine.
* Run `jupyter` from the `examples` directory of the local copy of `GpABC.jl`:
```bash
$ cd GpABC.jl/examples
$ jupyter notebook model-selection-example.ipynb
```
* The first line of the cell below activates the local project environment and downloads all the dependencies.

This notebook was tested under Julia 1.4

```julia
import Pkg; Pkg.activate(@__DIR__); Pkg.instantiate()
using GpABC, OrdinaryDiffEq, Distributions, Distances, Plots
pyplot()
```

     Activating environment at `~/gaussian_processes/GpABC.jl/examples/Project.toml`
    ┌ Info: Precompiling GpABC [e850a1a4-d859-11e8-3d54-a195e6d045d3]
    └ @ Base loading.jl:1260

    Plots.PyPlotBackend()

Prepare the observed data into the correct format: a 2D array with size (number of trajectories, number of time points).

The observed data contains information the number of susceptible, infected and recovered individuals at 13 time points.

```julia
times = [0.0, 0.6, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0]
data = [[ 20.     ,  10.     ,   0.     ],
        [  0.12313,  13.16813,   9.42344],
        [  0.12102,   7.17251,  11.18957],
        [  0.09898,   2.36466,  10.0365 ],
        [  0.37887,   0.92019,   6.87117],
        [  1.00661,   0.61958,   4.44955],
        [  1.20135,   0.17449,   3.01271],
        [  1.46433,   0.28039,   1.76431],
        [  1.37789,   0.0985 ,   1.28868],
        [  1.57073,   0.03343,   0.81813],
        [  1.4647 ,   0.28544,   0.52111],
        [  1.24719,   0.10138,   0.22746],
        [  1.56065,   0.21671,   0.19627]]

data = hcat(data...);
```

Next, we define the models. As in other `GpABC` functions, the model is represented using a function that takes parameters as an argument and returns trajectories that are the solution to the differential equation system. This is called the `simulator_function`. The solution should have the same shape as the reference data, which is (number of trajectories, number of time points).

This means that for the second model, which has 4 states, we need to modify the `simulator_function` in order to ignore the solution for the latent state $L$ as we have no reference data for that trajectory.

We also need to define different initial conditions for the two models as the first model has 3 states and the second has 4.

```julia
# Need initial conditions for each model since they have different numbers of states
ic1 = [20.0, 10.0, 0.0]
ic2 = [20.0, 0.0, 10.0, 0.0]
ics = [ic1, ic2]

# Define a simulator function for each model. Each function returns the solution of the model
# as a 2D array for some parameter values. The appropriate ODE is solved at the same time
# points as the observed data
function simulator1(params::Array{Float64,1})

    # p = (alpha, gamma, d, v)
    # x = (S, I, R)
    function model1(dx, x, p, t)
        dx[1] = p[1] - p[2]*x[1]*x[2] - p[3]*x[1] # dS/dt = alpha - gamma*S*I - d*S
        dx[2] = p[3]*x[1]*x[2] - p[4]*x[2] - p[3]*x[2] # dI/dt = gamma*S*I - v*I - d*I
        dx[3] = p[4]*x[2] - p[3]*x[3] # dR/dt = v*I - d*R
    end

    return(Array{Float64,2}(
        solve(ODEProblem(model1, ics[1], (times[1], times[end]), params),
                RK4(), saveat=times, force_dtmin=true)))
end

function simulator2(params::Array{Float64,1})

    # p = (alpha, gamma, d, v, delta)
    # x = (S, L, I, R)
    function model2(dx, x, p, t)
        dx[1] = p[1] - p[2]*x[1]*x[3] - p[3]*x[1] # dS/dt = alpha - gamma*S*I - d*S
        dx[2] = p[2]*x[1]*x[3] - p[5]*x[2] - p[3]*x[2] # dL/dt = gamma*S*I - delta*L - d*L
        dx[3] = p[5]*x[2] - p[4]*x[3] - p[3]*x[3] # dI/dt = delta*L - v*I - d*I
        dx[4] = p[4]*x[3] - p[3]*x[4] # dR/dt = v*I - d*R
    end

    # Model2 contains the species L, which is not measured - we remove it from the returned ODE solution
    # so that it can be compared to the reference data "data", which only contains S, I and R
    return(Array{Float64,2}(
        solve(ODEProblem(model2, ics[2], (times[1], times[end]), params),
                RK4(), saveat=times, force_dtmin=true)))[[1,3,4],:]
end;
```

We also need a set of priors for each model, as well as a summary statistic and threshold schedule. We will compare the full model solution with the data, which is the "`keep_all`" summary statistic. The model prior will be the default discrete uniform prior with span 2 (as there are two models).

```julia
#
# Priors and initial conditions - these are model-specfic as each model can
# have different numbers of parameters/species
#
priors1 = [Uniform(0.0, 5.0) for i in 1:4]
priors2 = vcat([Uniform(0.0, 5.0) for i in 1:4], Uniform(0.0, 10.0))
priors3 = vcat([Uniform(0.0, 5.0) for i in 1:4], Uniform(0.0, 10.0))

threshold_schedule = [20, 15, 10, 5, 3, 2.5, 2, 1.7, 1.5]
summary_statistic = "keep_all";
```

We are now ready to perform the model selection computation by calling [`SimulatedModelSelection`](https://tanhevg.github.io/GpABC.jl/latest/ref-ms/#GpABC.SimulatedModelSelection) and accepting 200 particles at each population (`n_particles = 200`).

First, we will do this using full model simulations:

```julia
n_particles = 200

ms_sim_result = SimulatedModelSelection(data,
    [simulator1, simulator2],
    [priors1, priors2],
    threshold_schedule,
    n_particles);
```

    ┌ Info: GpABC model selection simulation  Population 1 - ABC Rejection ϵ = 20.0
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:9
    ┌ Info: GpABC model selection simulation
    │ Completed 640 iterations, accepting 200 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 103	Model 2: 97
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:113
    ┌ Info: GpABC model selection simulation  Population 2 - ABC SMC ϵ = 15.0
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:24
    ┌ Info: GpABC model selection simulation
    │ Completed 1140 iterations, accepting 200 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 41	Model 2: 159
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:307
    ┌ Info: GpABC model selection simulation  Population 3 - ABC SMC ϵ = 10.0
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:24
    ┌ Info: GpABC model selection simulation
    │ Completed 2341 iterations, accepting 200 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 3	Model 2: 197
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:307
    ┌ Info: GpABC model selection simulation  Population 4 - ABC SMC ϵ = 5.0
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:24
    ┌ Warning: Simulated model selection reached maximum number of iterations (10000) on an SMC run - consider trying more iterations.
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:304
    ┌ Info: GpABC model selection simulation
    │ Completed 10000 iterations, accepting 104 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 67	Model 2: 37
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:307
    ┌ Info: GpABC model selection simulation  Population 5 - ABC SMC ϵ = 3.0
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:24
    ┌ Info: GpABC model selection simulation
    │ Completed 4503 iterations, accepting 200 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 62	Model 2: 138
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:307
    ┌ Info: GpABC model selection simulation  Population 6 - ABC SMC ϵ = 2.5
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:24
    ┌ Info: GpABC model selection simulation
    │ Completed 1372 iterations, accepting 200 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 86	Model 2: 114
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:307
    ┌ Info: GpABC model selection simulation  Population 7 - ABC SMC ϵ = 2.0
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:24
    ┌ Info: GpABC model selection simulation
    │ Completed 1461 iterations, accepting 200 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 175	Model 2: 25
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:307
    ┌ Info: GpABC model selection simulation  Population 8 - ABC SMC ϵ = 1.7
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:24
    ┌ Info: GpABC model selection simulation
    │ Completed 1752 iterations, accepting 200 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 108	Model 2: 92
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:307
    ┌ Info: GpABC model selection simulation  Population 9 - ABC SMC ϵ = 1.5
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:24
    ┌ Info: GpABC model selection simulation
    │ Completed 1135 iterations, accepting 200 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 119	Model 2: 81
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:307

A plot recipe allows this result to be plotted easily:

```julia
plot(ms_sim_result, title="Model selection result - simulation")
```

The number of accepted particles per population can be seen in the plot. Here, the "population" is the algorithmic population approach that is also taken during ABC-SMC. The number of particles accepted may be taken as a proxy for the posterior probability. Hence, the first model is correctly identified as the correct one.

Now, do the same computation using emulation by calling [`EmulatedModelSelection`](https://tanhevg.github.io/GpABC.jl/latest/ref-ms/#GpABC.EmulatedModelSelection). The only difference between the simulation and emulation function call is that we specify the number of design points as the first argument when using emulation.

We will use 200 design points.

```julia
ms_emu_result = EmulatedModelSelection(data,
    [simulator1, simulator2],
    [priors1, priors2],
    threshold_schedule,
    n_particles,
    200;
    summary_statistic = "keep_all",
    distance_function=Distances.euclidean);
```

    ┌ Info: GpABC model selection emulation  Population 1 - ABC Rejection ϵ = 20.0
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:9
    ┌ Info: GpABC model selection emulation
    │ Completed 15 iterations, accepting 200 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 110	Model 2: 90
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:212
    ┌ Info: GpABC model selection emulation  Population 2 - ABC SMC ϵ = 15.0
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:24
    ┌ Info: GpABC model selection emulation
    │ Completed 19 iterations, accepting 200 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 59	Model 2: 141
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:398
    ┌ Info: GpABC model selection emulation  Population 3 - ABC SMC ϵ = 10.0
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:24
    ┌ Info: GpABC model selection emulation
    │ Completed 41 iterations, accepting 200 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 98	Model 2: 102
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:398
    ┌ Info: GpABC model selection emulation  Population 4 - ABC SMC ϵ = 5.0
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:24
    ┌ Info: GpABC model selection emulation
    │ Completed 167 iterations, accepting 200 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 187	Model 2: 13
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:398
    ┌ Info: GpABC model selection emulation  Population 5 - ABC SMC ϵ = 3.0
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:24
    ┌ Info: GpABC model selection emulation
    │ Completed 96 iterations, accepting 200 particles in total.
    │ Number of accepted parameters by model:
    │ Model 1: 200	Model 2: 0
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:398
    ┌ Warning: All but one model is dead after population 5 - terminating model selection algorithm
    └ @ GpABC /bmm/home/et517/gaussian_processes/GpABC.jl/src/abc/model_selection.jl:35

```julia
plot(ms_emu_result, title="Model selection result - emulation")
```

We can see that emualtion also correctly identified the first model is as the preferred one.


```python
from mpi4py import MPI; import common; import grid; import functions; import la; import gdt
gdt.init_mpi()
```

$$\begin{align}\Omega &= [0, 1]^2\\
\Gamma_D &= \partial\Omega\end{align}$$

```python
g = grid.make_cube_grid__2d_simplex_aluconform(lower_left=[0, 0], upper_right=[1, 1], num_elements=[4, 4], num_refinements=2, overlap_size=[0, 0])
#g.visualize('grid')
```

$$\begin{align}\kappa(x; \mu) &:= 1 - (1 - \mu) \cos(\tfrac{1}{2} \pi x_0) \cos(\tfrac{1}{2} \pi x_1)\\
f(x) &:= \tfrac{1}{2} \pi^2 \cos(\tfrac{1}{2} \pi x_0) \cos(\tfrac{1}{2} \pi x_1)\\
g_D(x) &:= 0\end{align}$$

```python
#bump = functions.make_expression_function_1x1(g, 'x', 'cos(0.5*pi*x[0])*cos(0.5*pi*x[1])', order=3, name='bump')
#one = functions.make_constant_function_1x1(g, 1.0, name='one')
#diffusion = [one - bump, bump]
#diffusion[0].visualize(g, 'diffusion_affine_part')
#diffusion[1].visualize(g, 'diffusion_component')
#f = functions.make_expression_function_1x1(g, 'x', '0.5*pi*pi*cos(0.5*pi*x[0])*cos(0.5*pi*x[1])', order=3, name='rhs')
#f.visualize(g, 'force')
#g_D = functions.make_constant_function_1x1(g, 0.0, name='dirichlet')
kappa = functions.make_constant_function_1x1(g, 1.0, name='diffusion')
identity = functions.make_constant_function_2x2(g, [[0, 1], [1, 0]], name='id')
f = functions.make_constant_function_1x1(g, 1.0, name='force')
g_D = functions.make_constant_function_1x1(g, 0.0, name='dirichlet')
g_N = functions.make_constant_function_1x1(g, 0.0, name='neumann')
```

```python
space = gdt.make_cg_space__1x1__p1__fem(g)
#space.visualize("cg_space")

elliptic_op = gdt.make_elliptic_matrix_operator__istl_sparse(kappa, space)

system_assembler = gdt.make_system_assembler(space)
system_assembler.append(elliptic_op)
system_assembler.assemble()
```


# Computationally Visuallizing Crystals
 Christina Lee

Category: General

Prerequisites: none

In condensed matter, we find ourselves in the interesting middle ground of dealing with large numbers $10^{23}$ of extremely small particles, ex. atoms, electrons.

Luckily, the particles don't each do their own thing, but often come in nice, structured, repeated units.  Lattices.  So as our first step into the field, we will look at the most basic type, a Bravais Lattice.

In a Bravais Lattice, every site looks like every other site. Mathematically, we use three vectors, $\vec{a},\vec{b},\vec{c}$ to express how we move from one site to a neighbor.

\begin{equation}
\mathbf{R}_{lmn}=l \vec{a} + m \vec{b} + n \vec{c}  \;\;\;\; \text{for } l,m,n \in \mathbb{N}
\end{equation}

To keep things working out right, we have to put a constraint on these vectors; that we can't get one from scaling and adding the other two.  If we could, then we couldn't put sites in an entire 3 dimensional space.

Stay tuned for a later post where we explore more elaborate lattices.

```julia
# importing our packages
using Plots
gr()
```

    Plots.GRBackend()

## Define The Relevant Variables
Choose the lattice you want to look at, and put that string into the lattice variable.
Current options:

 Simple Cubic = "sc"

 Plane triangular lattice = "pt"

 Body-Centered Cubic = "bcc"

 Face-Centered Cubic = "fcc"

Note: Square is Simple Cubic for Nz=1

14 distinct lattice types are possible, but these common four give the important ideas.

Also, input the size of lattice you want to look at.

```julia
lattice="fcc";

Nx=3;
Ny=2;
Nz=3;
```

## The Lattice vectors

```julia
# A cell to just evaluate
# This one sets the unit vectors (a,b,c) for the different unit cells
# Can you guess what a lattice will look like by looking at the vectors?
if(lattice=="sc")
    d=3;
    a=[1,0,0];
    b=[0,1,0];
    c=[0,0,1];
elseif(lattice=="pt")
    d=2;
    a=[1,0,0];
    b=[.5,sqrt(3)/2,0];
    c=[0,0,1];
elseif(lattice=="bcc")
    d=3;
    a=[.5,.5,.5];
    b=[.5,.5,-.5];
    c=[.5,-.5,.5];
elseif(lattice=="fcc")
    d=3;
    a=[.5,.5,0];
    b=[.5,0,.5];
    c=[0,.5,.5];
end
"Cell Finished"
```

    "Cell Finished"

# Creating the Position arrays

Now we have everything we need about the specific lattice we want to look at.  From here on out, everything is completely general for any Bravais Lattice.

## Some helpful variables
The variables created in the next cell just make computation a bit easier, get the above variables into nicer forms.

```julia
# Another cell to just evaluate
N=Nx*Ny*Nz;    #The total number of sites

#these allow us to copy an entire row or layer at once
aM=transpose(a);
bM=transpose(repeat(b,outer=[1,Nx]));
cM=transpose(repeat(c,outer=[1,Nx*Ny]));

X=Array{Float64}(undef,N,3);  #where we store the positions
"Cell Finished"
```

    "Cell Finished"

## Entering the Position Values
Here, we first initialize a first row, then tile the first plane with the first row.
Afterward, we can tile the entire lattice with that first plane.

```julia
# Another cell to just evaluate
# Here we are actually calculating the positions for every site
for i in 1:Nx    #for the first row
    X[i,:]=(i-1)*a;
end

for j in 2:Ny    #copying the first row into the first layer
    X[Nx*(j-1).+(1:Nx),:]=X[1:Nx,:].+(j-1)*bM;
end

for j in 2:Nz    #copying the first layer into the entire cube
    X[Ny*Nx*(j-1).+(1:Nx*Ny),:]=X[1:Nx*Ny,:].+(j-1)*cM;
end
```

#### Programming Tip:

In Julia, ranges, like `1:Nx`, are a special variable type that can be manipulated.  We can add numbers to them:
`3+(1:3)=4:6`,
or add a minus sign to force it to iterate in the opposite direction, though with different start/stop:
`-(1:3)=-1:-1:-3`

Danger! Make sure to use the parentheses around the range if you are performing these operations.

## Plotting

```julia
plot(legend=false,title=lattice,xlabel="X",ylabel="Y",zlabel="Z")

ls=2
v=collect(0:ls)
zed=zeros(length(v))
for ii in 0:ls
    for jj in 0:ls
        plot!(zed .+ii,v,zed .+jj,linecolor=:black)
        plot!(zed .+ii,zed .+jj,v,linecolor=:black)
        plot!(v, zed .+ii,zed .+jj,linecolor=:black)
    end
end

scatter!(X[:,1],X[:,2],X[:,3])
```

## Go Back and Fiddle!
As you might have noticed, this isn't just a blog where you just read through it. Interact with it. Change some lines, and see what happens. I choose body centered cubic to display first, but what do the other lattices look like?

Look at different lattice sizes.

Can you hand draw them on paper?

```julia

```


# Automated Gradual Pruning Schedule

Michael Zhu and Suyog Gupta, ["To prune, or not to prune: exploring the efficacy of pruning for model compression"](https://arxiv.org/pdf/1710.01878), 2017 NIPS Workshop on Machine Learning of Phones and other Consumer Devices

After completing sensitivity analysis, decide on your pruning schedule.

## Table of Contents
1. [Implementation of the gradual sparsity function](#Implementation-of-the-gradual-sparsity-function)
2. [Visualize pruning schedule](#Visualize-pruning-schedule)
3. [References](#References)

```python
import numpy
import matplotlib.pyplot as plt
from functools import partial
import torch
from torch.autograd import Variable
from ipywidgets import widgets, interact
```

## Implementation of the gradual sparsity function

The function ```sparsity_target``` implements the gradual sparsity schedule from [[1]](#zhu-gupta):

"We introduce a new automated gradual pruning algorithm in which the sparsity is increased from an initial sparsity value $s_i$ (usually 0) to a final sparsity value $s_f$ over a span of $n$ pruning steps, starting at training step $t_0$ and with pruning frequency $\Delta t$."

$\large
\begin{align}
s_t = s_f + (s_i - s_f) \left(1- \frac{t-t_0}{n\Delta t}\right)^3
\end{align}
\ \ for
\large \ \ t \in \{t_0, t_0+\Delta t, ..., t_0+n\Delta t\}
$

Pruning happens once at the beginning of each epoch, until the duration of the pruning (the number of epochs to prune) is exceeded.  After pruning ends, the training continues without pruning, but the pruned weights are kept at zero.

```python
def sparsity_target(starting_epoch, ending_epoch, initial_sparsity, final_sparsity, current_epoch):
    if final_sparsity < initial_sparsity:
        return current_epoch
    if current_epoch < starting_epoch:
        return current_epoch

    span = ending_epoch - starting_epoch
    target_sparsity = ( final_sparsity +
                        (initial_sparsity - final_sparsity) *
                        (1.0 - ((current_epoch-starting_epoch)/span))**3)
    return target_sparsity
```

## Visualize pruning schedule
When using the Automated Gradual Pruning (AGP) schedule, you may want to visualize how the pruning schedule will look as a function of the epoch number.  This is called the *sparsity function*.  The widget below will help you do this.

There are three knobs you can use to change the schedule:
- ```duration```: this is the number of epochs over which to use the AGP schedule ($n\Delta t$).
- ```initial_sparsity```: $s_i$
- ```final_sparsity```: $s_f$
- ```frequency```: this is the pruning frequency ($\Delta t$).

```python
def draw_pruning(duration, initial_sparsity, final_sparsity, frequency):
    epochs = []
    sparsity_levels = []
    # The derivative of the sparsity (i.e. sparsity rate of change)
    d_sparsity = []

    if frequency=='':
        frequency = 1
    else:
        frequency = int(frequency)
    for epoch in range(0,40):
        epochs.append(epoch)
        current_epoch=Variable(torch.FloatTensor([epoch]), requires_grad=True)
        if epoch<duration and epoch%frequency == 0:
            sparsity = sparsity_target(
                     starting_epoch=0,
                     ending_epoch=duration,
                     initial_sparsity=initial_sparsity,
                     final_sparsity=final_sparsity,
                current_epoch=current_epoch
            )

            sparsity_levels.append(sparsity)
            sparsity.backward()
            d_sparsity.append(current_epoch.grad.item())
            current_epoch.grad.data.zero_()
        else:
            sparsity_levels.append(sparsity)
            d_sparsity.append(0)

    plt.plot(epochs, sparsity_levels, epochs, d_sparsity)
    plt.ylabel('sparsity (%)')
    plt.xlabel('epoch')
    plt.title('Pruning Rate')
    plt.ylim(0, 100)
    plt.draw()

duration_widget = widgets.IntSlider(min=0, max=100, step=1, value=28)
si_widget = widgets.IntSlider(min=0, max=100, step=1, value=0)
interact(draw_pruning,
         duration=duration_widget,
         initial_sparsity=si_widget,
         final_sparsity=(0,100,1),
         frequency='2');
```

## References
1.

 **Michael Zhu and Suyog Gupta**.
    [*To prune, or not to prune: exploring the efficacy of pruning for model compression*](https://arxiv.org/pdf/1710.01878),
    NIPS Workshop on Machine Learning of Phones and other Consumer Devices,
    2017.

```python

```