# 10 Ordinary Differential Equations (ODEs)

[ODE](http://mathworld.wolfram.com/OrdinaryDifferentialEquation.html)s describe many phenomena in physics. They describe the changes of a **dependent variable** $y(t)$ as a function of a **single independent variable** (e.g. $t$ or $x$).

An ODE of **order** $n$

$$
F(t, y^{(0)}, y^{(1)}, ..., y^{(n)}) = 0
$$

contains derivatives $y^{(k)}(t) \equiv y^{(k)} \equiv \frac{d^{k}y(t)}{dt^{k}}$ up to the $n$-th derivative (and $y^{(0)} \equiv y$).

### Initial and boundary conditions

* $n$ **initial conditions** are needed to *uniquely determine* the solution of a $n$-th order ODE, e.g, initial position and velocities.

* **Boundary conditions** (values of solution on domain boundries) can additionaly restrict solutions but the resulting *eigenvalue problems* are more difficult, e.g, wavefunction goes towards 0 for $\pm\infty$.

### Linear ODEs
A **linear** ODE contains no higher powers than 1 of any of the $y^{(k)}$.

*Superposition principle*: Linear combinations of solutions are also solutions.

#### Example: First order linear ODE

\begin{align}
\frac{dy}{dt} &= f(t)y + g(t)\\
y^{(1)} &= f(t)y + g(t)\\
% y^{(1)} - f(t)y - g(t) &= 0
\end{align}

##### Radioactive decay
$$
\frac{dN}{dt} = -k N
$$

### Non-linear ODEs
**Non-linear** ODEs can contain any powers in the dependent variable and its derivatives.

No superposition of solutions. Often impossible to solve analytically.

#### Example: Second order (general) ODE
\begin{gather}
\frac{d^2 y}{dt^2} + \lambda(t) \frac{dy}{dt} = f\left(t, y, \frac{dy}{dt}\right)\\
\end{gather}

##### Newton's equations of motion
$$
m\frac{d^2 x}{dt^2} = F(x) + F_\text{ext}(x, t) \quad \text{with}
\quad F(x) = -\frac{dU}{dx}
$$
(Force is often derived from a potential energy $U(x)$ and may contain non-linear terms such as $x^{-2}$ or $x^3$.)

## Partial differential equations (PDEs)
* more than one independent variable (e.g. $x$ and $t$)
* partial derivatives
* much more difficult than ODEs

#### Example: Schrödinger equation (Quantum Mechanics)
$$
i\hbar \frac{\partial\psi(\mathbf{x}, t)}{\partial t} = -\frac{\hbar^2}{2m}
   \left(\frac{\partial^2 \psi}{\partial x^2} +
         \frac{\partial^2 \psi}{\partial y^2} +
         \frac{\partial^2 \psi}{\partial z^2}
   \right) + V(\mathbf{x})\, \psi(\mathbf{x}, t)
$$

## Harmonic and anharmonic oscillator
* particle with mass $m$ connected to a spring
* spring described by a harmonic potential or anharmonic ones in the displacements from equilibrium $x$

\begin{align}
U_1(x) &= \frac{1}{2} k x^2, \quad k=1\\
U_2(x) &= \frac{1}{2} k x^2 \left(1 - \frac{2}{3}\alpha x\right), \quad k=1,\ \alpha=\frac{1}{2}\\
U_3(x) &= \frac{1}{p} k x^p, \quad k=1,\ p=6
\end{align}

1. What do these potentials look like? Sketch or plot.
2. Calculate the forces.

#### Potentials

```python
import numpy as np

def U1(x, k=1):
    return 0.5 * k * x*x

def U2(x, k=1, alpha=0.5):
    return 0.5 * k * x*x * (1 - (2/3)*alpha*x)

def U3(x, k=1, p=6):
    return (k/p) * np.power(x, p)
```

```python
import matplotlib
import matplotlib.pyplot as plt
matplotlib.style.use('seaborn-talk')
%matplotlib inline
```

```python
X = np.linspace(-3, 3, 100)
ax = plt.subplot(1,1,1)
ax.plot(X, U1(X), label=r"$U_1$")
ax.plot(X, U2(X), label=r"$U_2$")
ax.plot(X, U3(X), label=r"$U_3$")
ax.set_ylim(-0.5, 10)
ax.legend(loc="upper center");
```

#### Forces

\begin{align}
F_1(x) &= -kx\\
F_2(x) &= -kx(1 + \alpha x)\\
F_3(x) &= -k x^{p-1}
\end{align}

## ODE Algorithms
Basic idea:

1. Start with initial conditions, $y_0 \equiv y(t=0)$
2. Use $\frac{dy}{dt} = f(t, y)$ (the RHS!) to advance solution a small step $h$ forward in time: $y(t=h) \equiv y_1$
3. Repeat with $y_1$ to obtain $y_2 \equiv y(t=2h)$... and for all future values of $t$.

Possible issues
* small differences: subtractive cancelation and round-off error accumulation
* extrapolation: numerical "solution" can deviate wildly from exact
* possibly need adaptive $h$

### Euler's rule
Simple: forward difference

\begin{align}
f(t, y) = \frac{dy(t)}{dt} &\approx \frac{y(t_{n+1}) - y(t_n)}{h}\\
y_{n+1} &\approx y_n + h f(t_n, y_n) \quad \text{with} \quad y_n := y(t_n)
\end{align}

Error will be $\mathcal{O}(h^2)$ (bad!).

Also: what if we have a second order ODE ?!?! We only used $dy/dt$.

### Convert 2nd order ODE to 2 coupled _First Order_ ODEs

### Convert 2nd order ODE to 2 coupled 1st order ODEs
The 2nd order ODE is
$$
\frac{d^2 y}{dt^2} = f(t, y)
$$

Introduce "dummy" dependent variables $y_i$ with $y_0 \equiv y$ and

\begin{alignat}{1}
\frac{dy}{dt} &= \frac{dy_0}{dt} &=   y_1\\
\frac{d^2y}{dt^2} &= \frac{dy_1}{dt} &= {} f(t, y_0).
\end{alignat}

The first equation defines the velocity $y_1 = v$ and the second one is the original ODE.

### $n$-th oder ODE to $n$ coupled 1st order ODEs

The $n$-th order ODE is
$$
\frac{d^n y}{dt^n} = f(t, y, \frac{d y}{dt}, \frac{d^2 y}{dt^2}, \dots, \frac{d^{n-1} y}{dt^{n-1}})
$$

Introduce "dummy" dependent variables $y^{(i)}$ with $y^{(0)} \equiv y$ and

\begin{align}
\frac{dy^{(0)}}{dt} &=  y^{(1)}\\
\frac{dy^{(1)}}{dt} &=  y^{(2)}\\
\dots & \\
\frac{dy^{(n-1)}}{dt} &=  f(t, y^{(0)}, y^{(1)}, y^{(2)}, \dots, y^{(n-1)}).
\end{align}

### General standard (dynamic) form of ODEs
1 ODE of *any order* $n$ $\rightarrow$ $n$ coupled simultaneous first-order ODEs in $n$ unknowns  $y^{(0)}, \dots, y^{(n-1)}$:

\begin{align}
\frac{dy^{(0)}}{dt} &= f^{(0)}(t, y^{(0)}, \dots, y^{(n-1)})\\
\frac{dy^{(1)}}{dt} &= f^{(1)}(t, y^{(0)}, \dots, y^{(n-1)})\\
\vdots &  \\
\frac{dy^{(n-1)}}{dt} &= f^{(n-1)}(t, y^{(0)}, \dots, y^{(n-1)})\\
\end{align}

In $n$-dimensional vector notation:

\begin{align}
\frac{d\mathbf{y}(t)}{dt} &= \mathbf{f}(t, \mathbf{y})\\
\mathbf{y} &= \left(\begin{array}{c}
  y^{(0)}(t) \\
  y^{(1)}(t) \\
  \vdots \\
  y^{(n-1)}(t)
  \end{array}\right),
\quad
\mathbf{f} = \left(\begin{array}{c}
  f^{(0)}(t, \mathbf{y}) \\
  f^{(1)}(t, \mathbf{y}) \\
  \vdots \\
  f^{(n-1)}(t, \mathbf{y})
  \end{array}\right)
\end{align}

#### Example: Convert Newton's EOMs to standard form

$$
\frac{d^2 x}{dt^2} = m^{-1} F\Big(t, x, \frac{dx}{dt}\Big)
$$

RHS may *not contain any explicit derivatives* but components of $\mathbf{y}$ can represent derivatives.
* position $x$ as first dependent variable $y^{(0)}$ (as usual).
* velocity $dx/dt$ as second dependent variable $y^{(1)}$

\begin{align}
y^{(0)}(t) &:= x(t)\\
y^{(1)}(t) &:= \frac{dx}{dt} = \frac{dy^{(0)}}{dt}
\end{align}

One 2nd order ODE

$$
\frac{d^2 x}{dt^2} = m^{-1} F\Big(t, x, \frac{dx}{dt}\Big)
$$

to two simultaneous 1st order ODEs:

\begin{align}
\frac{dy^{(0)}}{dt} &= y^{(1)}(t)\\
\frac{dy^{(1)}}{dt} &= m^{-1} F\Big(t, y^{(0)}, y^{(1)}\Big)
\end{align}

\begin{align}
\frac{d\mathbf{y}(t)}{dt} &= \mathbf{f}(t, \mathbf{y})\\
\mathbf{y} &= \left(\begin{array}{c}
     y^{(0)} \\
     y^{(1)}
  \end{array}\right) =
  \left(\begin{array}{c}
     x(t) \\
     \frac{dx}{dt}
  \end{array}\right),\\
\mathbf{f} &= \left(\begin{array}{c}
    y^{(1)}(t) \\
    m^{-1} F\Big(t, y^{(0)}, y^{(1)}\Big)
  \end{array}\right) =
  \left(\begin{array}{c}
    \frac{dx}{dt} \\
    m^{-1} F\Big(t, x(t), \frac{dx}{dt}\Big)
  \end{array}\right)
\end{align}

#### Example:  1D harmonic oscillator in standard form
With $F_1 = -k x$:
$$
\frac{d^2 x}{dt^2} = -m^{-1}k x
$$
convert to

\begin{align}
\frac{dy^{(0)}}{dt} &= y^{(1)}(t) \\
\frac{dy^{(1)}}{dt} &= -m^{-1}k y^{(0)}
\end{align}

Force (or derivative) function $\mathbf{f}$ and initial conditions:

\begin{alignat}{3}
f^{(0)}(t, \mathbf{y}) &= y^{(1)},
    &\quad y^{(0)}(0) &= x_0,\\
f^{(1)}(t, \mathbf{y}) &=  -m^{-1} k y^{(0)},
    &\quad y^{(1)}(0) &= v_0.
\end{alignat}

### Euler's rule (standard form)
Given the $n$-dimensional vectors from the ODE standard form

$$
\frac{d\mathbf{y}}{dt} = \mathbf{f}(t, \mathbf{y})
$$
the **Euler rule** amounts to

\begin{align}
\mathbf{f}(t, \mathbf{y}) = \frac{d\mathbf{y}(t)}{dt} &\approx \frac{\mathbf{y}(t_{n+1}) - \mathbf{y}(t_n)}{\Delta t}\\
\mathbf{y}_{n+1} &\approx \mathbf{y}_n + \Delta t \mathbf{f}(t_n, \mathbf{y}_n) \quad \text{with} \quad \mathbf{y}_n := \mathbf{y}(t_n)
\end{align}

## Problem: Numerically integrate the 1D harmonic oscillator with Euler

\begin{alignat}{3}
f^{(0)}(t, \mathbf{y}) &= y^{(1)},
    &\quad y^{(0)}(0) &= x_0,\\
f^{(1)}(t, \mathbf{y}) &=   - \frac{k}{m} y^{(0)},
    &\quad y^{(1)}(0) &= v_0.
\end{alignat}

with $k=1$; $x_0 = 0$ and $v_0 = +1$.

### Explicit implementation:
* Note how in `f_harmonic` we are constructing the force vector of the standard ODE representation
* `y` is the vector of dependents in the standard representation
* We pre-allocate the array for `y` and then assign to individual elements with the
  ```python
  y[:] = ...
  ```
  notation, which has higher performance than creating the array anew every time.

```python
import numpy as np

def F1(x, k=1):
    """Harmonic force"""
    return -k*x

def f_harmonic(t, y, k=1, m=1):
    """Force vector in standard ODE form (n=2)"""
    return np.array([y[1], F1(y[0], k=k)/m])

t_max = 100
h = 0.01
Nsteps = int(t_max/h)
t_range = h * np.arange(Nsteps)
x = np.empty_like(t_range)

y = np.zeros(2)

# initial conditions
x0, v0 = 0.0, 1.0
y[:] = x0, v0

for i, t in enumerate(t_range):
    # store position that corresponds to time t_i
    x[i] = y[0]
    # Euler integrator
    y[:] = y + h * f_harmonic(t, y)
```

Plot the position $x(t)$ (which is $y_0$) against time:

```python
plt.plot(t_range, x)
plt.plot(t_range, np.sin(t_range), color="red")
```

### Modular solution with functions
We can make the Euler integrator a function, which makes the code more readable and modular and we can make the whole integration a function, too. This will allow us to easily run the integration with different initial values or `h` steps.

```python
import numpy as np

def F1(x, k=1):
    """Harmonic force"""
    return -k*x

def f_harmonic(t, y, k=1, m=1):
    """Force vector in standard ODE form (n=2)"""
    return np.array([y[1], F1(y[0], k=k)/m])

def euler(y, f, t, h):
    """Euler integrator.

    Returns new y at t+h.
    """
    return y + h * f(t, y)

def integrate(x0=0, v0=1, t_max=100, h=0.001):
    """Integrate the harmonic oscillator with force F1.

    Note that the spring constant k and particle mass m are currently
    pre-defined.

    Arguments
    ---------
    x0 : float
       initial position
    v0 : float
       initial velocity
    t_max : float
       time to integrate out to
    h : float, default 0.001
       integration time step

    Returns
    -------
    Tuple ``(t, x)`` with times and positions.
    """

    Nsteps = t_max/h
    t_range = h * np.arange(Nsteps)
    x = np.empty_like(t_range)
    y = np.zeros(2)
    # initial conditions
    y[:] = x0, v0

    for i, t in enumerate(t_range):
        # store position that corresponds to time t_i
        x[i] = y[0]
        # Euler integrator
        y[:] = euler(y, f_harmonic, t, h)

    return t_range, x
```

Plot the position as a function of time, $x(t)$.

```python
t, x = integrate(t_max=300, h=0.001)
plt.plot(t, x)
plt.plot(t, np.sin(t), color="red")
```

Analytical solution:
$$
\omega = \sqrt{\frac{k}{m}}
$$
and
$$
x(t) = A \sin(\omega t)
$$
here with $A=1$ and $\omega = 1$:

```python
plt.plot(t, x)
plt.plot(t, np.sin(t), color="red")
```

Note the increase in amplitude. Explore if smaller $h$ fixes this obvious problem.

```python
t, x = integrate(h=0.001)
plt.plot(t, x)
plt.plot(t, np.sin(t), color="red")
```

Smaller $h$ improves the integration (but Euler is still a bad algorithm... just run out for longer, i.e., higher `t_max`.)

### Note on Euler's global error

How does the global error after time $t$ behave, i.e., what is its dependency on step size $h$?

* Per-step (**local**) error is $\mathcal{O}(h^2)$.
* For time $t = N h$ we have $N \propto 1/h$ steps.

We assume that we linearly accumulate local error for the **global error** for $N$ steps:

$$
\mathcal{O}(h^2) \times h^{-1} \propto \mathcal{O}(h)
$$

Thus, halving $h$ should halve the global error.

```python

```


# Case 1: No Water Layer

* Author: **Team G15**
* Attempt: 3

## Analysis

### To find

1. Temperature of Roof Surface $(T_s)$
2. Total heat flux entering the house through the roof, $(q_t)$ when no water layer is present

### Nomenclature

* $T_s$ = roof surface temperature (outside)
* $T_a$ = ambient air temperature (outside)
* $T_r$ = room temperature (inside)
* $Nu_a$ = Nusselt number of air
* $Ra_a$ = Rayleigh number of air
* $Re_a$ = Reynolds number of air
* $Pr_a$ = Prandtl number of air
* $\alpha_a$ = thermal diffusivity of air
* $k_a$ = thermal conductivity of air
* $h_r$ = free convection coefficient of room air
* $\nu_a$ = dynamic Viscosity of air
* Roof layers:
  * 1: Concrete
  * 2: Brick
  * 3: Lime
* $k_i$ = thermal conductivity of $i^{th}$ roof layer
* $L_i$ = length of $i^{th}$ roof layer
* $q_{r}$ = radiative heat transfer (per unit area)
* $q_{c}$ = convective heat transfer (per unit area)
* $q_{t}$ = net heat transfer into the room (per unit area)
* $\beta$ = coefficient of thermal expansion
* $S$ = Intensity of Solar Radiation (i.e. solar constant)

### Assumptions

* Steady state with room maintained at fixed ambient temperature

### Equations

#### Energy balance,

$$ q_t = q_c + q_r $$

#### Radiation heat transfer,

\begin{align*}
  q_r &= \tau_s\cdot S - h_r \cdot (T_a - T_s) \\
\\
  h_r &= \epsilon_s\cdot \sigma\cdot \frac{(\overline T_s)^4 - (\overline T_a - 12)^4}{\overline T_a - \overline T_s}
\end{align*}

#### Convection heat transfer,

\begin{align*}
  q_c &= h_c\cdot (T_a - T_w) \\
\\
  h_c  &= \frac{k_a}{L_s}\cdot Nu_a \\
\\
  Nu_a &= 0.15\cdot Ra_a^{1/3} + 0.664\cdot Re_a^{1/2}\cdot Pr_a^{1/3} \\
\\
  Re_a &= \frac{v_a\cdot L_s}{\nu_a} \\
\\
  Ra_L &= \frac{g\cdot \beta\cdot (T_s - T_a)\cdot L_s^3}{\nu_a\cdot \alpha_a}
\end{align*}

#### Total heat transfer,

\begin{align*}
  q_t &= \frac{T_w - T_r}{R_{net}} \\
\\
  R_{net} &= \frac{1}{h_r} + \sum_{i=1}^{3} \frac{L_i}{k_i}
\end{align*}

### Properties

#### Outside Air

* Mild breeze $v_a = 2.78\ m/s$
* $T_a \in [305, 320] K$
* $T_f = 320K$
* $\beta = \frac{1}{T_f} = 0.0031\ K^{-1}$
* Table A.4, air ($T_f$):
  * $\nu = 18 \cdot 10^{-6}\ m^2/s$
  * $\alpha = 25 \cdot 10^{-6}\ m^2/s$
  * $Pr = 0.702$
  * $k = 27.7 \cdot 10^{-3}\ W/m\cdot K$
* $S = 1366\ W/m^2$

#### Roof

* $L_s = 5\ m$ (approx thickness of water layer)
* $\epsilon_s = 0.9$ (concrete surface)
* $\tau_s=0.9$
* $t = 0.2\ m$ thick with,
  * Cement = $5\ cm$
  * Brick = $10\ cm$
  * Lime = $5\ cm$
* $K_i$, Conductivity of each layer,
  * Cement = $0.72\ W/m\cdot K$
  * Brick = $0.71\ W/m\cdot K$
  * Lime = $0.73\ W/m\cdot K$

#### Inside air

* $T_r = 300K$ (Room Temperature)
* $h_r = 8.4\ W/m^2\cdot K$

### Tools used

* **Python**
* **SymPy** for creating symbolic equations and solving them
* **NumPy**
* **Matplotlib** for plotting results

## Solving (Python Code)

### Initialize Values

```python
import sympy as sp
import numpy as np
```

#### Outside Air

* Table A.4 used (from reference #2)

```python
v_a = 2.78 # Velocity (m / s)

# Temperatures
T_f = 320.0 # (K)
beta = 1/T_f # (K)
T_a = np.array([305.0, 310.0, 315.0, 320.0]) # (K)
T_a_avg = 273 + 37 # (K)

# Universal Constants
sigma = 5.67e-8 # Stefan Boltzmann constant (W / m^2 * K^4)
g = 9.8 # (m^2 / s)
S = 1366 # Solar constant

# Table A.6, air @ T = T_f
nu_a = 18e-6 # dynamic visosity (m^2 / s)
alpha_a = 25e-6 # (m^2 / s)
k_a = 27.7e-3 # thermal conductivity (W / m * K)
Pr_a = 0.702
```

#### Roof Layers

```python
# Temperatures
T_s = sp.symbols('T_s') # Roof surface temp (K)
T_s_avg = 273.0 + 35.0 # (K)

# Surface
L_s = 5 # Dimensions (m)
tau_s = 0.9 # Roof's solar absorbtivity
epsilon_s = 0.9 # Emissivity of roof surface (concrete)

# Layer 1: Concrete
k_1 = 0.72 # (W / m * K)
L_1 = 0.05 # (m)

# Layer 2: Brick
k_2 = 0.71 # (W / m * K)
L_2 = 0.10 # (m)

# Layer 3: Lime
k_3 = 0.73 # (W / m * K)
L_3 = 0.05 # (m)
```

#### Inside Air

```python
h_r = 8.4 # (W / m^2 * K)
T_r = 300 # (K)
```

### Equations

#### Radiation Heat

```python
h_r = epsilon_s * sigma * (T_s_avg**4 - (T_a_avg - 12)**4)/(T_a_avg - T_s_avg) # (W / m^2 * K)
q_r = tau_s * S - h_r * (T_a - T_s) # (W / m^2)

# Example at T_a = 310K and T_s = 314K
q_r_test = q_r[1].replace(T_s, 314)
print('Approximate value of q_r = %.2f W/m^2' % (q_r_test))
```

    Approximate value of q_r = 1343.00 W/m^2

#### Convection Heat

* From below analysis, we can neglect free convection in comparison to forced convection

##### Free Convection

```python
Ra_a = (g * beta * (T_s - T_a) * L_s**3) / (nu_a * alpha_a)
Nu_a_fr = 0.15 * Ra_a**(1/3)
h_c_fr = k_a / L_s * Nu_a_fr

# Example at T_a = 310K and T_s = 314K
h_c_fr_test = h_c_fr[1].replace(T_s, 314)
print('Approximate value of free convection coefficient = %.2f W/K*m^2' % (h_c_fr_test))
```

    Approximate value of free convection coefficient = 2.69 W/K*m^2

##### Forced Convection

```python
Re_a = v_a * L_s / nu_a
Nu_a_fo = 0.664 * Re_a**1/2 * Pr_a**1/3
h_c_fo = k_a / L_s * Nu_a_fo

# Example at T_a = 310K and T_s = 314K
print('Approximate value of forced convection coefficient = %.2f W/K*m^2' % (h_c_fo))
```

    Approximate value of forced convection coefficient = 332.36 W/K*m^2

##### Total Convection

```python
h_c = h_c_fo # Neglicting free convection
q_c = h_c * (T_a - T_s) # (W / m^2)

# Example at T_a = 310K and T_s = 314K
q_c_test = q_c[1].replace(T_s, 314)
print('Approximate value of q_c = %.2f W/m^2' % (q_c_test))
```

    Approximate value of q_c = -1329.43 W/m^2

#### Total Heat:

```python
R = 1/h_r + L_1/k_1 + L_2/k_2 + L_3/k_3 # (m^2 * K / W)

q_t = (T_s - T_r) / R # (W / m^2)

# Example at T_a = 310K and T_s = 314K
q_t_test = q_t.replace(T_s, 314)
print('Approximate value of q_t = %.2f W/m^2' % (q_t_test))
```

    Approximate value of q_t = 44.59 W/m^2

### Solving

\begin{align*}
  q_c + q_r &= q_t
\\
  \therefore\hspace{3pt} q_c + q_r - q_t &= 0
\end{align*}

#### Calculate $T_s$

```python
eq = q_c + q_r - q_t

n = len(eq)
T_s_calc = np.empty(n, dtype=object)

for i in range(n):
    T_s_calc[i] = round(sp.solve(eq[i], T_s)[0], 2)

for i in range(n):
    print('T_s = %.1f K for T_a = %.1f K' % (T_s_calc[i], T_a[i]))
```

    T_s = 309.0 K for T_a = 305.0 K
    T_s = 313.9 K for T_a = 310.0 K
    T_s = 318.9 K for T_a = 315.0 K
    T_s = 323.8 K for T_a = 320.0 K

#### Calculate $q_t$

```python
q_t_calc = np.empty(n, dtype=object)

for i in range(n):
    q_t_calc[i] = q_t.replace(T_s, T_s_calc[i])

for i in range(n):
    print('Heat entering = %.1f W/m^2 for T_a = %.1f K' % (q_t_calc[i], T_a[i]))
```

    Heat entering = 28.5 W/m^2 for T_a = 305.0 K
    Heat entering = 44.3 W/m^2 for T_a = 310.0 K
    Heat entering = 60.0 W/m^2 for T_a = 315.0 K
    Heat entering = 75.8 W/m^2 for T_a = 320.0 K

### Plot

* Total Heat Flux Entering ($q_t$) vs Outside Air Temp ($T_a$)

```python
def make_plot(x, y, xlabel, ylabel, title):
    plt.plot(x, y, color='#1F77B4cc', marker='o')
    plt.xticks(fontsize=20)
    plt.yticks(fontsize=20)
    plt.xlabel(xlabel, fontsize=20)
    plt.ylabel(ylabel, fontsize=20)
    plt.title(title, fontsize=18, pad=15)
```

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

# Initialize matplotlib
plt.rc('text', usetex=True) # Unnecessary
plt.style.use('ggplot')
plt.rcParams['grid.color'] = '#C0C0C0'

fig = plt.figure(figsize=(10, 6))
plt.plot(T_a, q_t_calc, color='#1F77B4cc', marker='o')
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.xlabel('$T_a$', fontsize=20)
plt.ylabel('$q_t$', fontsize=20)
plt.title('Total Heat Flux Entering ($q_t$) vs Outside Air Temp ($T_a$)', fontsize=18, pad=15)

make_plot(x=T_a, y=q_t_calc, xlabel='$T_a$', ylabel='$q_t$',
          title='Total Heat Flux Entering ($q_t$) vs Outside Air Temp ($T_a$)')
```

```python

```


# One plot function, please

Let's list all the specialized plot functions:
* `plot`: to plot univariate expressions.
* `plot_parametric`: to plot univariate 2D parametric expressions.
* `plot3d`: to plot functions/expressions of 2 variables.
* `plot3d_parametric_line`: to plot univariate 3D parametric expressions.
* `plot3d_parametric_surface`
* `plot_contour`: to draw contours of functions/expressions of 2 variables.
* `plot_implicit`: to plot relationals and boolean expressions.
* `plot_vector`: to plot vector fields.
* `plot_complex`: to plot complex functions.
* `plot_geometry`: to plot entities from `sympy.geometry` module.

Those are many functions and some of them are quite long to type. This module offers another function, `smart_plot`, which should be able to recognize what kind of expression we are trying to plot.

Let's see with the following examples the pros and cons of this function.

```python
from sympy import *
from spb import smart_plot as plot
var("x:z")
```

```python
help(plot)
```

In order to type less, we aliased `smart_plot` to `plot`. Let's say we'd like to plot a line:

```python
plot(sin(x))
```

Note that there are no axis labels. This design choice is motivated by the fact that `smart_plot` is a very general function which must work with all the aformentioned plot types. It would require quite an effort to extract the correct labels for every type of plot.

Therefore, if we need labels we either use one of the aforementioned plot functions, or we can set the `xlabel`, `ylabel`, `zlabel` keyword arguments. Let's try to plot a function of two variables:

```python
plot(cos(x**2 + y**2), (x, -3, 3), (y, -3, 3),
     xlabel="x", ylabel="y", zlabel="f(x, y)", n=125)
```

What if we would like to draw contours of the previous expression? We need to explicitly inform the function of our intentions by setting the `pt` keyword argument, which stands for _plot type_. This is necessary because `plot3d` and `plot_contour` both plot the same type of expressions, hence `smart_plot` is coded to automatically create 3D figures whenever it encounters functions of two variables.

```python
plot(cos(x**2 + y**2), (x, -3, 3), (y, -3, 3),
     xlabel="x", ylabel="y", pt="pc")
```

Let's now try to plot a 2D vector field:

```python
plot([-y, x], (x, -5, 5), (y, -3, 3), aspect="equal", n=15, legend=True)
```

What happened? Why is there a shaded region? Why are the ranges not considered?

In [tutorial 7](tutorial-7.vector-plots.ipynb) we mentioned that it is better to wrap vector fields into matrices rather than lists, otherwise the internal algorithm might gets confused. This is one of those occurences! If you look at the above arguments, `[-y, x], (x, -5, 5), (y, -3, 3)`, these are all tuples/lists. `smart_plot` algorithms needs to deal with many different expressions. Here, the algorithm thinks that `[-y, x]` is a vector field and somehow `(x, -5, 5)` and `(y, -3, 3)` are two implicit expressions. So, if you need to quickly plot vector fields, always wrap them into matrices:

```python
plot(Matrix([-y, x]), (x, -5, 5), (y, -3, 3), aspect="equal", n=15, legend=True)
```

Let's now try something more complicated. We are going to plot a vector field and a line with the same command:

```python
plot(
    (Matrix([-sin(y), cos(x)]), (x, -5, 5), (y, -3, 3), "vector"),
    (sin(x), (x, -5, 5)),
    aspect="equal", n=20, legend=True,
    quiver_kw=dict(scale=0.25), line_kw=dict(line_color="cyan")
)
```

A few things to note here:
* The number of discretization points `n=20` has been correctly applied to the vector field. However, if we closely inspect the line, it clearly has more points. That's because the line has been adaptively sampled. In order to have the line with 20 points we have to set `adaptive=False`.
* We can use all the usual dictionaries to customize the looks of the plot. Here we used `quiver_kw` to set the size of the quivers and `line_kw` to set the line color.

Let's try to plot a combination of a geometric entity with a line:

```python
%matplotlib widget
from spb.backends.matplotlib import MB
plot(
    (Circle((-pi / 2, 0), 1), ),
    (sin(x), (x, -5, 5)),
    backend=MB, aspect="equal", fill=False
)
```

Note that the first tuple only contains one element, the expression to be plotted.

Just for fun, let's try a complex plot:

```python
plot(gamma(z), (z, -3 - 3*I, 3 + 3*I), backend=MB, coloring="b")
```

```python

```


# Lab 2: Stochastic Gradient Descent & Pytorch

**Tomas Beuzen, January 2021**

## Table of Contents

Instructions

Imports

Exercise 1: Stochastic Gradient Descent

Exercise 2: SGDClassifier and SGDRegresor

Exercise 3: Neural Networks "By Hand"

Exercise 4: Predicting Fashion

(Optional) Exercise 5: Implementing Adam Optimization From Scratch

(Optional) Exercise 6: Gif or Jiff

Submit to Canvas and GitHub

## Instructions

rubric={mechanics:3}

**Link to your GitHub repository:**

You will receive marks for correctly submitting this assignment. To submit this assignment you should:

1. Push your assignment to your GitHub repository!
2. Provide a link to your repository in the space provided above.
2. Upload a HTML render of your assignment to Canvas. The last cell of this notebook will help you do that.
3. Be sure to follow the [General Lab Instructions](https://ubc-mds.github.io/resources_pages/general_lab_instructions/). You can view a description of the different rubrics used for grading in MDS [here](https://github.com/UBC-MDS/public/tree/master/rubric).

Here's a break down of the required and optional exercises in this lab:

|         | Number of Exercises | Points |
|:-------:|:-------------------:|:------:|
| Required| 13 | 35 |
| Optional| 2  | 2 |

## Imports

```python
import numpy as np
import pandas as pd
import time
import torch
from torch import nn
from torchvision import datasets, transforms
from sklearn.datasets import load_boston
from sklearn.preprocessing import MinMaxScaler
from sklearn.linear_model import LinearRegression, LogisticRegression, SGDClassifier
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
from canvasutils.submit import submit, convert_notebook
import matplotlib.pyplot as plt
plt.style.use('ggplot')
plt.rcParams.update({'font.size': 16, 'figure.figsize': (5,5), 'axes.grid': False})
```

## Exercise 1: Stochastic Gradient Descent

Below is a super-streamlined Python function that performs gradient descent, it's much like the code you saw in Lectures and in Lab 1. I just made a few small changes:

- Removed the docstring and `print` statements for brevity.
- The algorithm stops after `n_iters`.

I also include functions to calculate the MSE and gradient of MSE for a linear model $\hat{y}=w^Tx$.

```python
def gradient_descent(f, f_grad, w0, X, y, n_iters=1000, α=0.001):
    w = w0
    for _ in range(n_iters):
        w = w - α * f_grad(w, X, y)
    return w

def mse(w, X, y):
    return np.mean((X @ w - y) ** 2)

def mse_grad(w, X, y):
    return X.T @ (X @ w) - X.T @ y
```

The code works fine, although it is quite slow as per usual for vanilla gradient descent. For example, here we do ordinary least squares linear regression on the boston house-prices dataset from sklearn:

```python
X, y = load_boston(return_X_y=True)
X = MinMaxScaler().fit_transform(X)
```

```python
start = time.time()
w0 = np.zeros(X.shape[1])
w_gd = gradient_descent(mse, mse_grad, w0, X, y, n_iters=10**5)
print(f"Fitting time = {time.time() - start:.4f}s")
print("Weights:")
w_gd
```

Compared to sklearn's `LinearRegression()`:

```python
start = time.time()
lr = LinearRegression(fit_intercept=False).fit(X, y)
print(f"Fitting time = {time.time() - start:.4f}s")
print("Weights:")
lr.coef_
```

As we can see, the coefficients obtained from gradient descent are very similar to those obtained by sklearn's `LinearRegression()` (although sklearn is much faster). All is well so far.

### 1.1
rubric={accuracy:5}

In this exercise your task is to implement a function `stochastic_gradient_descent`, that performs SGD, _using_ the `gradient_descent` function provided above. You can have your function accept the same arguments as the `gradient_descent` function above, except:

- Change `n_iters` to `n_epochs`.
- Add an extra `batch_size` argument.
- Your implementation of SGD should follow "[Approach 1](https://pages.github.ubc.ca/MDS-2020-21/DSCI_572_sup-learn-2_students/lectures/lecture3_stochastic-gradient-descent.html#sampling-with-or-without-replacement)" from Lecture 3: shuffle the dataset and then divide it into batches. If the numer of samples is not divisible by the `batch_size`, your last batch will have less than `batch_size` samples - you can either throw this last batch away or use it (I usually choose to use it and that's the default in PyTorch, the library we'll be using to build neural networks starting next lecture).
- You can leave `α` constant for all iterations.

>The pedagogical goal here is to help you see how SGD relates to regular "vanilla" gradient descent. In reality it would be fine to implement SGD "from scratch" without calling a GD function.

```python
def stochastic_gradient_descent(f, f_grad, w0, X, y, n_epochs=1, α=0.001, batch_size=1):
    pass  # Your solution goes here.

stochastic_gradient_descent(mse, mse_grad, w0, X, y)  # Test your function with defaults (results probably won't be very good)
```

### 1.2
rubric={accuracy:3}

Show that when the batch size is set to the whole training set (i.e., `batch_size=len(X)`), you get exactly the same estimated coefficients with SGD and GD. Use the same learning rate (`α=0.001`) and number of epochs (`10 ** 5`) for both algorithms.

```python
w_gd = gradient_descent(mse, mse_grad, w0, X, y, n_iters=10**5)  # GD coefficients
print(w_gd)

w_sgd = None  # Your solution goes here.
print(w_sgd)
```

## Exercise 2: `SGDClassifier` and `SGDRegresor`

In this exercise we'll explore training a classifier with SGD on the [Sentiment140 dataset](http://help.sentiment140.com/home), which contains tweets labeled with sentiment associated with a brand, product, or topic. Please start by doing the following:

1. Download the corpus from [here](http://cs.stanford.edu/people/alecmgo/trainingandtestdata.zip).
2. Unzip.
3. Copy the file `training.1600000.processed.noemoticon.csv` into the current directory.
4. Create a `.gitignore` file so that you don't accidentally commit the dataset (I've tried to do this for you but please double check it).

Once you're done the above, steps, run the starter code below:

```python
# Data loading and preprocessing
df = pd.read_csv(
    "training.1600000.processed.noemoticon.csv",
    encoding="ISO-8859-1",
    names=["label", "id", "date", "no_query", "name", "text"],
)
df["label"] = df["label"].map({0: "neg", 4: "pos"})  # change 0's to "neg" and 4's to "pos"
df.head()
```

Now we split the data:

```python
X, y = df["text"], df["label"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=2021)
```

And then we encode it using `CountVectorizer`, which may take a minute or so:

```python
vec = CountVectorizer(stop_words='english')
X_train = vec.fit_transform(X_train)
X_test = vec.transform(X_test)
```

Note that our training data is rather large compared to datasets we've explored in the past:

```python
X_train.shape
```

Luckily, `CountVectorizer()` returns us a sparse matrix:

```python
type(X_train)
```

Recall that a sparse matrix is a more efficient representation of a matrix that contains many 0's. What percentage of our array is non-zero?

```python
print(f"{X_train.nnz / np.prod(X_train.shape) * 100:.5f}%")
```

So few non-zero values - lucky we have a sparse matrix! Anyway, let's train a classifier (this may take a while!):

```python
lr = LogisticRegression()
```

```python
t = time.time()
lr.fit(X_train, y_train)
print(f"Training took {time.time() - t:.1f} seconds")
```

```python
print(f"Train score: {lr.score(X_train, y_train):.2f}")
print(f"Test score: {lr.score(X_test, y_test):.2f}")
```

### 2.1
rubric={accuracy:3}

In sklearn, there is a classifier called `linear_model.SGDClassifier()` - [see the docs](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDClassifier.html) (there's also a `SGDRegressor` but we won't look at that here). As the name suggests, this model can train linear classifiers with SGD - in the true sense of the algorithm, 1 sample per iteration (i.e., batch size of 1).

Train a logistic regression model on the same dataset above using `SGDClassifier`. Compare the training time of your `SGDClassifier` to `LogisticRegression()`. You'll need to specify the correct `loss` argument in `SGDClassifier()` to train a logistic regression model. [Read the docstring](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.SGDClassifier.html) to find the appropriate `loss`.

> The pedagogical goal here is to demonstrate how using just one sample per iteration in SGD can significantly speed up training, and accuracy-wise, is not a terrible idea!

```python
# Your solution goes here.
```

### 2.2
rubric={reasoning:2}

Discuss the training and test accuracies of your `SGDClassifier()` and `LogisticRegression()` models. Is there any difference between the two models?

```python
# Your solution goes here.
```

### 2.3
rubric={reasoning:3}

One possible explanation for `SGDClassifier`'s speed is that it's just doing fewer iterations (epochs) before converging. `SGDClassifier` and `LogisticRegression` have an `n_iter_` attribute which you can check after fitting (in these sklearn models, `n_iter_` is equivalent to "number of epochs").
1. Compare these values and discuss them in the context of the above hypothesis.
2. Then, using the `max_iter` parameter, do a "fair" experiment where `SGDClassifier` and `LogisticRegression` do the same number of passes through the dataset, and comment on the results.

>To be completely "fair" we should also make sure that the tolerance and regularization strength in both models is the same (by default they are not) - but you don't need to worry about that here. Just focus on the number of iterations/epochs.

Your solution goes here.

## Exercise 3: Neural Networks "By Hand"

### 3.1
rubric={accuracy:4}

The neural networks we've seen in [Lecture 4](https://pages.github.ubc.ca/MDS-2020-21/DSCI_572_sup-learn-2_students/lectures/lecture4_pytorch-neural-networks-pt1.html) are just recursive functions where each layer is made up of the previous layer's output, multiplied by some weights, with some biases added, and passed through an activation function (we call these networks "fully-connected feed-forward networks", but more on that next week). We don't usually include the input layer when counting up the layers in a network. So a 2 layer network (`L=2`) really means: 1 input layer, 1 hidden layer, 1 output layer. A 3 layer network (`L=3`) really means: 1 input layer, 2 hidden layers, 1 output layer.

Mathematically, our networks have the form:

$$ x^{(l+1)} = h\left( W^{(l)} x^{(l)} + b^{(l)}\right) $$

where:
- $W^{(l)}$ is a matrix of weights.
- $b^{(l)}$ is a vector of biases.
- $x^{(l)}$ is the output of layer $l$:
    - $x^{(0)}$ are the inputs
    - $x^{(1)}$ are the outputs of the first hidden layers, i.e., $x^{(1)} = h\left( W^{(0)} x^{(0)} + b^{(0)}\right)$
    - etc.
    - $x^{(L)} = \hat{y}$
        - Classification: $\hat{y} = h\left( W^{(L-1)} x^{(L-1)} + b^{(L-1)}\right)$
        - Regression: $\hat{y} = W^{(L-1)} x^{(L-1)} + b^{(L-1)}$ (no activation!)

Suppose that we use a neural network with one hidden layer with a **ReLU activation** for a **regression** problem. After training, we obtain the following parameters:

$$\begin{align}W^{(0)} &= \begin{bmatrix}-2 & 2 & -1\\-1 & -2 & 0\end{bmatrix},  &b^{(0)}&=\begin{bmatrix}2 \\ 0\end{bmatrix} \\ W^{(1)} &= \begin{bmatrix}3 & 1\end{bmatrix},  &b^{(1)}&=-10\end{align}$$

For a training example with features $x = \begin{bmatrix}3 \\-2 \\ 2\end{bmatrix}$ what are the values in this network of $x^{(1)}$ and $\hat{y}$? Show your work using code cells or LaTeX.

```python
# Your answer goes here.
```

### 3.2
rubric={reasoning:4}

Draw this neural network using a circle/arrow diagram (similar to [the ones I drew in Lecture 4](https://pages.github.ubc.ca/MDS-2020-21/DSCI_572_sup-learn-2_students/lectures/lecture4_pytorch-neural-networks-pt1.html#neural-network-basics)). Label the diagram with the weight/bias values given above. If you want to draw this diagram by hand, that is fine: you can take a photo of the drawing and put it in here. If you are doing so, make sure you upload the image to your repo!

Your drawing goes here.

## Exercise 4: Predicting Fashion

In this Exercise I'm going to get you to train a neural network using the Fashion-MNIST dataset. Fashion-MNIST is a set of 28 x 28 pixel greyscale images of clothes. Some of you may have worked with this dataset before - it's a classic. I promise that our datasets will get more interesting than this, but this dataset is ideal for your first PyTorch exercise. Below is a sample of some of the images in the dataset - we have 10 classes in the dataset: T-shirt/tops, Trousers, Pullovers, Dresss, Coats, Sandals, Shirts, Sneakers, Bags, and Ankle Boots.

The goal of this exercise is to develop a network that can correctly predict a given image of "fashion" into one of the 10 classes. This is a multi-class classification task, our model should spit out 10 probabilities for a given image - one probability for each class. Ideally the class our model predicts with maximum probability is the correct one!

The below cell will download and load in the Fashion-MNIST data for you. We'll talk more about this process next week, but briefly:
- Think of images as ndarrays of data, in the case of grayscale images like we have here, each pixel has a value between 0 and 1 indicating how "bright" that pixel is. So each image here is just a 28 x 28 ndarray with values ranging from 0 to 1! (when we get to colour images, it's exactly the same, except each pixel has 3 values, one for each of the colour channels Red, Blue, Green. If we had colour images here our array would be 28 x 28 x 3).
- `transform`: applies some transformations to the images. Here we are converting the data to tensors. We'll work with these more next week so don't worry too much about them.
- `torch.utils.data.DataLoader`: these are "data loaders". Think of them as generators. During training/testing, we can easily query them for a batch of data of size `BATCH_SIZE`. Cool!

```python
BATCH_SIZE = 64

# Define a transform to normalize the data, which usually helps with training
transform = transforms.Compose([transforms.ToTensor()])
# Download data
trainset = datasets.FashionMNIST('~/.pytorch/F_MNIST_data/', download=True, train=True, transform=transform)
testset = datasets.FashionMNIST('~/.pytorch/F_MNIST_data/', download=True, train=False, transform=transform)
# Create data loaders (these are just generators that will give us `batch_size` samples as a time)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)
testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=True)
# Class labels
class_labels = ['T-shirt/top', 'Trouser', 'Pullover', 'Dress', 'Coat',
                'Sandal', 'Shirt', 'Sneaker', 'Bag', 'Ankle Boot']
```

Let's plot a random image (run this cell as many times as you like to see different images):

```python
image, label = next(iter(trainloader))  # Get a random batch of 64 images
i = np.random.randint(0, 64)            # Choose one image at random
plt.imshow(image[i, 0], cmap='gray')    # Plot
plt.title(class_labels[label[i]]);
```

### 4.1
rubric={accuracy:3}

Notice in the plot above that our image is 28 x 28 pixels. How do we feed this into a neural network? Well we can flatten it out into a vector of 784 elements (28 x 28 = 784) and create 784 input nodes! We'll do this later on - for now, all I want you to do is create a new class defining a classifier with the following architecture ([this section of Lecture 4](https://pages.github.ubc.ca/MDS-2020-21/DSCI_572_sup-learn-2_students/lectures/lecture4_pytorch-neural-networks-pt1.html#non-linear-regression-with-a-neural-network) might help here):
- linear layer that goes from `input_size` -> 256 nodes
- ReLU activation function
- linear layer that goes from 256 nodes -> 128 nodes
- ReLU activation function
- linear layer that goes from 128 nodes -> 64 nodes
- ReLU activation function
- output layer that goes from 64 nodes -> `output_size` nodes

I've given you some starter code to get you going.

>When we create our model in a later exercise we will specify `input_size=784` and `output_size=10`. The `784` is the flattened 28 x 28 image and the output size of 10 is so that we have one node for each item of clothing (remember we have 10 classes), and each node will contain the probability of that item of clothing being the one in a particular input image.

```python
class Classifier(nn.Module):
    def __init__(self, input_size, output_size):
        super().__init__()
        self.main = None  # Your code goes here

    def forward(self, x):
        out = self.main(x)
        return out
```

### 4.2
rubric={accuracy:1}

If your model has `input_size = 784` and `output_size=10`, how many parameters does your model have? (Ideally you can work this out by hand but you can use `torchsummary` like I showed in Lecture 4 if you must...)

```python
# Your answer goes here.
```

### 4.3
rubric={accuracy:3}

We haven't trained yet, but let's test out your network. The below function will help you plot your network's predictions for a particular image using `matplotlib`, run it:

```python
def plot_prediction(image, label, predictions):
    """Plot network predictions with matplotlib."""
    fig, (ax1, ax2) = plt.subplots(figsize=(8, 4), ncols=2)  # Plot
    ax1.imshow(image[0], cmap='gray')
    ax1.axis('off')
    ax1.set_title(class_labels[label])
    ax2.barh(np.arange(10), predictions.data.numpy().squeeze())
    ax2.set_title("Predictions")
    ax2.set_yticks(np.arange(10))
    ax2.set_yticklabels(class_labels)
    ax2.set_xlim(0, 1)
    plt.tight_layout();
```

```python
model = Classifier(input_size=784, output_size=10)
# Test on training images (run as many times as you like!)
image, label = next(iter(trainloader))        # Get a random batch of 64 images
predictions = model(image[0].view(1, -1))     # Get first image, flatten to shape (1, 784) and predict it
predictions = nn.Softmax(dim=1)(predictions)  # Coerce predictions to probabilities using Softmax()
plot_prediction(image[0], label[0], predictions)
```

Okay, those predictions are probably pretty bad. We need to train! Below is a training function (the same one we saw in Lecture 4). The only difference is that when I'm creating `y_hat` (my model predictions), I'm reshaping my data to be of shape `(batch_size, 784)` using `X.view(X.shape[0], -1)` so we can feed it into our network (`X.shape[0]` is the batch size, and the -1 just says "flatten remaining dimensions into a single dimension", so this turns something of shape `(64, 28, 28)` to `(64, 784)` which in English reads as "64 examples of vectors with length 784", i.e. 64 flattened images).

```python
def trainer(model, criterion, optimizer, dataloader, epochs=5, verbose=True):
    """Simple training wrapper for PyTorch network."""

    for epoch in range(epochs):
        losses = 0
        for X, y in dataloader:
            optimizer.zero_grad()       # Clear gradients w.r.t. parameters
            y_hat = model(X.view(X.shape[0], -1))  # Reshape data to (batch_size, 784) and forward pass to get output
            loss = criterion(y_hat, y)  # Calculate loss
            loss.backward()             # Getting gradients w.r.t. parameters
            optimizer.step()            # Update parameters
            losses += loss.item()       # Add loss for this batch to running total
        if verbose: print(f"epoch: {epoch + 1}, loss: {losses / len(dataloader):.4f}")
```

Define an appropriate `criterion` and `optimizer` to train your model with.
- We are doing multi-class classification here, what loss function do we use for this case (hint: see [this part of Lecture 4](https://pages.github.ubc.ca/MDS-2020-21/DSCI_572_sup-learn-2_students/lectures/lecture4_pytorch-neural-networks-pt1.html#multiclass-classification-optional))?
- Use any optimizer you like but I recommend Adam (by the way, optional Exercise 5 of this lab gets you to implement Adam from scratch 😉 )

I already created the dataloader `trainloader` for you at the start of this exercise. Pass all these things to `trainer()` to train your model (it may take a few minutes):

```python
# Your answer goes here.

# Uncomment and run the below once you've defined a criterion and optimizer
# trainer(model, criterion, optimizer, trainloader, epochs=5, verbose=True)
```

### 4.4
rubric={accuracy:1}

Test out your newly trained network on the training data:

```python
# Test model on training images (run as many times as you like!)
image, label = next(iter(trainloader))        # Get a random batch of 64 images
predictions = model(image[0].view(1, -1))     # Get first image, flatten to shape (1, 784) and predict it
predictions = nn.Softmax(dim=1)(predictions)  # Coerce predictions to probabilities using Softmax()
plot_prediction(image[0], label[0], predictions)
```

And test it out on the test data:

```python
# Test model on testing images (run as many times as you like!)
image, label = next(iter(testloader))        # Get a random batch of 64 images
predictions = model(image[0].view(1, -1))     # Get first image, flatten to shape (1, 784) and predict it
predictions = nn.Softmax(dim=1)(predictions)  # Coerce predictions to probabilities using Softmax()
plot_prediction(image[0], label[0], predictions)
```

Pretty sweet! That's all there is to it - you just created your first legitimate classifier! WELL DONE! I personally am super excited for you.

Oh right, I need to give you an exercise to answer. In this exercise we used a `BATCH_SIZE = 64`. This is a pretty common size to use, in fact the most common sizes are: 32, 64, 128, 256, and 512. In terms of optimizing a network, list one difference between using a small batch size vs large batch size?

Your answer goes here.

### 4.5

No marks and nothing to do for this question, just a bit of fun. Our network was trained on clothing images, but that doesn't mean we can't use it to predict other images (but this is probably a bad idea)! Let's see what our model thinks of a 28 x 28 image of me:

```python
image = torch.from_numpy(plt.imread("img/tomas_beuzen.png"))
predictions = model(image.view(1, -1))        # Flatten image to shape (1, 784) and predict it
predictions = nn.Softmax(dim=1)(predictions)  # Coerce predictions to probabilities using Softmax()
label = predictions.argmax(dim=1)             # Get class label from max probability
plot_prediction(image.numpy()[None, :, :], label, predictions)
```

## (Optional) Exercise 5: Implementing Adam Optimization From Scratch

rubric={accuracy:1}

Adam is an optimization algorithm that we'll be using a lot for the rest of the course. [Here's the original paper](https://arxiv.org/abs/1412.6980) that proposed it. It is essentially a fancier version of SGD. Without getting too technical, Adam really adds two additional features to SGD:
1. Momentum: which uses past gradients to help improve convergence speed, reduce noise in the path to the minimum, and avoid local minima.
2. Per-parameter learning rate: a learning rate is maintained and adapted for each parameter as iterations of optimization proceed.

Pretty cool! I recommend [reading this article](https://ruder.io/optimizing-gradient-descent/index.html) or [watching this video](https://www.youtube.com/watch?v=JXQT_vxqwIs) to learn more, but Adam boils down to the following equations:

Weight updating:

$$\mathbf{w}_{t+1} = \mathbf{w}_{t} - \frac{\alpha}{\sqrt{\hat{v}_{t}} + \epsilon} \hat{m}_{t}$$

The various components required for that equation:

$$\begin{align}
\hat{m}_{t} &= \frac{m_{t}}{1 - \beta_{1}^{t}}\\
\hat{v}_{t} &= \frac{v_{t}}{1 - \beta_{2}^{t}}
\end{align}$$

$$\begin{align}
m_{t} &= \beta_{1} m_{t-1} + (1 - \beta_{1}) g_{t}\\
v_{t} &= \beta_{2} v_{t-1} + (1 - \beta_{2}) g_{t}^{2}
\end{align}$$

Where:
- $t$ is the iteration of optimization, it increments up by one each time you update $\mathbf{w}$. Note that in the equation for $\hat{m}_{t}$ and $\hat{v}_{t}$, $\beta_{1}$ and $\beta_{2}$ are raised to the power of $t$.
- $g_{t}$ is the gradient of the loss function w.r.t to the parameters $w$.
- $m_{t}$ is known as the first moment (the mean) of the gradients. Initialize as 0.
- $v_{t}$ is known as the second moment (the uncentered variance) of the gradients. Initialize as 0.
- $\alpha$ is the learning rate. 0.1 is a good start.
- $\epsilon$ is just a term to prevent division by zero. Default: $10^{-8}$.
- $\beta_{1}$ is a hyperparameter that controls the influence of past gradients on subsequent updates. Default: $0.9$.
- $\beta_{2}$ is a hyperparameter that controls the influence of past gradients on subsequent updates. Default: $0.999$.

Here's a squiggly function for you to try and find the minimum parameter for. I've hard-coded the "optimum parameter" as $w_{opt}=4$ but I want you to find this value using Adam optimization and starting at $w \neq w_{opt}$. I've provided you the function (`f()`), the MSE loss w.r.t this function (`loss()`), and the gradient of the loss (`loss_grad()`). Run the cell below:

```python
def f(w, X):
    """Squiggly function"""
    return w * np.cos(w * X)

def loss(w, X, y):
    """MSE loss."""
    return np.mean((f(w, X) - y) ** 2)

def loss_grad(w, X, y):
    """Gradient of MSE."""
    t = np.cos(w * X) - w * X * np.sin(w * X)
    return np.mean((f(w, X) - y) * t)

w_opt = 4
X = np.arange(-3, 3, 0.1)
y = f(w_opt, X)
l = [loss(w, X, y) for w in np.arange(-10, 11, 0.1)]
plt.plot(np.arange(-10, 11, 0.1), l)
plt.xlabel("w")
plt.ylabel("MSE")
plt.grid(True);
```

Your task here is to implement Adam from scratch. Then use it to find $w_{opt}$ for the above function. I've provided some code below that you should run when you're ready. Note:
- I've specified a default of 100 epochs. We have a *tiny* dataset here of 60 samples so this is nothing really. Feel free to add more epochs if you wish.
- You can start with the default values for the various Adam terms I give in the equations above.
- You *may* need to play around with the hyperparameter $\alpha$ to get to the minimum (I've given a default of 0.3 in the starter code below - I didn't need to change this value in my solution). You can leave $\beta_{1}$, $\beta_{2}$ as is - often we don't tune those ones and I didn't in my solution, but you can tune them if you want.
- Adam is generally used with batches like SGD so my solution has the ability to accept a `batch_size` argument, but you don't have to code up this functionality and I didn't include that argument in the starter code below. So feel free to just use all the data each iteration for simplicity like vanilla GD would do. But if you're feeling adventurous, thrown in a `batch_size` argument 😉

>The pedagogical goal here is to get you to implement Adam and play around with it to see how it can "jump over" local minima. If you get it working, it's pretty awesome!

```python
def Adam(X, y, w0, loss, loss_grad, n_epochs=100, alpha=0.3, beta1=0.9, beta2=0.999, eta=10e-8):
    # Your code goes here.
    w = None
    return w

w0 = [9]
w = Adam(X, y, w0, loss, loss_grad)
print(w)  # Should be close to 4

```

## (Optional) Exercise 6: Gif or Jiff

rubric={accuracy:1}

Practically a free mark here for making it to the end. But I do have a question for you.

.gif files are animations made of a series of images. For example, this is me when someone tells me they have 80% accuracy with their ML model:

My question to you - are these files pronounced "Gif" or "Jiff"?

Your answer here:

## Submit to Canvas and GitHub

When you are ready to submit your assignment do the following:
1. Run all cells in your notebook to make sure there are no errors by doing `Kernel -> Restart Kernel and Run All Cells...`
2. Save your notebook.
3. Convert your notebook to `.html` format using the `convert_notebook()` function below or by `File -> Export Notebook As... -> Export Notebook to HTML`
4. Run the code `submit()` below to go through an interactive submission process to Canvas.
5. Finally, push all your work to GitHub (including the rendered html file).

```python
# convert_notebook("lab2.ipynb", "html")  # save your notebook, then uncomment and run when you want to convert to html
```

```python
# submit(course_code=59090)  # uncomment and run when ready to submit to Canvas
```


# Source:

These solutions are based on the answers provided by Adrian Monninger, with very slight modifications.

# Problem Set 2-3: Plots to Explore the Perfect Foresight Model

## Exercise 1:
PerfForesightCRRA derives a number of results as approximations; for instance, the exact formula for the consumption function is derived as
\begin{align}
    c_t = \Big(\frac{R-(R\beta)^{1/\rho}}{R}\Big)o_t
\end{align}
and approximated by
\begin{align}
    c_t \approx (r - \rho^{-1}(r - \mathscr{v}))o_t
\end{align}
and the saving rate is approximated in the last section.

For plausible values of the model parameters that satisfy the relevant impatience and human wealth conditions, make some plots examining the quality of those approximations. In particular, explore the relationship between the impatience conditions and the accuracy of the approximations.

### Answer:
The MPC ($\kappa$) depends on R, beta and rho. These parameters make a difference between true and approximated consumption. We observe different values for each parameter and assess the differences between the actual calculation of consumption and the approximated one.

In doing so, we observe three different cases for thorn:
1. thorn<1: "absolute impatience": consumer deliberately spends so much today that she will have to cut future spendings.
1. thorn>1: "absolute patience": the consumer wants to defer resources into the future in order to achieve consumption growth.
1. thorn=1: "absolutely poised"

Therefore, we have to make sure that Finite Human Wealth Condition (FHWC): $G<R$ and Relevant Impatience Condition (RIC): $thorn <R$ hold. While the first one is easy to check, as we only look at one period with a fixed human wealth coefficient. The second condition is important for the range of the parameters we choose.

Savings are defined as
\begin{align}
    s_t = ra_{t-1} + p_t - c_t
\end{align}

Hence, approximation errors in consumption translate into errors in savings

```python
import numpy as np
import math
import matplotlib.pyplot as plt
# case 1: thorn <1: "absolute impatience": consumer deliberately spends so much today that she will have to cut future spendings.

# previous savings, income, and Growth rate are fixed:
G = 1.01 # Due to FHWC
a = 5
p = 10

# a) variable beta
R = 1.03 # due to RIC: thorn should be smaller than R
r = R-1
beta_1 = np.linspace(0.5, 1/R, 50) # reasonable values: beta min=0.5
rho = 2

# define variables
v_1 = (1 - beta_1)/beta_1
thorn_1 = (𝚁*beta_1)**(1/rho)
thorn_R_1 = thorn_1/R

# Assume agent starts with no bank balances: overall wealth is just human wealth.

o = (p/(1-(G/R)))
# calculate c for different values of DiscFac
c_1 = o*(R-thorn_1)/R
# approximate c
c_app_1 = (r - (1/rho)*(r - v_1))*o

# savings
s_1 = r*a + p - c_1
s_app_1 = r*a + p - c_app_1

# b) variable R
beta = 0.95
rho = 5
R_2 = np.linspace(1, 1/beta, 50) # Due to RIC
r_2 = R_2-1
G_2 = np.linspace(0.4, (1/beta)-0.1, 50) # due to reasonable FHWC
# define variables
v_2 = (1 - beta)/beta
thorn_2 = (𝚁_2*beta)**(1/rho)
thorn_R_2 = thorn_2/R_2
o = (p/(1-(G_2/R_2)))

c_2 = o*(R_2-thorn_2)/R_2
# approximate c
c_app_2 = (r_2 - (1/rho)*(r_2 - v_2))*o

# savings
s_2 = r_2*a + p - c_2
s_app_2 = r_2*a + p - c_app_2

# c) variable rho
beta = 0.96
R = 1.03
r = R-1
rho_3 = np.linspace(.5, 20, 50)

# define variables
v_3 = (1 - beta)/beta
thorn_3 = (𝚁*beta)**(1/rho_3)
thorn_R_3 = thorn_3/R
o = (p/(1-(G/R)))

c_3 = o*(R-thorn_3)/R
# approximate c
c_app_3 = (r - (1/rho_3)*(r - v_3))*o

# savings
s_3 = r*a + p - c_3
s_app_3 = r*a + p - c_app_3

print('case 1: thorn <1: "absolute impatience"')

# plot
fig, ax = plt.subplots(3,3,figsize=[15,9])

# beta
#consumption
ax[0,0].plot(beta_1, c_1, label='c')
ax[0,0].plot(beta_1, c_app_1, label='c (approx)')
ax[0,0].set_xlabel('beta')
ax[0,0].set_ylabel('consumption')
ax[0,0].title.set_text('Variable beta')
ax[0,0].legend()

# saving
ax[1,0].plot(beta_1,s_1, label='s')
ax[1,0].plot(beta_1,s_app_1, label='s (approx)')
ax[1,0].set_xlabel('beta')
ax[1,0].set_ylabel('savings')
ax[1,0].title.set_text('Variable beta')
ax[1,0].legend()

# thorn
ax[2,0].plot(beta_1,thorn_1, label='thorn')
ax[2,0].plot(beta_1,thorn_R_1, label='thorn_R')
ax[2,0].set_xlabel('beta')
ax[2,0].set_ylabel('thorn')
ax[2,0].title.set_text('Variable beta')
ax[2,0].legend()

# R
#consumption
ax[0,1].plot(R_2,c_2, label='c')
ax[0,1].plot(R_2,c_app_2, label='c (approx)')
ax[0,1].set_xlabel('R')
ax[0,1].set_ylabel('consumption')
ax[0,1].title.set_text('Variable R')
ax[0,1].legend()

# saving
ax[1,1].plot(R_2,s_2, label='s')
ax[1,1].plot(R_2,s_app_2, label='s (approx)')
ax[1,1].set_xlabel('R')
ax[1,1].set_ylabel('savings')
ax[1,1].title.set_text('Variable R')
ax[1,1].legend()

# thorn
ax[2,1].plot(R_2,thorn_2, label='thorn')
ax[2,1].plot(R_2, thorn_R_2, label='thorn_R')
ax[2,1].set_xlabel('R')
ax[2,1].set_ylabel('thorn')
ax[2,1].title.set_text('Variable R')
ax[2,1].legend()

# rho
#consumption
ax[0,2].plot(rho_3,c_3, label='c')
ax[0,2].plot(rho_3,c_app_3, label='c (approx)')
ax[0,2].set_xlabel('rho')
ax[0,2].set_ylabel('consumption')
ax[0,2].title.set_text('Variable rho')
ax[0,2].legend()

# saving
ax[1,2].plot(rho_3,s_3, label='s')
ax[1,2].plot(rho_3,s_app_3, label='s (approx)')
ax[1,2].set_xlabel('rho')
ax[1,2].set_ylabel('savings')
ax[1,2].title.set_text('Variable rho')
ax[1,2].legend()

ax[2,2].plot(rho_3,thorn_3, label='thorn')
ax[2,2].plot(rho_3,thorn_R_3, label='thorn_R')
ax[2,2].set_xlabel('rho')
ax[2,2].set_ylabel('consumption')
ax[2,2].title.set_text('Variable rho')
ax[2,2].legend()

fig.tight_layout()
```

```python
# case 2: thorn >1: "absolute patience": the consumer wants to defer resources into the future in order to achieve consumption growth.
# previous savings, income, and Growth rate are fixed:
G = 1.01
a = 5
p = 10

# a) variable beta
R = 1.03
r = R-1
beta_1 = np.linspace(1/R, 1.5, 50) # reasonable values: beta max=1.5
rho = 5

# define variables
v_1 = (1 - beta_1)/beta_1
thorn_1 = (𝚁*beta_1)**(1/rho)
thorn_R_1 = thorn_1/R
o = (p/(1-(G/R)))

# calculate c for different values of DiscFac
c_1 = o*(R-thorn_1)/R
# approximate c
c_app_1 = (r - (1/rho)*(r - v_1))*o

# savings
s_1 = r*a + p - c_1
s_app_1 = r*a + p - c_app_1

# b) variable R
beta = 1.01
rho = 5
R_2 = np.linspace(1/beta, 1.5, 50) #max 1 due to RIC
G_2 = np.linspace((1/beta)-0.1, 1-0.1, 50) # due to reasonable FHWC
r_2 = R_2-1

# define variables
v_2 = (1 - beta)/beta
thorn_2 = (𝚁_2*beta)**(1/rho)
thorn_R_2 = thorn_2/R_2
o = (p/(1-(G_2/R_2)))

# calculate c for different values R
c_2 = o*(R_2-thorn_2)/R_2
# approximate c
c_app_2 = (r_2 - (1/rho)*(r_2 - v_2))*o

# savings
s_2 = r_2*a + p - c_2
s_app_2 = r_2*a + p - c_app_2

# c) variable rho
beta = 0.96
R = 1.1
r = R-1
rho_3 = np.linspace(0.5, 20, 50)

# define variables
v_3 = (1 - beta)/beta
thorn_3 = (𝚁*beta)**(1/rho_3)
thorn_R_3 = thorn_3/R
o = (p/(1-(G/R)))

# calculate c for different values of R
c_3 = o*(R-thorn_3)/R
# approximate c
c_app_3 = (r - (1/rho_3)*(r - v_3))*o

# savings
s_3 = r*a + p - c_3
s_app_3 = r*a + p - c_app_3

print('case 2: thorn >1: "absolute patience"')

# plot
fig, ax = plt.subplots(3,3,figsize=[15,9])

# beta
#consumption
ax[0,0].plot(beta_1, c_1, label='c')
ax[0,0].plot(beta_1, c_app_1, label='c (approx)')
ax[0,0].set_xlabel('beta')
ax[0,0].set_ylabel('consumption')
ax[0,0].title.set_text('Variable beta')
ax[0,0].legend()

# saving
ax[1,0].plot(beta_1,s_1, label='s')
ax[1,0].plot(beta_1,s_app_1, label='s (approx)')
ax[1,0].set_xlabel('beta')
ax[1,0].set_ylabel('savings')
ax[1,0].title.set_text('Variable beta')
ax[1,0].legend()

# thorn
ax[2,0].plot(beta_1,thorn_1, label='thorn')
ax[2,0].plot(beta_1,thorn_R_1, label='thorn_R')
ax[2,0].set_xlabel('beta')
ax[2,0].set_ylabel('thorn')
ax[2,0].title.set_text('Variable beta')
ax[2,0].legend()

# R
#consumption
ax[0,1].plot(R_2,c_2, label='c')
ax[0,1].plot(R_2,c_app_2, label='c (approx)')
ax[0,1].set_xlabel('R')
ax[0,1].set_ylabel('consumption')
ax[0,1].title.set_text('Variable R')
ax[0,1].legend()

# saving
ax[1,1].plot(R_2,s_2, label='s')
ax[1,1].plot(R_2,s_app_2, label='s (approx)')
ax[1,1].set_xlabel('R')
ax[1,1].set_ylabel('savings')
ax[1,1].title.set_text('Variable R')
ax[1,1].legend()

# thorn
ax[2,1].plot(R_2,thorn_2, label='thorn')
ax[2,1].plot(R_2, thorn_R_2, label='thorn_R')
ax[2,1].set_xlabel('R')
ax[2,1].set_ylabel('thorn')
ax[2,1].title.set_text('Variable R')
ax[2,1].legend()

# rho
#consumption
ax[0,2].plot(rho_3,c_3, label='c')
ax[0,2].plot(rho_3,c_app_3, label='c (approx)')
ax[0,2].set_xlabel('rho')
ax[0,2].set_ylabel('consumption')
ax[0,2].title.set_text('Variable rho')
ax[0,2].legend()

# saving
ax[1,2].plot(rho_3,s_3, label='s')
ax[1,2].plot(rho_3,s_app_3, label='s (approx)')
ax[1,2].set_xlabel('rho')
ax[1,2].set_ylabel('savings')
ax[1,2].title.set_text('Variable rho')
ax[1,2].legend()

ax[2,2].plot(rho_3,thorn_3, label='thorn')
ax[2,2].plot(rho_3,thorn_R_3, label='thorn_R')
ax[2,2].set_xlabel('rho')
ax[2,2].set_ylabel('consumption')
ax[2,2].title.set_text('Variable rho')
ax[2,2].legend()

fig.tight_layout()
```

```python
# case 3: thorn = 1: absolutely poised
# previous savings, income are fixed:
a = 5
p = 10

# a) variable beta and R
R_1 = np.linspace(1, 1.5, 50) # R > thorn = 1 (RIC)
r_1 = R_1-1
beta_1 = np.linspace(1, 1/1.5, 50)
rho = 5
G_1 = np.linspace(0.9, 1.4, 50) # due to reasonable FHWC
# define variables
v_1 = (1 - beta_1)/beta_1
thorn_1 = (𝚁_1*beta_1)**(1/rho)
thorn_R_1 = thorn_1/R_1
o = (p/(1-(G_1/R_1)))

# calculate c for different values of DiscFac
c_1 = o*(R_1-thorn_1)/R_1
# approximate c
c_app_1 = (r_1 - (1/rho)*(r_1 - v_1))*o

# b) variable rho
R = 1.04 # due to RIC
G = 1.01 # due to FHWC
beta = 1/R
r = R-1
rho_3 = np.linspace(0.5, 20, 50)

# define variables
v_3 = (1 - beta)/beta
thorn_3 = (𝚁*beta)**(1/rho_3)
thorn_R_3 = thorn_3/R
o = (p/(1-(G/R)))

# calculate c for different values of DiscFac
c_3 = o*(R-thorn_3)/R
# approximate c
c_app_3 = (r - (1/rho_3)*(r - v_3))*o

print('case 3: thorn = 1: absolutely poised')
# plot
fig, ax = plt.subplots(3,2,figsize=[15,9])

# beta
#consumption
ax[0,0].plot(beta_1, c_1, label='c')
ax[0,0].plot(beta_1, c_app_1, label='c (approx)')
ax[0,0].set_xlabel('beta')
ax[0,0].set_ylabel('consumption')
ax[0,0].title.set_text('Variable beta')
ax[0,0].legend()

# saving
ax[1,0].plot(beta_1,s_1, label='s')
ax[1,0].plot(beta_1,s_app_1, label='s (approx)')
ax[1,0].set_xlabel('beta')
ax[1,0].set_ylabel('savings')
ax[1,0].title.set_text('Variable beta')
ax[1,0].legend()

# thorn
ax[2,0].plot(beta_1,thorn_1, label='thorn')
ax[2,0].plot(beta_1,thorn_R_1, label='thorn_R')
ax[2,0].set_xlabel('beta')
ax[2,0].set_ylabel('thorn')
ax[2,0].title.set_text('Variable beta')
ax[2,0].legend()

# rho
#consumption
ax[0,1].plot(rho_3,c_3, label='c')
ax[0,1].plot(rho_3,c_app_3, label='c (approx)')
ax[0,1].set_xlabel('rho')
ax[0,1].set_ylabel('consumption')
ax[0,1].title.set_text('Variable rho')
ax[0,1].legend()

# saving
ax[1,1].plot(rho_3,s_3, label='s')
ax[1,1].plot(rho_3,s_app_3, label='s (approx)')
ax[1,1].set_xlabel('rho')
ax[1,1].set_ylabel('savings')
ax[1,1].title.set_text('Variable rho')
ax[1,1].legend()

ax[2,1].plot(rho_3,thorn_3, label='thorn')
ax[2,1].plot(rho_3,thorn_R_3, label='thorn_R')
ax[2,1].set_xlabel('rho')
ax[2,1].set_ylabel('consumption')
ax[2,1].title.set_text('Variable rho')
ax[2,1].legend()

fig.tight_layout()
```

### Conclusion
This exercise shows that the approximation is very close to the true consumption if beta and R are close to 1 (which makes sense, based on the way the approximation is done). In specific, values for R and beta which are more than 30% off are critical.

Interestingly, rho does not play such a large role. While for large $\rho$’s the difference between the consumption series is visible, it does not increase at a certain level.

For savings, this problem holds as well, based on the fact, that savings is the difference between income and consumption.

To sum up, the consumption approximation works for most reasonable cases. Reasonable means that interest rates are usually less than 10% and impatience between periods is not very large.

## Exercise 2:
Make some plots that illustrate the points made in sections 4.1 and 4.2 of PerfForesightCRRA about the size of the human wealth effect and the relationship between interest rates and the saving rate.

### a) Size of human wealth effect
We take equation (35) and choose different values for $\beta$($v$), $r$, $\rho$, $g$. This shows how the size of the human wealth effect depends on each parameter. Additionally it shows that the human wealth effect is large.
To not run into trouble, we make sure that the RIC (thorn_g <0) and FHIWC (r>g) holds

```python
# variable beta
p = 10
r = 0.04
beta_1 = np.linspace(0.5, 0.99, 50)
v_1 = (1 - beta_1)/beta_1
g = 0.02
rho = 2

thorn_g = rho**(-1) * (r - v_1) - g
c_1 = p*(1 - thorn_g/(r - g))

# variable R
p = 10
r_2 = np.linspace(0.021, 0.99, 50)
beta = 0.96
v = (1 - beta)/beta
g = 0.02
rho = 2

thorn_g = rho**(-1) * (r_2 - v) - g
c_2 = p*(1 - thorn_g/(r_2 - g))

# variable rho
p = 10
r = 0.04
beta = 0.96
v = (1 - beta)/beta
g = 0.02
rho_3 = np.linspace(0.5, 10, 50)

thorn_g = rho_3**(-1) * (r - v) - g
c_3 = p*(1 - thorn_g/(r - g))

# variable g
p = 10
r = 0.04
beta = 0.96
v = (1 - beta)/beta
g_4 = np.linspace(0.001, 0.039, 50)
rho = 2

thorn_g = rho**(-1) * (r - v) - g_4
c_4 = p*(1 - thorn_g/(r - g_4))

# plot
fig, bx = plt.subplots(2,2,figsize=[15,9])

# beta
bx[0,0].plot(beta_1, c_1, label='human wealth')
bx[0,0].set_xlabel('beta')
bx[0,0].set_ylabel('Consumption')
bx[0,0].title.set_text('Variable beta')
bx[0,0].legend()

#R
bx[0,1].plot(r_2,c_2, label='human wealth')
bx[0,1].set_xlabel('r')
bx[0,1].set_ylabel('Consumption')
bx[0,1].title.set_text('Variable r')
bx[0,1].legend()

# rho
bx[1,0].plot(rho_3,c_3, label='human wealth')
bx[1,0].set_xlabel('rho')
bx[1,0].set_ylabel('Consumption')
bx[1,0].title.set_text('Variable rho')
bx[1,0].legend()

# g
bx[1,1].plot(g_4,c_4, label='human wealth')
bx[1,1].set_xlabel('g')
bx[1,1].set_ylabel('Consumption')
bx[1,1].title.set_text('Variable g')
bx[1,1].legend()

fig.tight_layout()
```

### Conclusion:
We can see that the human wealth effect is indeed sizable!

However, each parameter has a different influence on it. While this is already visible from the formula, the pictures illustrate the relationship very nicely.

1. A smaller beta increases the human wealth effect.
Beta is the patience factor. If you are less patient (smaller beta), the agent will consume more today. This increases the effect.
1. rho increases the human wealth effect if little
A rho close to 0 means that the agent does not care in which period consumption should take place. Hence, the human wealth effect gets larger, as less income needs to be saved.
1. r is only important if it gets close to g
1. g increases the effect if it gets closer to r
When r = g, interest rates and income growth have the same size. Therefore, the agent converges to the finite human wealth condition. If this is true, the present discount value of future income would be infinite.

### Relationship between interest rates and saving rate
\begin{align}
    \zeta_t = \frac{thorn_g/(r - g) + \rho^{-1}(r - v)a_{t-1}}{1 + ra_{t-1}}
\end{align}

The points which are made are:
1. When $a_{t-1}$ approaches infinity, the saving rate converges to a constant
1. sign of the saving rate depends on thorn for a large enough $a_{t-1}$
1. The response of the saving rate to the interest rate depends on $\rho^{-1}vr^{-2}$

#### 1. When  $a_{t-1}$  approaches infinity, the saving rate converges to a constant

```python
# inputs
p = 10
r = 0.04
beta = 0.96
v = (1 - beta)/beta
g = 0.02
rho = 2
a_1 = np.linspace(0.5, 200, 50)

thorn_g = rho**(-1)*(r - v) - g
zeta = (thorn_g/(r-g) + (rho**(-1))*(r-v)*a_1)/(1 + r*a_1)

# plot
plt.figure(figsize=(9,6))
plt.plot(a_1, zeta, label = 'saving rate')
plt.title('Saving rate and increasing assets')
plt.xlabel('assets')
plt.ylabel('savingrate')
plt.legend()
plt.show()
```

## Conclusion
As predicted, the saving rate converges to a constant with increasing assets.

#### 2. sign of the saving rate depends on thorn

```python
#1. thorn<1
# inputs
p = 10
r = 0.04
R = 1+r
beta = 0.96
v = (1 - beta)/beta
g = 0.02
rho = 2
a_1 = np.linspace(0.5, 200, 50)

thorn_1 = (𝚁*beta)**(1/rho)
thorn_g = rho**(-1)*(r - v) - g
zeta_1 = (thorn_g/(r-g) + (rho**(-1))*(r-v)*a_1)/(1 + r*a_1)

#2. thorn>1
# inputs
p = 10
r = 0.04
R = 1+r
beta = 1.2
v = (1 - beta)/beta
g = 0.02
rho = 2
a_1 = np.linspace(0.5, 200, 50)

thorn_2 = (𝚁*beta)**(1/rho)
thorn_g = rho**(-1)*(r - v) - g
zeta_2 = (thorn_g/(r-g) + (rho**(-1))*(r-v)*a_1)/(1 + r*a_1)

#3. thorn=1
# inputs
p = 10
r = 0.04
R = 1+r
beta = 1/R
v = (1 - beta)/beta
g = 0.02
rho = 2
a_1 = np.linspace(0.5, 200, 50)

thorn_3 = (𝚁*beta)**(1/rho)
thorn_g = rho**(-1)*(r - v) - g
zeta_3 = (thorn_g/(r-g) + (rho**(-1))*(r-v)*a_1)/(1 + r*a_1)

# plot
plt.figure(figsize=(9,6))
plt.plot(a_1, zeta_1, label = 'thorn<1')
plt.plot(a_1, zeta_2, label = 'thorn>1')
plt.plot(a_1, zeta_3, label = 'thorn=1')
plt.title('Saving rate and increasing assets')
plt.xlabel('assets')
plt.ylabel('savingrate')
plt.legend()
plt.show()
```

### Conclusion:
Indeed, the sign of the savingrate depends on thorn.
1. A thorn<1 means that the agent is absolute impatient. Therefore, consumption should take place rather sooner than later. Hence, the household dissaves if possible.
2. Thorn >1 means that the agent is absolute patient. Hence, savings increase the utility, as returns can be consumed later in life.

#### 3. The response depends on $\rho^{-1}vr^{-2}$

```python
# variable rho
rho_1 = np.linspace(0.5, 10, 50)
v = 0.05
r = 0.05

d_zeta_1 = rho_1**(-1)*v*r**(-2)
# variable v
rho = 2
v_2 = np.linspace(0.01, 0.1, 50)
r = 0.05

d_zeta_2 = rho**(-1)*v_2*r**(-2)
# variable r
rho = 2
v = 0.05
r_3 = np.linspace(0.01, 0.1, 50)

d_zeta_3 = rho**(-1)*v*r_3**(-2)

# plot
fig, bx = plt.subplots(2,2,figsize=[15,9])

# rho
bx[0,0].plot(rho_1, d_zeta_1, label='d savingrate')
bx[0,0].set_xlabel('rho')
bx[0,0].set_ylabel('d savingrate')
bx[0,0].title.set_text('Variable rho')
bx[0,0].legend()

#v
bx[0,1].plot(v_2,d_zeta_2, label='d savingrate')
bx[0,1].set_xlabel('v')
bx[0,1].set_ylabel('d savingrate')
bx[0,1].title.set_text('Variable v')
bx[0,1].legend()

# rho
bx[1,0].plot(r_3,d_zeta_3, label='d savingrate')
bx[1,0].set_xlabel('r')
bx[1,0].set_ylabel('d savingrate')
bx[1,0].title.set_text('Variable r')
bx[1,0].legend()

fig.tight_layout()
```

### Conclusion:
the response of the saving rate depends on v in a linear form. Additionally, the smaller rho and r, the larger the response.
1. The larger v, the smaller beta which is our patience factor. If the agent gets less patient, a change in r should increase savings larger.
1. the smaller rho, the larger the effect. Again, rho close to zero makes the houseold poised, with respect to when to consume. We now get the same results as for problem set 1.2, where we have seen that the effect depends on R*beta.
1. A interest rate close to zero amplifies the effect. As we have an infinite horizon model, a small increase from a small interest rate, can have large effects over many periods.

```python

```


Euler Problem 60
================

The primes 3, 7, 109, and 673, are quite remarkable. By taking any two primes and concatenating them in any order the result will always be prime. For example, taking 7 and 109, both 7109 and 1097 are prime. The sum of these four primes, 792, represents the lowest sum for a set of four primes with this property.

Find the lowest sum for a set of five primes for which any two primes concatenate to produce another prime.

```python
from time import time
start = time()

from sympy import primerange, isprime

primes = list(primerange(2, 50000))
N = len(primes)
minsum = 1e100

def concat(p, q):
    return int(f'{p}{q}')

def edge(p, q):
    if (p,q) in E:
        return E[p,q]
    answer = isprime(concat(p,q)) and isprime(concat(q,p))
    E[p,q] = answer
    return answer

cliques = [([], 0)]

for p in primes:
    E = {}
    if p >= minsum:
        break
    new_cliques = []
    for clique, weight in cliques:
        if all(edge(p,q) for q in clique):
            new_clique = clique + [p]
            if len(new_clique) == 5:
                minsum = min(minsum, weight + p)
            new_cliques.append((new_clique, weight + p))
    cliques.extend(new_cliques)

print(minsum)
print("Time: %.2f seconds"  % (time() - start))
```

    26033
    Time: 293.43 seconds

```python

```


# Object Ranking

```python
import warnings
warnings.filterwarnings('ignore')
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from csrank import FATEObjectRanker
```

    Using TensorFlow backend.

## The Medoid Ranking Problem

In the medoid problem the goal of the learner is to sort a set of randomly generated points based on their distance to the _medoid_.
This problem is inspired by the setting of ranking similarity learning, where the goal is to learn a similarity function from triplets of objects.

We will generate a random dataset where each instance contains 5 objects and 2 features for easy plotting.

```python
from csrank import ObjectRankingDatasetGenerator
seed = 123
n_train = 10000
n_test = 10000
n_features = 2
n_objects = 5
gen = ObjectRankingDatasetGenerator(dataset_type='medoid', random_state=seed,
                                n_train_instances=n_train,
                                n_test_instances=n_test,
                                n_objects=n_objects,
                                n_features=n_features)
```

```python
X_train, Y_train, X_test, Y_test = gen.get_single_train_test_split()
```

Let us plot a random instance. The points are ranked by distance to the medoid. The medoid itself gets always gets the index 0.

```python
fig, ax = plt.subplots(figsize=(5,5))
inst = np.random.choice(n_train)
ax.scatter(X_train[inst][:, 0], X_train[inst][:, 1])
for i in range(n_objects):
    ax.text(X_train[inst, i, 0]+0.01,
            X_train[inst, i, 1]+0.01,
            s=int(Y_train[inst, i]))
```

## The FATE network
The first-aggregate-then-evaluate approach learns an embedding of each object and then aggregates that into a _context_:
\begin{equation}
	\mu_{C(\vec{x})} = \frac{1}{|C(\vec{x})|} \sum_{\vec{y} \in C(\vec{x})} \phi(\vec{y})
\end{equation}
and then scores each object $\vec{x}$ using a generalized utility function $U (\vec{x}, \mu_{C(\vec{x})})$

```python
from keras.optimizers import SGD
from csrank import FATEObjectRanker
from csrank.losses import smooth_rank_loss
fate = FATEObjectRanker(
    n_object_features=n_features,
    loss_function=smooth_rank_loss,
    optimizer=SGD(lr=1e-4, nesterov=True, momentum=0.9))
```

We will run the training for only 10 epochs to get an idea of the convergence:

```python
fate.fit(X_train, Y_train, verbose=True, epochs=10)
```

    Train on 9000 samples, validate on 1000 samples
    Epoch 1/10
    9000/9000 [==============================] - 8s 848us/step - loss: 11.3143 - zero_one_rank_loss_for_scores_ties: 0.4318 - val_loss: 11.2180 - val_zero_one_rank_loss_for_scores_ties: 0.3547
    Epoch 2/10
    9000/9000 [==============================] - 1s 99us/step - loss: 11.2005 - zero_one_rank_loss_for_scores_ties: 0.3448 - val_loss: 11.1587 - val_zero_one_rank_loss_for_scores_ties: 0.3164
    Epoch 3/10
    9000/9000 [==============================] - 1s 112us/step - loss: 11.1526 - zero_one_rank_loss_for_scores_ties: 0.3150 - val_loss: 11.1150 - val_zero_one_rank_loss_for_scores_ties: 0.2932
    Epoch 4/10
    9000/9000 [==============================] - 1s 88us/step - loss: 11.1175 - zero_one_rank_loss_for_scores_ties: 0.2964 - val_loss: 11.0819 - val_zero_one_rank_loss_for_scores_ties: 0.2779
    Epoch 5/10
    9000/9000 [==============================] - 1s 160us/step - loss: 11.0899 - zero_one_rank_loss_for_scores_ties: 0.2850 - val_loss: 11.0541 - val_zero_one_rank_loss_for_scores_ties: 0.2657
    Epoch 6/10
    9000/9000 [==============================] - 1s 128us/step - loss: 11.0661 - zero_one_rank_loss_for_scores_ties: 0.2773 - val_loss: 11.0332 - val_zero_one_rank_loss_for_scores_ties: 0.2572
    Epoch 7/10
    9000/9000 [==============================] - 1s 101us/step - loss: 11.0433 - zero_one_rank_loss_for_scores_ties: 0.2704 - val_loss: 11.0121 - val_zero_one_rank_loss_for_scores_ties: 0.2524
    Epoch 8/10
    9000/9000 [==============================] - 1s 117us/step - loss: 11.0227 - zero_one_rank_loss_for_scores_ties: 0.2654 - val_loss: 10.9913 - val_zero_one_rank_loss_for_scores_ties: 0.2477
    Epoch 9/10
    9000/9000 [==============================] - 1s 117us/step - loss: 11.0028 - zero_one_rank_loss_for_scores_ties: 0.2619 - val_loss: 10.9738 - val_zero_one_rank_loss_for_scores_ties: 0.2453
    Epoch 10/10
    9000/9000 [==============================] - 1s 116us/step - loss: 10.9843 - zero_one_rank_loss_for_scores_ties: 0.2590 - val_loss: 10.9541 - val_zero_one_rank_loss_for_scores_ties: 0.2416

```python
scores = fate.predict_scores(X_test)
```

```python
from csrank.metrics import zero_one_rank_loss_for_scores
from keras import backend as K
K.eval(zero_one_rank_loss_for_scores(Y_test, scores))
```

    0.26382047

Not converged yet, but let us visualize the scores it assigns to test instances:

```python
fig, ax = plt.subplots(figsize=(5,5))
inst = np.random.choice(n_test)
ax.scatter(X_test[inst][:, 0], X_test[inst][:, 1])
for i in range(n_objects):
    ax.text(X_test[inst, i, 0]+0.01,
            X_test[inst, i, 1]+0.01,
            s=int(Y_test[inst, i]))
    ax.text(X_test[inst, i, 0]+0.01,
            X_test[inst, i, 1]-0.025,
            s='{:.1f}'.format(scores[inst][i]),
            color='b')
```


# Is $Y$ normal?
In linear regression, notation such as the below equation can be somewhat puzzeling, if you are a beginner, like me:
$$ Y \sim \mathcal{N}(\alpha+\beta \cdot x,\,\epsilon)\ $$

What does this mean?

Does it mean that $ Y $ is now also normal distributed?

$$ Y \sim \mathcal{N}(\mu,\,\sigma^{2})\ $$

This notebook explores this a bit, in order to get a more clear understanding.

```python
# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python
# For example, here's several helpful packages to load

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
import matplotlib.pyplot as plt
import seaborn as sns
import sympy as sp
```

Lets consider a simple linear line:

```python
x,y,alpha,beta = sp.symbols('x y alpha beta')
eq = sp.Eq(y,
          alpha + beta*x)
eq
```

$\displaystyle y = \alpha + \beta x$

```python
## Converting sympy expression to python method:
lambda_eq = sp.lambdify([x,alpha,beta],eq.rhs)
lambda_eq
```

    <function _lambdifygenerated(x, alpha, beta)>

Here is a graph showing this line for 5 values of $x$:

```python
N_x = 5
N_epsilon = 1000
N = N_x*N_epsilon

xs = np.linspace(0,1, int(N_x))
x_ = np.tile(xs,N_epsilon)

df = pd.DataFrame(index=x_)
df['x'] = x_
df['alpha'] = 10
df['beta'] = 60
df['y'] = lambda_eq(x=df['x'], alpha=df['alpha'], beta=df['beta'])
```

```python
fig,ax=plt.subplots()
df.groupby(by='x').first().plot(y='y', style='o-', lw=2, ax=ax, zorder=2)
ax.set_ylabel('y')
ax.set_xlabel('x');
```

Let's pretend that this line represents something that we can measure from a physical experiment. We assume that the measuring error is normal distributed $ \epsilon \sim \mathcal{N}(0,\,\sigma^{2})\ $ so that the measured values of $y$ is expressed as:

```python
epsilon = sp.symbols('epsilon')
eq_probabalitic = sp.Eq(eq.lhs,
                       eq.rhs + epsilon)

eq_probabalitic
```

$\displaystyle y = \alpha + \beta x + \epsilon$

We measure $y$ 1000 times for each of the 5 values of $x$. This means that we will get 1000 values of the measuring error $\epsilon \sim \mathcal{N}(0,2^{2})\ $ for each $x$.

```python
np.random.seed(42)
df['epsilon'] = df.groupby('x')['y'].transform(lambda x: np.random.normal(loc=0, scale = 2.0, size=N_epsilon))
```

```python
## Converting sympy expression to python method:
lambda_eq_probabalitic = sp.lambdify([x,alpha,beta,epsilon],eq_probabalitic.rhs)
lambda_eq_probabalitic
```

    <function _lambdifygenerated(x, alpha, beta, epsilon)>

```python
df['y_measure'] = lambda_eq_probabalitic(x=df['x'], alpha=df['alpha'], beta=df['beta'], epsilon=df['epsilon'])

```

The measured values of $y$ is shown in the graph below:

```python
fig,ax=plt.subplots()
df.plot(y='y', style='-', lw=2, ax=ax, zorder=2)
df.plot(y='y_measure', style='.', alpha=0.05, ax=ax, zorder=1)
ax.set_ylabel('y')
ax.set_xlabel('x');
```

The measured $y$ has values distributed around the true values of $y$. These distributions becomes even more visible if we create histograms of the measured $y$:

```python
fig,ax=plt.subplots()
for the_x, group in df.groupby(by='x'):
    group.hist(column='y_measure', bins=20, ax=ax, label='x=%0.2f' % the_x)

ax.set_xlabel('y_measure')
ax.legend();
```

So it is very clear from the above graph that the measured $y$ is not normal distributed but the measurement error $\epsilon$ (being the deviation from the idealized line) is:

```python
ax_epsilon = sns.histplot(data=df, x="epsilon", stat="density", bins=20, kde=True);
#ax_epsilon.set_xlim(ax_y.get_xlim());
ax_epsilon.set_xlabel('Measure error: $\epsilon$');
```

The histogram of the idealized $y$ looks like this, by the way:

```python
ax_y = sns.histplot(data=df, x="y", stat="density", bins=20, kde=True);
```


# Algorithms Exercise 2

## Imports

```python
%matplotlib inline
from matplotlib import pyplot as plt
import seaborn as sns
import numpy as np
```

## Peak finding

Write a function `find_peaks` that finds and returns the indices of the local maxima in a sequence. Your function should:

* Properly handle local maxima at the endpoints of the input array.
* Return a Numpy array of integer indices.
* Handle any Python iterable as input.

```python
def find_peaks(a):
    """Find the indices of the local maxima in a sequence."""
    P=[]
    for i in range(0,len(a)):
        if i==0 and a[1]<a[0]:
            P.append(0)
        elif i==len(a)-1 and a[i-1]<a[i]:
            P.append(i)
        else:
            if a[i-1]<a[i] and a[i+1]<a[i]:
                P.append(i)
    return np.array(P)
```

```python
p1 = find_peaks([2,0,1,0,2,0,1])
assert np.allclose(p1, np.array([0,2,4,6]))
p2 = find_peaks(np.array([0,1,2,3]))
assert np.allclose(p2, np.array([3]))
p3 = find_peaks([3,2,1,0])
assert np.allclose(p3, np.array([0]))
```

Here is a string with the first 10000 digits of $\pi$ (after the decimal). Write code to perform the following:

* Convert that string to a Numpy array of integers.
* Find the indices of the local maxima in the digits of $\pi$.
* Use `np.diff` to find the distances between consequtive local maxima.
* Visualize that distribution using an appropriately customized histogram.

```python
from sympy import pi, N
pi_digits_str = str(N(pi, 10001))[2:]
```

```python
from IPython.display import display
```

```python
pi=find_peaks(pi_digits_str)
df=np.diff(pi)
plt.hist(df,range=(0,max(df)),bins=max(df))
plt.title('$\Pi$ Maximums')
plt.xlabel('Digits between Maxes')
```

```python
assert True # use this for grading the pi digits histogram
```