# DH matrix for the RA-02 4DOF Robot
The dh paramters can be found in this [paper](https://www.researchgate.net/publication/279201859_Kinematics_Modeling_of_a_4-DOF_Robotic_Arm)

```python
%autosave 120
#Robot links lengths in meters
L1 = 11.5e-2
L2 = 12e-2
L3 = 9e-2
L4 = 9e-2
```

    Autosaving every 120 seconds

```python
import sys
sys.path.append('../')
from dh import dh_solver
#create an object
ra02 = dh_solver()
```

```python
import sympy
from sympy import Symbol

#adding the dh parameters in this order [ d, theta, a, alpha]
ra02.add([L1    ,    Symbol("theta1"),     0 ,     sympy.pi/2   ])
ra02.add([0     ,    Symbol("theta2"),     L2,     0            ])
ra02.add([0     ,    Symbol("theta3"),     L3,     0            ])
ra02.add([0     ,    Symbol("theta4"),     L4,     0            ])

```

```python
#to get the dh matrices in symbolic form
T = ra02.calc_symbolic_matrices()
print(T)
```

    100%|██████████| 4/4 [00:00<00:00, 39.46it/s]

    Matrix([[((-sin(theta1)*sin(theta2)*cos(alpha1) + cos(theta1)*cos(theta2))*cos(theta3) + (sin(alpha1)*sin(alpha2)*sin(theta1) - sin(theta1)*cos(alpha1)*cos(alpha2)*cos(theta2) - sin(theta2)*cos(alpha2)*cos(theta1))*sin(theta3))*cos(theta4) + (-(-sin(theta1)*sin(theta2)*cos(alpha1) + cos(theta1)*cos(theta2))*sin(theta3)*cos(alpha3) + (sin(alpha1)*sin(alpha2)*sin(theta1) - sin(theta1)*cos(alpha1)*cos(alpha2)*cos(theta2) - sin(theta2)*cos(alpha2)*cos(theta1))*cos(alpha3)*cos(theta3) + (sin(alpha1)*sin(theta1)*cos(alpha2) + sin(alpha2)*sin(theta1)*cos(alpha1)*cos(theta2) + sin(alpha2)*sin(theta2)*cos(theta1))*sin(alpha3))*sin(theta4), -((-sin(theta1)*sin(theta2)*cos(alpha1) + cos(theta1)*cos(theta2))*cos(theta3) + (sin(alpha1)*sin(alpha2)*sin(theta1) - sin(theta1)*cos(alpha1)*cos(alpha2)*cos(theta2) - sin(theta2)*cos(alpha2)*cos(theta1))*sin(theta3))*sin(theta4)*cos(alpha4) + ((-sin(theta1)*sin(theta2)*cos(alpha1) + cos(theta1)*cos(theta2))*sin(alpha3)*sin(theta3) - (sin(alpha1)*sin(alpha2)*sin(theta1) - sin(theta1)*cos(alpha1)*cos(alpha2)*cos(theta2) - sin(theta2)*cos(alpha2)*cos(theta1))*sin(alpha3)*cos(theta3) + (sin(alpha1)*sin(theta1)*cos(alpha2) + sin(alpha2)*sin(theta1)*cos(alpha1)*cos(theta2) + sin(alpha2)*sin(theta2)*cos(theta1))*cos(alpha3))*sin(alpha4) + (-(-sin(theta1)*sin(theta2)*cos(alpha1) + cos(theta1)*cos(theta2))*sin(theta3)*cos(alpha3) + (sin(alpha1)*sin(alpha2)*sin(theta1) - sin(theta1)*cos(alpha1)*cos(alpha2)*cos(theta2) - sin(theta2)*cos(alpha2)*cos(theta1))*cos(alpha3)*cos(theta3) + (sin(alpha1)*sin(theta1)*cos(alpha2) + sin(alpha2)*sin(theta1)*cos(alpha1)*cos(theta2) + sin(alpha2)*sin(theta2)*cos(theta1))*sin(alpha3))*cos(alpha4)*cos(theta4), ((-sin(theta1)*sin(theta2)*cos(alpha1) + cos(theta1)*cos(theta2))*cos(theta3) + (sin(alpha1)*sin(alpha2)*sin(theta1) - sin(theta1)*cos(alpha1)*cos(alpha2)*cos(theta2) - sin(theta2)*cos(alpha2)*cos(theta1))*sin(theta3))*sin(alpha4)*sin(theta4) + ((-sin(theta1)*sin(theta2)*cos(alpha1) + cos(theta1)*cos(theta2))*sin(alpha3)*sin(theta3) - (sin(alpha1)*sin(alpha2)*sin(theta1) - sin(theta1)*cos(alpha1)*cos(alpha2)*cos(theta2) - sin(theta2)*cos(alpha2)*cos(theta1))*sin(alpha3)*cos(theta3) + (sin(alpha1)*sin(theta1)*cos(alpha2) + sin(alpha2)*sin(theta1)*cos(alpha1)*cos(theta2) + sin(alpha2)*sin(theta2)*cos(theta1))*cos(alpha3))*cos(alpha4) - (-(-sin(theta1)*sin(theta2)*cos(alpha1) + cos(theta1)*cos(theta2))*sin(theta3)*cos(alpha3) + (sin(alpha1)*sin(alpha2)*sin(theta1) - sin(theta1)*cos(alpha1)*cos(alpha2)*cos(theta2) - sin(theta2)*cos(alpha2)*cos(theta1))*cos(alpha3)*cos(theta3) + (sin(alpha1)*sin(theta1)*cos(alpha2) + sin(alpha2)*sin(theta1)*cos(alpha1)*cos(theta2) + sin(alpha2)*sin(theta2)*cos(theta1))*sin(alpha3))*sin(alpha4)*cos(theta4), a1*cos(theta1) - a2*sin(alpha2)*sin(theta1)*cos(alpha1) + a2*cos(theta1)*cos(theta2) + a3*(-sin(theta1)*sin(theta2)*cos(alpha1) + cos(theta1)*cos(theta2))*cos(theta3) + a3*(sin(alpha1)*sin(alpha2)*sin(theta1) - sin(theta1)*cos(alpha1)*cos(alpha2)*cos(theta2) - sin(theta2)*cos(alpha2)*cos(theta1))*sin(alpha3) + a4*((-sin(theta1)*sin(theta2)*cos(alpha1) + cos(theta1)*cos(theta2))*cos(theta3) + (sin(alpha1)*sin(alpha2)*sin(theta1) - sin(theta1)*cos(alpha1)*cos(alpha2)*cos(theta2) - sin(theta2)*cos(alpha2)*cos(theta1))*sin(theta3))*cos(theta4) + a4*(-(-sin(theta1)*sin(theta2)*cos(alpha1) + cos(theta1)*cos(theta2))*sin(theta3)*cos(alpha3) + (sin(alpha1)*sin(alpha2)*sin(theta1) - sin(theta1)*cos(alpha1)*cos(alpha2)*cos(theta2) - sin(theta2)*cos(alpha2)*cos(theta1))*cos(alpha3)*cos(theta3) + (sin(alpha1)*sin(theta1)*cos(alpha2) + sin(alpha2)*sin(theta1)*cos(alpha1)*cos(theta2) + sin(alpha2)*sin(theta2)*cos(theta1))*sin(alpha3))*sin(alpha4) + d2*sin(alpha1)*sin(theta1) + d3*(sin(alpha1)*sin(theta1)*cos(alpha2) + sin(alpha2)*sin(theta1)*cos(alpha1)*cos(theta2) + sin(alpha2)*sin(theta2)*cos(theta1)) + d4*((-sin(theta1)*sin(theta2)*cos(alpha1) + cos(theta1)*cos(theta2))*sin(alpha3)*sin(theta3) - (sin(alpha1)*sin(alpha2)*sin(theta1) - sin(theta1)*cos(alpha1)*cos(alpha2)*cos(theta2) - sin(theta2)*cos(alpha2)*cos(theta1))*sin(alpha3)*cos(theta3) + (sin(alpha1)*sin(theta1)*cos(alpha2) + sin(alpha2)*sin(theta1)*cos(alpha1)*cos(theta2) + sin(alpha2)*sin(theta2)*cos(theta1))*cos(alpha3))], [((sin(theta1)*cos(theta2) + sin(theta2)*cos(alpha1)*cos(theta1))*cos(theta3) + (-sin(alpha1)*sin(alpha2)*cos(theta1) - sin(theta1)*sin(theta2)*cos(alpha2) + cos(alpha1)*cos(alpha2)*cos(theta1)*cos(theta2))*sin(theta3))*cos(theta4) + (-(sin(theta1)*cos(theta2) + sin(theta2)*cos(alpha1)*cos(theta1))*sin(theta3)*cos(alpha3) + (-sin(alpha1)*sin(alpha2)*cos(theta1) - sin(theta1)*sin(theta2)*cos(alpha2) + cos(alpha1)*cos(alpha2)*cos(theta1)*cos(theta2))*cos(alpha3)*cos(theta3) + (-sin(alpha1)*cos(alpha2)*cos(theta1) + sin(alpha2)*sin(theta1)*sin(theta2) - sin(alpha2)*cos(alpha1)*cos(theta1)*cos(theta2))*sin(alpha3))*sin(theta4), -((sin(theta1)*cos(theta2) + sin(theta2)*cos(alpha1)*cos(theta1))*cos(theta3) + (-sin(alpha1)*sin(alpha2)*cos(theta1) - sin(theta1)*sin(theta2)*cos(alpha2) + cos(alpha1)*cos(alpha2)*cos(theta1)*cos(theta2))*sin(theta3))*sin(theta4)*cos(alpha4) + ((sin(theta1)*cos(theta2) + sin(theta2)*cos(alpha1)*cos(theta1))*sin(alpha3)*sin(theta3) - (-sin(alpha1)*sin(alpha2)*cos(theta1) - sin(theta1)*sin(theta2)*cos(alpha2) + cos(alpha1)*cos(alpha2)*cos(theta1)*cos(theta2))*sin(alpha3)*cos(theta3) + (-sin(alpha1)*cos(alpha2)*cos(theta1) + sin(alpha2)*sin(theta1)*sin(theta2) - sin(alpha2)*cos(alpha1)*cos(theta1)*cos(theta2))*cos(alpha3))*sin(alpha4) + (-(sin(theta1)*cos(theta2) + sin(theta2)*cos(alpha1)*cos(theta1))*sin(theta3)*cos(alpha3) + (-sin(alpha1)*sin(alpha2)*cos(theta1) - sin(theta1)*sin(theta2)*cos(alpha2) + cos(alpha1)*cos(alpha2)*cos(theta1)*cos(theta2))*cos(alpha3)*cos(theta3) + (-sin(alpha1)*cos(alpha2)*cos(theta1) + sin(alpha2)*sin(theta1)*sin(theta2) - sin(alpha2)*cos(alpha1)*cos(theta1)*cos(theta2))*sin(alpha3))*cos(alpha4)*cos(theta4), ((sin(theta1)*cos(theta2) + sin(theta2)*cos(alpha1)*cos(theta1))*cos(theta3) + (-sin(alpha1)*sin(alpha2)*cos(theta1) - sin(theta1)*sin(theta2)*cos(alpha2) + cos(alpha1)*cos(alpha2)*cos(theta1)*cos(theta2))*sin(theta3))*sin(alpha4)*sin(theta4) + ((sin(theta1)*cos(theta2) + sin(theta2)*cos(alpha1)*cos(theta1))*sin(alpha3)*sin(theta3) - (-sin(alpha1)*sin(alpha2)*cos(theta1) - sin(theta1)*sin(theta2)*cos(alpha2) + cos(alpha1)*cos(alpha2)*cos(theta1)*cos(theta2))*sin(alpha3)*cos(theta3) + (-sin(alpha1)*cos(alpha2)*cos(theta1) + sin(alpha2)*sin(theta1)*sin(theta2) - sin(alpha2)*cos(alpha1)*cos(theta1)*cos(theta2))*cos(alpha3))*cos(alpha4) - (-(sin(theta1)*cos(theta2) + sin(theta2)*cos(alpha1)*cos(theta1))*sin(theta3)*cos(alpha3) + (-sin(alpha1)*sin(alpha2)*cos(theta1) - sin(theta1)*sin(theta2)*cos(alpha2) + cos(alpha1)*cos(alpha2)*cos(theta1)*cos(theta2))*cos(alpha3)*cos(theta3) + (-sin(alpha1)*cos(alpha2)*cos(theta1) + sin(alpha2)*sin(theta1)*sin(theta2) - sin(alpha2)*cos(alpha1)*cos(theta1)*cos(theta2))*sin(alpha3))*sin(alpha4)*cos(theta4), a1*sin(alpha1) + a2*sin(alpha2)*cos(alpha1)*cos(theta1) + a2*sin(theta1)*cos(theta2) + a3*(sin(theta1)*cos(theta2) + sin(theta2)*cos(alpha1)*cos(theta1))*cos(theta3) + a3*(-sin(alpha1)*sin(alpha2)*cos(theta1) - sin(theta1)*sin(theta2)*cos(alpha2) + cos(alpha1)*cos(alpha2)*cos(theta1)*cos(theta2))*sin(alpha3) + a4*((sin(theta1)*cos(theta2) + sin(theta2)*cos(alpha1)*cos(theta1))*cos(theta3) + (-sin(alpha1)*sin(alpha2)*cos(theta1) - sin(theta1)*sin(theta2)*cos(alpha2) + cos(alpha1)*cos(alpha2)*cos(theta1)*cos(theta2))*sin(theta3))*cos(theta4) + a4*(-(sin(theta1)*cos(theta2) + sin(theta2)*cos(alpha1)*cos(theta1))*sin(theta3)*cos(alpha3) + (-sin(alpha1)*sin(alpha2)*cos(theta1) - sin(theta1)*sin(theta2)*cos(alpha2) + cos(alpha1)*cos(alpha2)*cos(theta1)*cos(theta2))*cos(alpha3)*cos(theta3) + (-sin(alpha1)*cos(alpha2)*cos(theta1) + sin(alpha2)*sin(theta1)*sin(theta2) - sin(alpha2)*cos(alpha1)*cos(theta1)*cos(theta2))*sin(alpha3))*sin(alpha4) - d2*sin(alpha1)*cos(theta1) + d3*(-sin(alpha1)*cos(alpha2)*cos(theta1) + sin(alpha2)*sin(theta1)*sin(theta2) - sin(alpha2)*cos(alpha1)*cos(theta1)*cos(theta2)) + d4*((sin(theta1)*cos(theta2) + sin(theta2)*cos(alpha1)*cos(theta1))*sin(alpha3)*sin(theta3) - (-sin(alpha1)*sin(alpha2)*cos(theta1) - sin(theta1)*sin(theta2)*cos(alpha2) + cos(alpha1)*cos(alpha2)*cos(theta1)*cos(theta2))*sin(alpha3)*cos(theta3) + (-sin(alpha1)*cos(alpha2)*cos(theta1) + sin(alpha2)*sin(theta1)*sin(theta2) - sin(alpha2)*cos(alpha1)*cos(theta1)*cos(theta2))*cos(alpha3))], [((sin(alpha1)*cos(alpha2)*cos(theta2) + sin(alpha2)*cos(alpha1))*sin(theta3) + sin(alpha1)*sin(theta2)*cos(theta3))*cos(theta4) + ((-sin(alpha1)*sin(alpha2)*cos(theta2) + cos(alpha1)*cos(alpha2))*sin(alpha3) + (sin(alpha1)*cos(alpha2)*cos(theta2) + sin(alpha2)*cos(alpha1))*cos(alpha3)*cos(theta3) - sin(alpha1)*sin(theta2)*sin(theta3)*cos(alpha3))*sin(theta4), -((sin(alpha1)*cos(alpha2)*cos(theta2) + sin(alpha2)*cos(alpha1))*sin(theta3) + sin(alpha1)*sin(theta2)*cos(theta3))*sin(theta4)*cos(alpha4) + ((-sin(alpha1)*sin(alpha2)*cos(theta2) + cos(alpha1)*cos(alpha2))*sin(alpha3) + (sin(alpha1)*cos(alpha2)*cos(theta2) + sin(alpha2)*cos(alpha1))*cos(alpha3)*cos(theta3) - sin(alpha1)*sin(theta2)*sin(theta3)*cos(alpha3))*cos(alpha4)*cos(theta4) + ((-sin(alpha1)*sin(alpha2)*cos(theta2) + cos(alpha1)*cos(alpha2))*cos(alpha3) - (sin(alpha1)*cos(alpha2)*cos(theta2) + sin(alpha2)*cos(alpha1))*sin(alpha3)*cos(theta3) + sin(alpha1)*sin(alpha3)*sin(theta2)*sin(theta3))*sin(alpha4), ((sin(alpha1)*cos(alpha2)*cos(theta2) + sin(alpha2)*cos(alpha1))*sin(theta3) + sin(alpha1)*sin(theta2)*cos(theta3))*sin(alpha4)*sin(theta4) - ((-sin(alpha1)*sin(alpha2)*cos(theta2) + cos(alpha1)*cos(alpha2))*sin(alpha3) + (sin(alpha1)*cos(alpha2)*cos(theta2) + sin(alpha2)*cos(alpha1))*cos(alpha3)*cos(theta3) - sin(alpha1)*sin(theta2)*sin(theta3)*cos(alpha3))*sin(alpha4)*cos(theta4) + ((-sin(alpha1)*sin(alpha2)*cos(theta2) + cos(alpha1)*cos(alpha2))*cos(alpha3) - (sin(alpha1)*cos(alpha2)*cos(theta2) + sin(alpha2)*cos(alpha1))*sin(alpha3)*cos(theta3) + sin(alpha1)*sin(alpha3)*sin(theta2)*sin(theta3))*cos(alpha4), a2*sin(alpha1)*sin(alpha2) + a3*(sin(alpha1)*cos(alpha2)*cos(theta2) + sin(alpha2)*cos(alpha1))*sin(alpha3) + a3*sin(alpha1)*sin(theta2)*cos(theta3) + a4*((sin(alpha1)*cos(alpha2)*cos(theta2) + sin(alpha2)*cos(alpha1))*sin(theta3) + sin(alpha1)*sin(theta2)*cos(theta3))*cos(theta4) + a4*((-sin(alpha1)*sin(alpha2)*cos(theta2) + cos(alpha1)*cos(alpha2))*sin(alpha3) + (sin(alpha1)*cos(alpha2)*cos(theta2) + sin(alpha2)*cos(alpha1))*cos(alpha3)*cos(theta3) - sin(alpha1)*sin(theta2)*sin(theta3)*cos(alpha3))*sin(alpha4) + d1 + d2*cos(alpha1) + d3*(-sin(alpha1)*sin(alpha2)*cos(theta2) + cos(alpha1)*cos(alpha2)) + d4*((-sin(alpha1)*sin(alpha2)*cos(theta2) + cos(alpha1)*cos(alpha2))*cos(alpha3) - (sin(alpha1)*cos(alpha2)*cos(theta2) + sin(alpha2)*cos(alpha1))*sin(alpha3)*cos(theta3) + sin(alpha1)*sin(alpha3)*sin(theta2)*sin(theta3))], [0, 0, 0, 1]])

```python
ra02.T_list
```

    [Matrix([
     [cos(theta1), -sin(theta1)*cos(alpha1),  sin(alpha1)*sin(theta1), a1*cos(theta1)],
     [sin(theta1),  cos(alpha1)*cos(theta1), -sin(alpha1)*cos(theta1), a1*sin(alpha1)],
     [          0,              sin(alpha1),              cos(alpha1),             d1],
     [          0,                        0,                        0,              1]]),
     Matrix([
     [cos(theta2), -sin(theta2)*cos(alpha2),  sin(alpha2)*sin(theta2), a2*cos(theta2)],
     [sin(theta2),  cos(alpha2)*cos(theta2), -sin(alpha2)*cos(theta2), a2*sin(alpha2)],
     [          0,              sin(alpha2),              cos(alpha2),             d2],
     [          0,                        0,                        0,              1]]),
     Matrix([
     [cos(theta3), -sin(theta3)*cos(alpha3),  sin(alpha3)*sin(theta3), a3*cos(theta3)],
     [sin(theta3),  cos(alpha3)*cos(theta3), -sin(alpha3)*cos(theta3), a3*sin(alpha3)],
     [          0,              sin(alpha3),              cos(alpha3),             d3],
     [          0,                        0,                        0,              1]]),
     Matrix([
     [cos(theta4), -sin(theta4)*cos(alpha4),  sin(alpha4)*sin(theta4), a4*cos(theta4)],
     [sin(theta4),  cos(alpha4)*cos(theta4), -sin(alpha4)*cos(theta4), a4*sin(alpha4)],
     [          0,              sin(alpha4),              cos(alpha4),             d4],
     [          0,                        0,                        0,              1]])]

```python
T1 = ra02.calc_dh_matrix()
print(T1)
```

    100%|██████████| 4/4 [00:00<00:00, 1772.37it/s]
    100%|██████████| 4/4 [00:00<00:00, 13.75it/s]

    Matrix([[(-sin(theta2)*sin(theta3)*cos(theta1) + cos(theta1)*cos(theta2)*cos(theta3))*cos(theta4) + (-sin(theta2)*cos(theta1)*cos(theta3) - sin(theta3)*cos(theta1)*cos(theta2))*sin(theta4), -(-sin(theta2)*sin(theta3)*cos(theta1) + cos(theta1)*cos(theta2)*cos(theta3))*sin(theta4) + (-sin(theta2)*cos(theta1)*cos(theta3) - sin(theta3)*cos(theta1)*cos(theta2))*cos(theta4), sin(theta1), 0.09*(-sin(theta2)*sin(theta3)*cos(theta1) + cos(theta1)*cos(theta2)*cos(theta3))*cos(theta4) + 0.09*cos(theta1)*cos(theta2)*cos(theta3) + 0.12*cos(theta1)*cos(theta2)], [(-sin(theta1)*sin(theta2)*sin(theta3) + sin(theta1)*cos(theta2)*cos(theta3))*cos(theta4) + (-sin(theta1)*sin(theta2)*cos(theta3) - sin(theta1)*sin(theta3)*cos(theta2))*sin(theta4), -(-sin(theta1)*sin(theta2)*sin(theta3) + sin(theta1)*cos(theta2)*cos(theta3))*sin(theta4) + (-sin(theta1)*sin(theta2)*cos(theta3) - sin(theta1)*sin(theta3)*cos(theta2))*cos(theta4), -cos(theta1), 0.09*(-sin(theta1)*sin(theta2)*sin(theta3) + sin(theta1)*cos(theta2)*cos(theta3))*cos(theta4) + 0.09*sin(theta1)*cos(theta2)*cos(theta3) + 0.12*sin(theta1)*cos(theta2)], [(-sin(theta2)*sin(theta3) + cos(theta2)*cos(theta3))*sin(theta4) + (sin(theta2)*cos(theta3) + sin(theta3)*cos(theta2))*cos(theta4), (-sin(theta2)*sin(theta3) + cos(theta2)*cos(theta3))*cos(theta4) - (sin(theta2)*cos(theta3) + sin(theta3)*cos(theta2))*sin(theta4), 0, 0.09*(sin(theta2)*cos(theta3) + sin(theta3)*cos(theta2))*cos(theta4) + 0.09*sin(theta2)*cos(theta3) + 0.115], [0, 0, 0, 1]])

```python
T2 = sympy.simplify(T1)
print(T2)
```

    Matrix([[cos(theta1)*cos(theta2 + theta3 + theta4), -sin(theta2 + theta3 + theta4)*cos(theta1), sin(theta1), (0.09*cos(theta2)*cos(theta3) + 0.12*cos(theta2) + 0.09*cos(theta4)*cos(theta2 + theta3))*cos(theta1)], [sin(theta1)*cos(theta2 + theta3 + theta4), -sin(theta1)*sin(theta2 + theta3 + theta4), -cos(theta1), (0.09*cos(theta2)*cos(theta3) + 0.12*cos(theta2) + 0.09*cos(theta4)*cos(theta2 + theta3))*sin(theta1)], [sin(theta2 + theta3 + theta4), cos(theta2 + theta3 + theta4), 0, 0.09*sin(theta2)*cos(theta3) + 0.09*sin(theta2 + theta3)*cos(theta4) + 0.115], [0, 0, 0, 1]])

```python
from IPython.display import Latex
a = sympy.latex(T2)
Latex(a)
```

\left[\begin{matrix}\cos{\left (\theta_{1} \right )} \cos{\left (\theta_{2} + \theta_{3} + \theta_{4} \right )} & - \sin{\left (\theta_{2} + \theta_{3} + \theta_{4} \right )} \cos{\left (\theta_{1} \right )} & \sin{\left (\theta_{1} \right )} & \left(0.09 \cos{\left (\theta_{2} \right )} \cos{\left (\theta_{3} \right )} + 0.12 \cos{\left (\theta_{2} \right )} + 0.09 \cos{\left (\theta_{4} \right )} \cos{\left (\theta_{2} + \theta_{3} \right )}\right) \cos{\left (\theta_{1} \right )}\\\sin{\left (\theta_{1} \right )} \cos{\left (\theta_{2} + \theta_{3} + \theta_{4} \right )} & - \sin{\left (\theta_{1} \right )} \sin{\left (\theta_{2} + \theta_{3} + \theta_{4} \right )} & - \cos{\left (\theta_{1} \right )} & \left(0.09 \cos{\left (\theta_{2} \right )} \cos{\left (\theta_{3} \right )} + 0.12 \cos{\left (\theta_{2} \right )} + 0.09 \cos{\left (\theta_{4} \right )} \cos{\left (\theta_{2} + \theta_{3} \right )}\right) \sin{\left (\theta_{1} \right )}\\\sin{\left (\theta_{2} + \theta_{3} + \theta_{4} \right )} & \cos{\left (\theta_{2} + \theta_{3} + \theta_{4} \right )} & 0 & 0.09 \sin{\left (\theta_{2} \right )} \cos{\left (\theta_{3} \right )} + 0.09 \sin{\left (\theta_{2} + \theta_{3} \right )} \cos{\left (\theta_{4} \right )} + 0.115\\0 & 0 & 0 & 1\end{matrix}\right]

```python
#makes it easier to copy and paste by each element alone of T2
for i in range(4):
    for j in range(4):
        print("( {} , {} )".format(i,j))
        print(T2[i,j])
```

    ( 0 , 0 )
    cos(theta1)*cos(theta2 + theta3 + theta4)
    ( 0 , 1 )
    -sin(theta2 + theta3 + theta4)*cos(theta1)
    ( 0 , 2 )
    sin(theta1)
    ( 0 , 3 )
    (0.09*cos(theta2)*cos(theta3) + 0.12*cos(theta2) + 0.09*cos(theta4)*cos(theta2 + theta3))*cos(theta1)
    ( 1 , 0 )
    sin(theta1)*cos(theta2 + theta3 + theta4)
    ( 1 , 1 )
    -sin(theta1)*sin(theta2 + theta3 + theta4)
    ( 1 , 2 )
    -cos(theta1)
    ( 1 , 3 )
    (0.09*cos(theta2)*cos(theta3) + 0.12*cos(theta2) + 0.09*cos(theta4)*cos(theta2 + theta3))*sin(theta1)
    ( 2 , 0 )
    sin(theta2 + theta3 + theta4)
    ( 2 , 1 )
    cos(theta2 + theta3 + theta4)
    ( 2 , 2 )
    0
    ( 2 , 3 )
    0.09*sin(theta2)*cos(theta3) + 0.09*sin(theta2 + theta3)*cos(theta4) + 0.115
    ( 3 , 0 )
    0
    ( 3 , 1 )
    0
    ( 3 , 2 )
    0
    ( 3 , 3 )
    1

```python

```


```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('classic')
%matplotlib inline
```

# Class 12: Introduction to the `linearsolve` Module

In general, dynamic stochastic general equilibrium (DSGE) models do not admit analytic (i.e., pencil-and-paper) solutions and they are time-consuming to work with. The `linearsolve` module approximates, solves, and simulates DSGE models and therefore makes DSGE models easier to use.

## Installing `linearsolve`

`linearsolve` is not included in the Anaconda Python installation and so before you can import it, you need to download and install the `linearsolve` package from PyPI, the Python Package Index. In Windows, open the Anaconda Prompt and in Mac, open the Terminal and run the following commmand:

    pip install linearsolve

You only have to install the package once.

```python
# Import the linearsolve under the 'ls' namespace
import linearsolve as ls
```

## Example: A One-Equation Model of TFP

Consider the following AR(1) specification for $\log$ TFP:

\begin{align}
\log A_{t+1} & = \rho \log A_t + \epsilon_{t+1}
\end{align}

where $\epsilon_{t+1} \sim \mathcal{N}(0,\sigma^2)$. Let's simulate the model with `linearsolve`. To do this we need to do several things:

1. Create a Pandas series that stores the names of the parameters of the model.
2. Define a function that returns the equilibrium conditions of the model solved for zero.
3. Initialize an instance of the `linearsolve.model` class
4. Compute and input the steady state of the model.
5. Approximate and solve the model.
6. Compute simulations of the model.

**Step 1:** Create a variable called `parameters` that stores parameter values as a Pandas Series.

```python
# Create a variable called 'parameters' that stores the model parameter values in a Pandas Series
parameters = pd.Series()
parameters['rho'] = .9
parameters['sigma_squared'] = 1

# Print stored parameter values
print(parameters)
```

    rho              0.9
    sigma_squared    1.0
    dtype: float64

```python
# Create variable called 'varNames' that stores the variable names in a list with state variables ordered first
varNames = ['a']

# Create variable called 'shockNames' that stores an exogenous shock name for each state variable.
shockNames = ['e_a']
```

**Step 2:** Define a function called `equilibrium_equations` that evaluates the equilibrium
equations of model solved for zero. The function should accept three arguments:

1. `variables_forward`: Values of $t+1$-dated variables
2. `variables_current`: Values of $t-1$-dated variables
3. `parameters`: Pandas Series with the parameters for the model

The function should return a NumPy array of the model's equilibrium conditions solved for zero.

```python
# Define a function that evaluates the equilibrium conditions of the model solved for zero. PROVIDED
def equilibrium_equations(variables_forward,variables_current,parameters):

    # Create a variable called 'p' that storres the model's parameters. PROVIDED.
    p = parameters

    # Create variable called 'cur' that stores the values of current (date t) variables. PROVIDED.
    cur = variables_current

    # Create variable called 'fwd' that stores the values of one-period-ahead (date t+1) variables. PROVIDED.
    fwd = variables_forward

    # Create variable called 'tfp_proc' that returns the law of motion for TFP solved for zero (exogenous shock excluded)
    tfp_proc = p.rho*np.log(cur.a) - np.log(fwd.a)

    # Return equilibrium conditions stacked in a numpy array. PROVIDED.
    return np.array([
        tfp_proc
        ])
```

**Step 3:** Initialize a model instance using the `ls.model()` function. The

1. `equations`: Name of function that stores the equilibrium conditions
2. `nstates`: Number of *state* variables (i.e., variables that are *predetermined*)
3. `varNames`: List of the names of the endogenous variables
4. `shockNames`: List of the names of the exogenous shocks
5. `parameters`: Pandas Series of parameter values

```python
# Initialize the model into a variable named 'rbc_model' using the ls.model() function
model = ls.model(equations = equilibrium_equations,
                 nstates=1,
                 varNames=varNames,
                 shockNames=shockNames,
                 parameters = parameters)
```

**Step 4:** Set the steady state of the model. Either use the `.compute_ss()` method which requires an initial guess of what the steady state is. Or set the steady state `.ss` attribute directly.

```python
# Compute the steady state numerically using .compute_ss() method of model
model.compute_ss([1])
print(model.ss)
```

    a    1.0
    dtype: float64

```python
# Set the .ss attribute of model directly
model.ss = pd.Series({'a':1})
```

**Step 5:** Compute a transform the model into a log-linear approximation around the nonstochastic steady state. Then rewrite the equilibrium conditions so that all endogenous variables are expressed as linear functions of state variables and exogenous shocks.

```python
# Appproximate and solve using the .approximate_and_solve() method of model
model.approximate_and_solve()
```

**Step 6:** Simulate the model using one of the following methods:

1. `.impulse()`: Compute impulse responses to a one-time shock to each exogenous variables. Results stored in `.irs` attribute.
2. `.stoch_sim()`: Compute stochastic simulation. Results stored in `.simulated` attribute.

First, we'll compute an impulse response simulation. Let's consider the effect of a one time shock to $\epsilon$ of 0.01 in period 5. Simulate 41 periods.

```python
# Compute impulse response of a to a one-time shock to e_a
model.impulse(T=41,t0=5,shocks=[0.01])
```

The impulse response simulations are stored in the `.irs` attribute as a dictionary with keys equal to the names of the exogenous shocks.

```python
# Print model.irs
print(model.irs)
```

    {'e_a':      e_a         a
    0   0.00  0.000000
    1   0.00  0.000000
    2   0.00  0.000000
    3   0.00  0.000000
    4   0.00  0.000000
    5   0.01  0.010000
    6   0.00  0.009000
    7   0.00  0.008100
    8   0.00  0.007290
    9   0.00  0.006561
    10  0.00  0.005905
    11  0.00  0.005314
    12  0.00  0.004783
    13  0.00  0.004305
    14  0.00  0.003874
    15  0.00  0.003487
    16  0.00  0.003138
    17  0.00  0.002824
    18  0.00  0.002542
    19  0.00  0.002288
    20  0.00  0.002059
    21  0.00  0.001853
    22  0.00  0.001668
    23  0.00  0.001501
    24  0.00  0.001351
    25  0.00  0.001216
    26  0.00  0.001094
    27  0.00  0.000985
    28  0.00  0.000886
    29  0.00  0.000798
    30  0.00  0.000718
    31  0.00  0.000646
    32  0.00  0.000581
    33  0.00  0.000523
    34  0.00  0.000471
    35  0.00  0.000424
    36  0.00  0.000382
    37  0.00  0.000343
    38  0.00  0.000309
    39  0.00  0.000278
    40  0.00  0.000250}

Let's look at the first 10 rows of the `'e_a'` element of `model.irs`.

```python
# Print first 10 rows of the element in model.irs that corresponds to the shock to TFP
print(model.irs['e_a'].head())
```

       e_a    a
    0  0.0  0.0
    1  0.0  0.0
    2  0.0  0.0
    3  0.0  0.0
    4  0.0  0.0

```python
# Plot simulated impulse response to e_a
model.irs['e_a'][['e_a','a']].plot(lw=3,alpha=0.5,grid=True).legend(loc='upper right',ncol=2)
```

Next, we'll use the `.stoch_sim()` method to compute a stochastic simulation. The method takes arguments:

1. `seed`: Seed of NumPy RNG. (Optional)
2. `T`: Number of periods to simulate
3. `covMat`: Covariance matrix for exogenous shock process

The simulation should be for 201 periods.

```python
# Compute stochastic simulation
model.stoch_sim(seed=126,T=201,covMat= [[parameters['sigma_squared']]])

# Print the first 10 rows of `model.simulated`
print(model.simulated.head(10))
```

            e_a         a
    0 -0.600644  0.342803
    1  0.768343  1.076866
    2 -0.650052  0.319127
    3 -0.044046  0.243168
    4 -0.081916  0.136935
    5  0.307046  0.430287
    6  0.628282  1.015541
    7  0.145086  1.059073
    8  0.298050  1.251216
    9 -0.851712  0.274382

The stochastic simulations are stored in the `.stoch_sim` attribute as a Pandas `DataFrame`.

```python
# Plot the stochastic simulation
model.simulated[['e_a','a']].plot(lw=3,alpha=0.5,grid=True).legend(loc='upper right',ncol=2)
```

## Example 2: The Stochastic Solow Growth Model Revisited

Now consider the following system of equations:

\begin{align}
Y_t & = A_t K_t^{\alpha} \\
K_{t+1} & = sY_t + (1-\delta) K_t\\
\log A_{t+1} & = \rho \log A_t + \epsilon_{t+1}
\end{align}

where $\epsilon_{t+1} \sim \mathcal{N}(0,\sigma^2)$. Let's simulate the model with `linearsolve`. Before proceding, let's also go ahead and rewrite the model with all variables moved to the lefthand side of the equations:

\begin{align}
0 & = A_t K_t^{\alpha} - Y_t \\
0 & = sY_t + (1-\delta) K_t - K_{t+1}\\
0 & = \rho \log A_t + \epsilon_{t+1} - \log A_{t+1}
\end{align}

Capital and TFP are called *state variables* because they're $t+1$ values are predetermined. Output is called a *costate* or *control* variable. Note that the model as 3 endogenous variables with 2 state variables.

Use the following values for the simulation:

| $\rho$ | $\sigma$ | $s$ | $\alpha$ | $\delta $ | $T$  |
|--------|----------|-----|----------|-----------|------|
| 0.75   | 0.006    | 0.1 | 0.35     |  0.025    | 201  |

### Initialization, Approximation, and Solution

The next several cells initialize the model in `linearsolve` and then approximate and solve it.

```python
# Create a variable called 'parameters' that stores the model parameter values in a Pandas Series
parameters = pd.Series()
parameters['rho'] = .75
parameters['sigma_squared'] = 0.006**2
parameters['alpha'] = 0.35
parameters['delta'] = 0.025
parameters['s'] = 0.1

# Print the model's parameters
print(parameters)
```

    rho              0.750000
    sigma_squared    0.000036
    alpha            0.350000
    delta            0.025000
    s                0.100000
    dtype: float64

```python
# Create variable called 'varNames' that stores the variable names in a list with state variables ordered first
varNames = ['a','k','y']

# Create variable called 'shockNames' that stores an exogenous shock name for each state variable.
shockNames = ['e_a','e_k']
```

```python
# Define a function that evaluates the equilibrium conditions of the model solved for zero. PROVIDED
def equilibrium_equations(variables_forward,variables_current,parameters):

    # Parameters. PROVIDED
    p = parameters

    # Current variables. PROVIDED
    cur = variables_current

    # Forward variables. PROVIDED
    fwd = variables_forward

    # Production function
    production_function = cur.a*cur.k**p.alpha - cur.y

    # Capital evolution
    capital_evolution = p.s*cur.a*cur.k**p.alpha + (1 - p.delta)*cur.k - fwd.k

    # Exogenous tfp
    tfp_process = p.rho*np.log(cur.a) - np.log(fwd.a)

    # Stack equilibrium conditions into a numpy array
    return np.array([
        production_function,
        capital_evolution,
        tfp_process
        ])
```

Next, initialize the model.

```python
# Initialize the model into a variable named 'rbc_model'
model = ls.model(equations = equilibrium_equations,
                 nstates=2,
                 varNames=varNames,
                 shockNames=shockNames,
                 parameters = parameters)
```

```python
# Compute the steady state numerically using .compute_ss() method of model
guess = [1,4,1]
model.compute_ss(guess)

# Print the computed steady state
print(model.ss)
```

    a    1.000000
    k    8.438129
    y    2.109532
    dtype: float64

```python
# Find the log-linear approximation around the non-stochastic steady state and solve using .approximate_and_solve() method of model
model.approximate_and_solve()
```

### A Few Details About the Approximation (Optional)

The previous step constructs a log-linear approximation of the model and then solves for the endogenous variables as functions of the state variables and exogenous shocks only.

View the approximated model by calling the `.approximated()` method.

```python
# Print the log-linear approximation to the models's equilibrium conditions
print(model.approximated())
```

    Log-linear equilibrium conditions:

                 0 = -2.1095·a[t]-0.7383·k[t]+2.1095·y[t]

    -8.4381·k[t+1] = -0.211·a[t]-8.301·k[t]

           -a[t+1] = -0.75·a[t]

Each variable represents the log-deviation from the steady state of the respective variable in our model. For example, the variable `y[t]` means $\log(Y_t) - \log(Y)$ in terms of the stochastic Solow model. But how do these equations relate to the original model?

The first equation appears to be:

\begin{align}
0 &= -2.1095\cdot a_t-0.7383\cdot k_t + 2.1095\cdot y_t
\end{align}

Note that dividing by 2.1095 and solving for $y_t$ yields:

\begin{align}
y_t &= a_t +0.3499\cdot k_t,
\end{align}

so the coefficient on $k_t$ appears to be close to $\alpha=0.35$. We can derive this linear equation directly. First, start with the production function:

\begin{align}
Y_t &= A_t K_t^{\alpha}.
\end{align}

Then divide both sides by steady state output:

\begin{align}
\frac{Y_t}{Y} &= \frac{A_t K_t^{\alpha}}{AK^{\alpha}} \, = \, \frac{A_t}{A}\frac{K_t^{\alpha}}{K^{\alpha}}.
\end{align}

Then, take the log of both sides:
\begin{align}
\log\left(\frac{Y_t}{Y} \right)&= \log\left(\frac{A_t}{A}\right) + \alpha\log\left(\frac{K_t}{K}\right).
\end{align}

finally, letting $y_t = \log(Y_t/Y)$, k_t = $\log(K_t/K)$, and $a_t = \log(A_t/A)$, we have:

\begin{align}
y_t &= a_t + \alpha k_t.
\end{align}

However, understanding this process isn't as important as being able to interpret the graphs and statistics that compute using the output of `linearsolve`.

### A Few Details About the Solution (Optional)

It's also worth seeing what it means for a model to be *solved*. After `linearsolve` computes the log-linear approximation to the model, it solves for each endogenous variable as a function of state variables only. View the solved model by calling the `.solved()` method.

```python
# Print the solved model
print(model.solved())
```

    Solution to the log-linear system:

    a[t+1] = 0.75·a[t]+e_a[t+1]

    k[t+1] = 0.025·a[t]+0.9837·k[t]+e_k[t+1]

      y[t] = a[t]+0.35·k[t]

### Impulse Responses

Compute a 41 period impulse responses of the model's variables to a 0.01 unit shock to TFP in period 5.

```python
# Compute impulse responses
model.impulse(T=41,t0=5,shocks=[0.01,0])

# Print the first 10 rows of the computed impulse responses.
print(model.irs['e_a'].head(10))
```

        e_a         a         k         y
    0  0.00  0.000000  0.000000  0.000000
    1  0.00  0.000000  0.000000  0.000000
    2  0.00  0.000000  0.000000  0.000000
    3  0.00  0.000000  0.000000  0.000000
    4  0.00  0.000000  0.000000  0.000000
    5  0.01  0.010000  0.000000  0.010000
    6  0.00  0.007500  0.000250  0.007588
    7  0.00  0.005625  0.000433  0.005777
    8  0.00  0.004219  0.000567  0.004417
    9  0.00  0.003164  0.000663  0.003396

```python
# Plot the computed impulse responses to a TFP shock
fig = plt.figure(figsize=(12,4))

ax1 = fig.add_subplot(1,2,1)
model.irs['e_a'][['a','y','k']].plot(lw=3,alpha=0.5,grid=True,ax = ax1).legend(loc='upper right',ncol=2)
ax1.set_title('Endogenous Variables')

ax2 = fig.add_subplot(1,2,2)
model.irs['e_a'][['e_a']].plot(lw=3,alpha=0.5,grid=True,ax = ax2).legend(loc='upper right',ncol=2)
ax2.set_title('TFP Shock Process')

fig.tight_layout()
```

### Stochastic Simulation

Compute a 201 period stochastic simulation of the model's variables. Set the variance of $\epsilon_t$ to $\sigma^2$ and the variance of the shock to capital to 0 so that the covariance matrix for the shock process is:

\begin{align}
\text{Covariance matrix} & = \left[\begin{array}{cc} \sigma^2 & 0\\ 0 & 0\end{array} \right]
\end{align}

```python
# Compute stochastic simulation and print the simulated values.
model.stoch_sim(seed=126,T=201,covMat= [[parameters['sigma_squared'],0],[0,0]])

# Print first 10 rows of model.simulated
print(model.simulated.head(10))
```

            e_a  e_k         a         k         y
    0  0.001484  0.0  0.003862  0.000649  0.004089
    1  0.001427  0.0  0.004323  0.000735  0.004580
    2  0.003155  0.0  0.006397  0.000831  0.006688
    3  0.011305  0.0  0.016103  0.000978  0.016445
    4 -0.001778  0.0  0.010299  0.001364  0.010777
    5 -0.013211  0.0 -0.005486  0.001600 -0.004927
    6  0.004054  0.0 -0.000060  0.001437  0.000442
    7  0.006153  0.0  0.006108  0.001412  0.006602
    8 -0.001336  0.0  0.003245  0.001541  0.003785
    9 -0.006801  0.0 -0.004367  0.001598 -0.003808

```python
# Plot the computed stochastic simulation
fig = plt.figure(figsize=(12,4))

ax1 = fig.add_subplot(1,2,1)
model.simulated[['a','y','k']].plot(lw=3,alpha=0.5,grid=True,ax = ax1).legend(loc='upper right',ncol=3)
ax1.set_title('Endogenous Variables')

ax2 = fig.add_subplot(1,2,2)
model.simulated[['e_a']].plot(lw=3,alpha=0.5,grid=True,ax = ax2).legend(loc='upper right',ncol=2)
ax2.set_title('TFP Shock Process')

fig.tight_layout()
```

```python
# Compute standard deviations of simulated TFP, output, and capital
model.simulated[['a','y','k']].std()
```

    a    0.009478
    y    0.009484
    k    0.001385
    dtype: float64

```python
# Compute correlation coefficients of simulated TFP, output, and capital
model.simulated[['a','y','k']].corr()
```

      a
      y
      k

      a
      1.000000
      0.998694
      -0.012365

      y
      0.998694
      1.000000
      0.038745

      k
      -0.012365
      0.038745
      1.000000


 Text provided under a Creative Commons Attribution license, CC-BY. All code is made available under the FSF-approved MIT license. (c) Kyle T. Mandli

```python
from __future__ import print_function

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

Note to lecturers:  This notebook is designed to work best as a classic Jupyter Notebook with nbextensions
* hide_input: to hide selected python cells particularly for just plotting
* RISE:  Interactive js slide presentations

# Introduction to Numerical Methods

## Outline

* Introduction and Motivation
* Some examples of Numerical Problems
* Overview of Course materials
* Course Logistics
* **Prerequisites and "Fluency"**
* Overview of the course computational environment (Python, jupyter, git)

## What are Numerical Methods?

* An extremely broad field -- much broader than this class

* Rough definition:  analysis and application of algorithms to allow computers to solve problems in math, science, engineering...

* Strictly speaking, numerical methods don't require computers (e.g. Newton's method is ~ 17 century)

* But computers make things practical...Consider this class an introduction to computational math

## Why do we need numerical methods?

### Some problems have **no closed form solution**

   Example:  find $f(x)=0$ for the 5th order polynomial

$$
    x^5 + 3x^2+ 2x + 3 = 0
$$

$$
    p(x) = x^5 + 3x^2+ 2x + 3 = 0
$$

```python
p = lambda x:  x**5 +3*x**2 +2*x +3

from scipy.optimize import brentq

x = numpy.linspace(-2,2,100)
x0 = brentq(p,-2,2)

plt.figure(figsize=(8,6))
plt.plot(x,p(x),linewidth=2)
plt.plot(x,numpy.zeros(x.shape),'k',linewidth=2)
plt.scatter(x0,p(x0),s=50,c='r')
plt.grid()
plt.xlabel('x',fontsize=16)
plt.ylabel('p(x)',fontsize=16)
plt.title('Root at x={}'.format(x0),fontsize=18)
plt.show()
```

The 5th order polynomial  $p(x) = x^5 + 3x^2+ 2x + 3 = 0$ actually has 5 complex roots

```python
c = [1,0,0,3,2,3]
roots = numpy.roots(c)
fig = plt.figure(figsize=(8,6))
plt.scatter(numpy.real(roots),numpy.imag(roots),s=50,c='r')
x=numpy.linspace(-2,2)
plt.plot(x,numpy.zeros(x.shape),'k',linewidth=2)
plt.plot(numpy.zeros(x.shape),x,'k',linewidth=2)

plt.xlabel('Re',fontsize=16)
plt.ylabel('Im',fontsize=16)
plt.grid()
plt.title('Complex roots of a 5th order polynomial')
plt.axis('square')
plt.xlim([-2,2])
plt.ylim([-2,2])
plt.show()
```

which require completely different numerical methods to find.

In general, the roots of most nonlinear systems,

$$
    \mathbf{F}(x,y,z,t) = \mathbf{0},
$$

cannot be solved directly.

### Some problems are too big to be done by hand.

It may not possible to calculate an exact answer in a convenient amount of time. It may be that a system is over constrained and obtaining an answer that matches all requirements is not possible.

### Some problems are simply not analytic

#### Data analysis of digital data

Finding trends in real data represented without a closed form (analytical form).

```python
data = numpy.loadtxt("./data/sunspot.dat")

fig = plt.figure(figsize=(10, 5))
fig.set_figwidth(fig.get_figwidth() * 2)

axes = fig.add_subplot(1, 2, 1)
axes.plot(data[:, 0], data[:, 1],linewidth=2)
axes.set_xlabel("Year",fontsize=16)
axes.set_ylabel("Number",fontsize=16)
axes.set_title("Number of Sunspots",fontsize=18)

axes = fig.add_subplot(1, 2, 2)
N = int(data.shape[0] / 2)
period = 1.0 / numpy.fft.fftfreq(data.shape[0])[1:N]
sunspot_freq = numpy.fft.fft(data[:, 1])[1:N]
freq = numpy.fft.fftfreq(data.shape[0])[1:N]
axes.plot(period, numpy.abs(sunspot_freq)**2,linewidth=2)
axes.set_xlabel("Years/Cycle",fontsize=16)
axes.set_ylabel("Power Spectrum",fontsize=16)
axes.set_title("Frequency of Sunspots",fontsize=18)
axes.set_xlim((0, 50))

plt.show()
```

### Sometimes you actually want an answer

(rather than show it exists or is unique)

##### Example:  Solution of non-linear dynamical systems

$$
    \frac{d\mathbf{u}}{dt} = \mathbf{F}(t,\mathbf{u}),\quad \mathbf{u}(0)=\mathbf{u}_0
$$

* Can prove that if $\mathbf{F}$ is sufficiently smooth (Lipshitz Continuous) over some domain that a unique solution exists for some local interval in time (Picard-Lindelhof Theorem)
* However, the theorem provides **no** way to  efficiently find a solution

### Example: A non-linear  model of Epidemics

Classical [Kermack and McKendrick (1927)](https://royalsocietypublishing.org/doi/10.1098/rspa.1927.0118) SIR model of epidemics (with reinfection)

$$
\begin{align}
    \frac{ds}{dt} &= -si + kr \\
    \frac{di}{dt} &= si -\sigma i \\
    \frac{dr}{dt} &= \sigma i - kr\\
\end{align}
$$

Where the variable $s$ represents the fraction of a population that is **Susceptible** to infection, $i$ is the proportion **Infected** and $r$ the fraction **Recovered**.  For this model $s+i+r =1$.  The parameters $\sigma, k \geq 0$ control the relative rates of infection and recovery.

$$
\begin{align}
    \dot{s} &= -si + kr \\
    \dot{i} &= si -\sigma i \\
    \dot{r} &= \sigma i - kr\\
\end{align}
$$

```python
# Solve using SciPy's ODE integrator solve_ivp
from scipy.integrate import

# define the RHS of our system of ODE's
def sir(t, u, sigma, k):
    s,i,r = u
    return numpy.array([-s*i + k*r,
                        (s - sigma)*i,
                        sigma*i - k*r ])

```

### Numerical Solutions

```python
sigma = .2
k = 0.01
t_max = 150
sir_0 = [0.999, 0.001, 0.]
sol = solve_ivp(sir, [0, t_max] , sir_0, args=(sigma, k), rtol=1.e-6, atol=1.e-9,dense_output = True)
```

```python
t = numpy.linspace(0, t_max, 300)
z = sol.sol(t)

fig = plt.figure(figsize=(20,7))
axes = fig.add_subplot(1,2,1)
axes.plot(t,z[0],'r',label='s', linewidth=2)
axes.plot(t,z[1],'b',label='i', linewidth=2)
axes.plot(t,z[2],'g',label='r',  linewidth=2)
axes.plot(t,sigma*numpy.ones(t.shape),'k--',label='$\sigma$')

axes.legend(loc='best',shadow=True, fontsize=14)
axes.set_xlabel('Time',fontsize=16)
axes.set_ylabel('Population fraction',fontsize=16)
axes.grid()
axes.set_title('SIR system: $\sigma={}$, $k={}$'.format(sigma,k),fontsize=18)
plt.show()
```

### Questions an epidemiologist or a mathematician might ask:
 - Does this solution admit steady states?
 - Under what conditions is this problem stable, unstable, oscillatory?
 - How do we solve the initial value problem?
 - How do we understand the non-linear dynamics?
 - What impacts do small changes to the different parameters have?
 - How do we estimate parameters ($\sigma$, $k$) from data?
 - How do we evaluate whether this is a *good* model?

### Big Points

* Numerics **complement**  analytic methods, but don't replace them

* You need both to  **understand**  your problems

* Numerical methods are only a part of the bigger problem of **Mathematical Modeling**

## Mathematical Modeling

* Murphy was a modeler...

* Strictly speaking this is not a course in Mathematical Modeling

We usually have experience and insights into a system and have a good idea what the solutions should be. At the same time the numerics can help us build our intuition and complement analytical methods. The numerics can also be used to test ideas and extend our understanding of a system. In doing so we can update mathematical descriptions to account for new aspects.

The process of describing physical systems using mathematical expressions is often referred to as mathematical modeling. It is a process in which intuition, simulation, and analysis are used to build expressions that mimic general behaviours of physical systems. The general practice is to start with a basic description and refine it over many iterations.

Each step in the process introduces error. In this course we will focus on the errors associated with numerical approximation. It is important, though, to be able to determine the difference between errors associated with simplifications associated with the model and the errors associated with the approximation of mathematical expressions.

## Why is this exciting?

Computers have revolutionized our ability to explore...

[Top 10 Algorithms of the 20th Century](http://www.siam.org/pdf/news/637.pdf?t=1&cn=ZmxleGlibGVfcmVjcw%3D%3D&refsrc=email&iid=658bdab6af614c83a8df1f8e02035eae&uid=755271476&nid=244+285282312)

...and there is more to come!

## The issues:
### Accuracy and Efficiency

- Numerical methods, invariably include an enormous range of  approximations , each with attendant  errors

- Good numerical methods also return error estimates, and are stable in the presence of floating point error

- The detailed analysis of algorithms and their errors is formally  Numerical Analysis

##  **This is not a class in numerical analysis**

This is principally a Methods class where I will emphasize

- Standard Methods and their errors
- Give insight into how they work (and when they don't)
- Give you practice implementing them to solve **your** problems
- Point you to high quality numerical software that you should be using in your own work
-  Help you make good choices

## Course Content

Topics Covered: (see Syllabus on [Courseworks](https://courseworks2.columbia.edu/courses/107701/assignments/syllabus))

- Quick *Review* of computational Tools (Python,numpy,matplotlib,scipy,git)
- Sources of Error and Error Analysis
- Rootfinding/Optimization of non-linear functions $f(x)$
- Interpolation
- Numerical integration (quadrature) and differentiation
- Solution of ODE Initial value problems
- Solving  systems  of non-linear equations $\mathbf{F}(\mathbf{x})=\mathbf{0}$
- ODE 2-point Boundary value problems (towards numerical PDE's)
- Numerical Linear Algebra

### Topics Not Covered:

- Optimization -- linear programming, constrained optimization
- Numerical Solution of PDE's (APMA E4301)
- Mathematical Modeling (maybe a little)
- Machine Learning/Data Science (APMA 4990 Intro Math of Data Science)
- Lot's of things that would be fun...

## Purpose of this course:

Choose and understand critical methods that are essential for Numerical PDE's, modeling and scientific computation.

    HPC model of Windturbine turbulence by Andrew Kirby
    FEniCS FEM model of a turbocharger

### A FEM model from Spiegelman

Coupled fluid/solid dynamics model of magmatism in the Earth's mantle.

```python
from IPython.lib.display import YouTubeVideo
vid = YouTubeVideo('Pfp4snvqhus',rel=0,width=700,height=450, loop=1)
display(vid)
```

### All of these problems require Interpolation, numerical quadrature,numerical linear algebra ,

#### Plus PDE's, Vector Calculus, functional analysis,computational geometry, C++/Python programming

## Course Logistics

- 2 Lectures per week (recorded for CVN, makeups)
- Office Hours
- 8 Problem sets (75\%)
- Final Project (25\%)

- No primary text but
- All notes available as Jupyter notebooks through github

## Course Pre-requisites

### Numerical methods aren't particularly hard...

* however...They require significant  **fluency**  in
    * Calculus
    * Vector Calculus
    * ODE's
    * Linear Algebra
    * Programming: Python 3 (numpy, matplotlib, scipy)

#### Please fill out the [User Survey](https://forms.gle/RFeYHhNnCskbSxVA8) if you haven't already

#### The Objective: Computational Fluency

- the ability to:
    - move smoothly between continous and discrete problems
    - recognize and use a range of mathematical/computational techniques
    - use best practices for software development
    - keep track of errors and artifacts
    - stay on top of complex, multi-part problems
    - **Debug**

- All are required to formulate, solve, and **understand** problems of your own choosing

- Anything that can go wrong...will go wrong

## Computational Tools/Environment

### Computer Languages

* Compiled languages: C, C++, Fortran
* Commercial interactive modeling systems: Matlab, Mathematical, IDL
* Open-source interpreted languages: Python, R, Ruby, Julia

#### C, C++, Fortran

##### Pros:
 - Performance and legacy computing codes available

##### Cons:
 - Syntax not optimized for casual programming
 - No interactive facilities
 - Difficult visualization, text processing, etc.

#### IDL, Matlab, Mathematica, etc.

##### Pros:
 - Interactive with easy visualization tools
 - Extensive scientific and engineering libraries available

##### Cons:
 - Costly and proprietary
 - Unpleasant for large-scale computing and non-mathematical tasks

### Python (3.x)

##### Features and Project Goals:
 - Python is free (BSD-like license) and highly portable (Windows, Mac OS X, Linux, etc.)
 - Interactive interpreter
 - Readability
 - Simple
 - Extensive documentation
 - Memory management is (mostly) transparent
 - Clean and object-oriented
 - Built-in types

##### Pros:
- Comprehensive standard library
 - Well-established 3rd-party packages (NumPy, SciPy, matplotlib, etc.)
 - Easily wraps existing legacy code in C, C++ and Fortran
 - Python mastery is marketable
 - Scalability
   - Interactive experimentation
   - Good tools for documenting code, and the resulting code tends to be easier to maintain compared to other interpreted languages.
   - Code can be one-line scripts or million-line projects
   - Used by novices and full-time professionals alike

##### Cons:
 - Can be slow
 - Packaging system is a bit crufty
 - Discipline is forced on the programmer by the design of the language. Can limit choices available for how to implement an algorithm. (The *python* way.)
 - Too many Monty Python jokes (not really a con)

### Version Control via `git`

`git` is version control system allowing a user to take "snapshots" of data.  These snapshots can then be compared, combined or recalled as needed.

Why version control (or `git`)?
 - Nearly ubiquitous in modern software engineering and hence an essential skill
 - Can save you from yourself
 - Greatly simplifies the way multiple people can work on and share a single project.
 - Steep learning curve so expose yourself often for maximal success

[Learn more!](https://git-scm.com/)

### Jupyter Notebooks

The notebook environment gives us a convenient means for working with code while annotating it.  We will only cover the key concepts here and hope that you will explore on your own the environments.

[Documentation](https://jupyter.readthedocs.io/en/latest/)

#### Toolbar

 - Notebooks are modal, they have an edit mode (editing cells) and command mode.
 - Highly recommend taking the tour and checking out the help menu

#### Content types
   - Markdown
   - $\LaTeX$ -- $x^2 + y^2 = \sin(x)$
   - Python
   - NumPy, SciPy, and other packages
   - Other languages (R, Julia)

### Running the Notebooks

To run and interact with the notebooks you need the Jupyter software and scipy software stack

#### Interactive Environments -- all web-based

* Jupyter Lab
* Classical Jupyter Notebook
* Presentation modes (e.g. RISE)

#### Standard Scipy software stack
* Python 3.x
* SciPy
* Numpy
* Matplotlib
* Sympy
* Pandas
* etc

### Installations: A few options

#### Install on your own machine
 - [Notebook Quick Start Guide](https://jupyter-notebook-beginner-guide.readthedocs.io/en/latest/index.html)
 - [Anaconda](http://anaconda.com/downloads)

Remember to grab the  **Python 3.x**  versions!

#### The "cloud"

Instead of running things locally on your machine there are a number of cloud services that you are welcome to use in order to get everything running easily:
 - [Google's Colaboratory](https://colab.research.google.com)
 - [Microsoft Azure Notebooks](https://notebooks.azure.com)
 - [CoCalc](https://cocalc.com/)
 - [Columbia's CUIT Jupyter Hub server](https://apma4300.columbiajupyter2.org/hub/login?next=%2Fhub%2Fuser-redirect%2Fgit-pull%3Frepo%3Dhttps%253A%252F%252Fgithub.com%252Fmspieg%252Fintro-numerical-methods%26urlpath%3Dlab) (with `nbgitpuller` installed)

### Obtaining the Notebooks

All notebooks can be  found on [github](http://github.com/mspieg/intro-numerical-methods).

It is highly recommended to  obtain a github account if you do not already have one.  Will allow you start to become comfortable with `git`.

If you want to work directly with the repository,  make your own personal fork,  and keep track of any changes and submit issues to the github project page if you find mistakes (PLEASE DO THIS!).  It will be much easier to do so if you have your own account on github and work with your own clone. You can then submit a merge request to have your changes incorporated into other repositories.

### However...

`git` and `jupyter` don't always play that well together so some care needs to be taken to keep my changes and your changes in sync.

To alleviate this problem, we will use [`nbgitpuller`](https://jupyterhub.github.io/nbgitpuller/) to try and keep my notes and yours up-to-date.

Simply log in to your Jupyter Hub Account through this [link](https://apma4300.columbiajupyter2.org/hub/user-redirect/git-pull?repo=https%3A%2F%2Fgithub.com%2Fmspieg%2Fintro-numerical-methods&urlpath=lab) which will automatically update the notebooks with any of my recent changes while preserving your work.

The nbgitpuller link is also available through Courseworks.

**Warning**:  this doesn't always work, but if it breaks I'll help you fix it

###  Let's take a little time to make sure everything is working...

### Peer Review

**Why?**
In this class reviewing your peer's work can lead to
 - Better understanding of a problem
 - See alternative solutions to the same problem
 - Learn how to read other people's code
 - Hopefully learn some tips about your own coding style
 - Another skill highly used in practice
 - Demonstrate why readibility and discipline with respect to code matters.


# Random walk

Models leading to diffusion equations, see the section [Applications of diffusion equations](diffu_app.ipynb), are
usually based on reasoning with *averaged* physical quantities such as
concentration, temperature, and velocity.  The underlying physical
processes involve complicated microscopic movement of atoms and
molecules, but an average of a large number of molecules is performed
in a small volume before the modeling starts, and the averaged
quantity inside this volume is assigned as a point value at the
centroid of the volume. This means that concentration, temperature,
and velocity at a space-time point represent averages around the
point in a small time interval and small spatial volume.

Random walk is a principally different kind of modeling procedure
compared to the reasoning behind partial differential equations.  The
idea in random walk is to have a large number of "particles" that
undergo random movements. Averaging can then be used afterwards to
compute macroscopic quantities like concentration. The "particles"
and their random movement represent a very simplified microscopic
behavior of molecules, much simpler and computationally much more
efficient than direct [molecular simulation](https://en.wikipedia.org/wiki/Molecular_dynamics), yet the random
walk model has been very powerful to describe a wide range of
phenomena, including heat conduction, quantum mechanics, polymer
chains, population genetics, neuroscience, hazard games, and pricing
of financial instruments.

It can be shown that random walk, when averaged, produces models that
are mathematically equivalent to diffusion equations. This is the
primary reason why we treat random walk in this chapter: two very
different algorithms (finite difference stencils and random walk)
solve the same type of problems. The simplicity of the random walk
algorithm makes it particularly attractive for solving diffusion
equations on massively parallel computers.
The exposition here is as simple as possible, and good thorough
derivation of the models is provided by Hjorth-Jensen [[hjorten]](#hjorten).

## Random walk in 1D

Imagine that we have some particles that perform random moves, either
to the right or to the left. We may flip a coin to decide the movement
of each particle, say head implies movement to the right and tail
means movement to the left. Each move is one unit length.  Physicists
use the term *random walk* for this type of movement.
The movement is also known as [drunkard's walk](https://en.wikipedia.org/wiki/The_Drunkard%27s_Walk).
You may try this yourself: flip the coin and make one step to the left
or right, and repeat the process.

We introduce the symbol $N$ for the number of steps in a random walk.
[Figure](#diffu:randomwalk:1D:fig:ensemble) shows four different
random walks with $N=200$.

Ensemble of 4 random walks, each with 200 steps.

## Statistical considerations

Let $S_k$ be the stochastic variable representing a step to the left
or to the right in step number $k$. We have that $S_k=-1$ with
probability $p$ and $S_k=1$ with probability $q=1-p$. The variable
$S_k$ is known as a [Bernoulli variable](https://en.wikipedia.org/wiki/Bernoulli_distribution). The
expectation of $S_k$ is

$$
\hbox{E}\lbrack{S_k}\rbrack = p\cdot (-1) + q\cdot 1 = 1 - 2p,
$$

and the variance is

$$
\hbox{Var}\lbrack{S_k}\rbrack = \hbox{E}\lbrack{S_k^2}\rbrack - \hbox{E}\lbrack{S_k}\rbrack^2 = 1 - (1-2p)^2 = 4p(1-p)\thinspace .
$$

The position after $k$ steps is another stochastic variable

$$
\bar X_k = \sum_{i=0}^{k-1} S_i\thinspace .
$$

The expected position is

$$
\hbox{E}\lbrack{\bar X_k}\rbrack =
\hbox{E}\lbrack{\sum_{i=0}^{k-1} S_i}\rbrack = \sum_{i=0}^{k-1} \hbox{E}\lbrack{S_i}\rbrack= k(1-2p)\thinspace .
$$

All the $S_k$ variables are independent. The variance therefore becomes

$$
\hbox{Var}\lbrack{\bar X_k}\rbrack = \hbox{Var}\lbrack{\sum_{i=0}^{k-1} S_i}\rbrack = \sum_{i=0}^{k-1} \hbox{Var}\lbrack{S_i}\rbrack =
k4p(1-p)\thinspace .
$$

We see that ${\hbox{Var}\lbrack\bar X_k\rbrack}$ is proportional with the number of steps $k$.
For the very important case $p=q=\frac{1}{2}$, $\hbox{E}\lbrack{\bar X_k}\rbrack=0$ and
$\hbox{Var}\lbrack{\bar X_k}\rbrack=k$.

How can we estimate $\hbox{E}\lbrack{\bar X_k}\rbrack=0$ and $\hbox{Var}\lbrack{\bar X_k}\rbrack=N$?
We must have many random walks of the type in
[Figure](#diffu:randomwalk:1D:fig:ensemble). For a given $k$, say $k=100$,
we find all the values of $\bar X_k$, name them $\bar x_{0,k}$, $\bar x_{1,k}$,
$\bar x_{2,k}$, and so on. The empirical estimate of $\hbox{E}\lbrack{\bar X_k}\rbrack$ is the
average,

$$
\hbox{E}\lbrack{\bar X_k}\rbrack \approx \frac{1}{W}\sum_{j=0}^{W-1} \bar x_{j,k},
$$

while an empirical estimate of $\hbox{Var}\lbrack{\bar X_k}\rbrack$ is

$$
\hbox{Var}\lbrack{\bar X_k}\rbrack \approx \frac{1}{W}\sum_{j=0}^{W-1} (\bar x_{j,k})^2 -
\left(\frac{1}{W}\sum_{j=0}^{W-1} \bar x_{j,k}\right)^2\thinspace .
$$

That is, we take the statistics for a given $K$ across the ensemble
of random walks ("vertically" in
[Figure](#diffu:randomwalk:1D:fig:ensemble)). The key quantities
to record are $\sum_i \bar x_{i,k}$ and $\sum_i \bar x_{i,k}^2$.

## Playing around with some code

Python has a `random` module for drawing random numbers, and this
module has a function `uniform(a, b)` for drawing a uniformly
distributed random number in the interval $[a,b)$.  If an event
happens with probability $p$, we can simulate this on the computer by
drawing a random number $r$ in $[0,1)$, because then $r\leq p$ with
probability $p$ and $r>p$ with probability $1-p$:

```python
import random
r = random.uniform(0, 1)
if r <= p:
    # Event happens
else:
    # Event does not happen
```

A random walk with $N$ steps, starting at $x_0$, where we move
to the left with probability $p$ and to the right
with probability $1-p$ can now be implemented by

```python
import random, numpy as np
```

```python
# %load -s random_walk1D, src-diffu/random_walk.py
def random_walk1D(x0, N, p, random=random):
    """1D random walk with 1 particle and N moves."""
    # random is argument so we can use np.random instead
    # and use it for testing

    # Store position in step k in position[k]
    position = np.zeros(N+1)
    position[0] = x0
    current_pos = x0
    for k in range(N):
        r = random.uniform(0, 1)
        if r <= p:
            current_pos -= 1
        else:
            current_pos += 1
        position[k+1] = current_pos
    return position

```

Since $N$ is supposed to be large and we want to repeat the process for
many particles, we should speed up the code as much as possible.
Vectorization is the obvious technique here: we draw all the random
numbers at once with aid of `numpy`, and then we formulate vector
operations to get rid of the loop over the steps (`k`).
The `numpy.random` module has vectorized versions of the functions in
Python's built-in `random` module. For example, `numpy.random.uniform(a, b, N)`
returns `N` random numbers uniformly distributed between `a` (included)
and `b` (not included).

We can then make an array of all the steps in a random walk: if
the random number is less than or equal to $p$, the step is $-1$,
otherwise the step is $1$:

```python
N=200
p=50
r = np.random.uniform(0, 1, size=N)
steps = np.where(r <= p, -1, 1)
```

Our vectorized implementation using Devito is as follows:

```python
from devito import Dimension, Constant, TimeFunction, Function, Eq, Operator
```

```python
# %load -s random_walk1D_vec, src-diffu/random_walk.py
def random_walk1D_vec(x0, N, p, random=random):
    """Vectorized version of random_walk1D."""
    # Create and initialise r with dimension x_d
    x_d = Dimension(name='x_d', spacing=Constant('h_x'))
    r = TimeFunction(name='r', dimensions=(x_d,), shape=(N+1,))
    r.data[0] = x0

    rs = random.uniform(0, 1, size=N)
    steps = Function(name='steps', dimensions=(x_d,), shape=(N+1,))
    steps.data[:N] = np.where(rs <= p, -1, 1)

    # The next value is computed using the previous value and the steps function at that point
    eq = Eq(r.forward, r + steps)
    op = Operator(eq)
    op.apply()
    return r.data

```

### Fixing the random sequence

During software development with random numbers it is advantageous to
always generate the same sequence of random numbers, as this may help
debugging processes. To fix the sequence, we set a *seed* of the random
number generator to some chosen integer, e.g.,

```python
np.random.seed(10)
```

Calls to `random_walk1D` give positions of the particle as
depicted in [Figure](#diffu:randomwalk:1D:code1:fig1). The particle starts
at the origin and moves with $p=\frac{1}{2}$. Since the seed is the same,
the plot to the left is just a magnification of the first 1,000 steps in
the plot to the right.

1,000 (left) and 50,000 (right) steps of a random walk.

## Equivalence with diffusion

The original random walk algorithm can be said to work with
dimensionless coordinates $\bar x_i = -N + i$, $i=0,1,\ldots, 2N+1$
($i\in [-N,N]$), and $\bar t_n=n$, $n=0,1,\ldots,N$.  A mesh with
spacings $\Delta x$ and $\Delta t$ with dimensions can be introduced
by

$$
x_i = X_0 + \bar x_i \Delta x,\quad t_n = \bar t_n\Delta t\thinspace .
$$

If we implement the algorithm with dimensionless coordinates, we can just
use this rescaling to obtain the movement in a coordinate system
without unit spacings.

Let $P^{n+1}_i$ be the probability of finding the particle at mesh point
$\bar x_i$ at time $\bar t_{n+1}$. We can reach mesh point $(i,n+1)$ in two
ways: either coming in from the left from $(i-1,n)$ or from the
right ($i+1,n)$. Each has probability $\frac{1}{2}$ (if we assume
$p=q=\frac{1}{2}$). The fundamental equation for $P^{n+1}_i$ is

$$
\begin{equation}
P^{n+1}_i = \frac{1}{2} P^{n}_{i-1} + \frac{1}{2} P^{n}_{i+1}\thinspace .
\label{diffu:randomwalk:1D:pde:Markov} \tag{1}
\end{equation}
$$

(This equation is easiest to understand if one looks at the random walk
as a Markov process and applies the transition probabilities, but this is
beyond scope of the present text.)

Subtracting $P^{n}_i$ from ([1](#diffu:randomwalk:1D:pde:Markov)) results
in

$$
P^{n+1}_i - P^{n}_i = \frac{1}{2} (P^{n}_{i-1} -2P^{n}_i + \frac{1}{2} P^{n}_{i+1})\thinspace .
$$

Readers who have seen the Forward Euler discretization of a 1D
diffusion equation recognize this scheme as very close to such a
discretization. We have

$$
\frac{\partial}{\partial t}P(x_i,t_{n})
= \frac{P^{n+1}_i - P^{n}_i}{\Delta t} + \mathcal{O}({\Delta t}),
$$

or in dimensionless coordinates

$$
\frac{\partial}{\partial\bar t}P(\bar x_i,\bar t_n)
\approx P^{n+1}_i - P^{n}_i\thinspace .
$$

Similarly, we have

$$
\begin{align*}
\frac{\partial^2}{\partial x^2}P(x_i,t_n) &=
\frac{P^{n}_{i-1} -2P^{n}_i + \frac{1}{2} P^{n}_{i+1}}{\Delta x^2}
+ \mathcal{O}({\Delta x^2}),\\
\frac{\partial^2}{\partial x^2}P(\bar x_i,\bar t_n) &\approx
P^{n}_{i-1} -2P^{n}_i + \frac{1}{2} P^{n}_{i+1}\thinspace .
\end{align*}
$$

Equation ([1](#diffu:randomwalk:1D:pde:Markov)) is therefore equivalent with
the dimensionless diffusion equation

$$
\begin{equation}
\frac{\partial P}{\partial\bar t} = \frac{1}{2}
\frac{\partial^2 P}{\partial \bar x^2},
\label{diffu:randomwalk:1D:pde:dimless} \tag{2}
\end{equation}
$$

or the diffusion equation

$$
\begin{equation}
\frac{\partial P}{\partial t} = D\frac{\partial^2 P}{\partial x^2},
\label{diffu:randomwalk:1D:pde:dim} \tag{3}
\end{equation}
$$

with diffusion coefficient

$$
D = \frac{\Delta x^2}{2\Delta t}\thinspace .
$$

This derivation shows the tight link between random walk and diffusion.
If we keep track of where the particle is, and repeat the process
many times, or run the algorithms for lots of particles, the histogram
of the positions will approximate the solution of the diffusion equation
for the local probability $P^n_i$.

Suppose all the random walks start at the origin. Then the initial
condition for the probability distribution is the Dirac delta
function $\delta(x)$. The solution of ([2](#diffu:randomwalk:1D:pde:dimless))
can be shown to be

$$
\begin{equation}
\bar P(\bar x,\bar t) = \frac{1}{\sqrt{4\pi\alpha t}}e^{-\frac{x^2}{4\alpha t}},
\label{diffu:randomwalk:1D:pde:dimless:sol} \tag{4}
\end{equation}
$$

where $\alpha = \frac{1}{2}$.

## Implementation of multiple walks

Our next task is to implement an ensemble of walks (for statistics,
see the section [Statistical considerations](#diffu:randomwalk:1D:EVar)) and also provide data from
the walks such that we can compute the probabilities of the positions
as introduced in the previous section. An appropriate representation
of probabilities $P^n_i$ are histograms (with $i$ along the $x$ axis)
for a few selected values of $n$.

To estimate the expectation and variance of the random walks,
the section [Statistical considerations](#diffu:randomwalk:1D:EVar) points to recording
$\sum_j x_{j,k}$ and $\sum_j x_{j,k}^2$, where $x_{j,k}$ is the
position at time/step level $k$ in random walk number $j$.
The histogram of positions needs the individual values $x_{i,k}$
for all $i$ values and some selected $k$ values.

We introduce `position[k]` to hold $\sum_j x_{j,k}$,
`position2[k]` to hold $\sum_j (x_{j,k})^2$, and
`pos_hist[i,k]` to hold $x_{i,k}$. A selection of $k$ values can be
specified by saying how many, `num_times`, and let them be equally
spaced through time:

```python
pos_hist_times = [(N//num_times)*i for i in range(num_times)]
```

This is one of the few situations where we want integer division (`//`) or
real division rounded to an integer.

### Vectorized version

We have already vectorized a single random walk. The additional
challenge here is to vectorize the computation of the data for the
histogram, `pos_hist`, but given the selected steps in `pos_hist_times`,
we can find the corresponding positions by indexing with the
list `pos_hist_times`: `position[post_hist_times]`, which are to be
inserted in `pos_hist[n,:]`.

```python
# %load -s random_walks1D_vec, src-diffu/random_walk.py
def random_walks1D_vec(x0, N, p, num_walks=1, num_times=1, random=random):
    """Vectorized version of random_walks1D."""
    position = np.zeros(N+1)    # Accumulated positions
    position2 = np.zeros(N+1)    # Accumulated positions**2
    # Histogram at num_times selected time points
    pos_hist = np.zeros((num_walks, num_times))
    pos_hist_times = [(N//num_times)*i for i in range(num_times)]

    # Create and initialise our TimeFunction r
    x_d = Dimension(name='x_d')
    t_d = Dimension(name='t_d')
    r = TimeFunction(name='r', dimensions=(x_d, t_d), shape=(N+1, num_walks))

    # Setting each walk's first element to x0
    r.data[0,:] = x0

    steps = Function(name='steps', dimensions=(x_d,t_d), shape=(N+1,num_walks))

    # Populating steps with -1 if value in rs <= p at that point and 1 otherwise
    rs = random.uniform(0, 1, size=N*num_walks).reshape(num_walks, N)
    for n in range(num_walks):
        steps.data[:N, n] = np.where(rs[n] <= p, -1, 1)

    # Creating and applying operator that contains equation for adding steps
    eq = Eq(r.forward, r + steps)
    op = Operator(eq)
    op.apply()

    # Summing over data to give positions
    position = np.sum(r.data, axis=1)               # Accumulated positions
    position2 = np.sum(r.data**2, axis=1)           # Accumulated positions**2

    pos_hist[:,:] = np.transpose(r.data[pos_hist_times,:])

    return position, position2, pos_hist, np.array(pos_hist_times)

```

What is the gain of the vectorized implementations? One important gain
is that each vectorized operation can be automatically parallelized
if one applies a parallel `numpy` library like [Numba](http://numba.pydata.org). On a single CPU, however, the speed up of the vectorized operations
is also significant. With $N=1,000$ and 50,000 repeated walks,
the two vectorized versions run about 25 and 18 times faster than the scalar
version, with `random_walks1D_vec1` being fastest.

### Test function

During program development, it is highly recommended to carry out
computations by hand for, e.g., `N=4` and `num_walks=3`.  Normally,
this is done by executing the program with these parameters and
checking with pen and paper that the computations make sense.  The
next step is to use this test for correctness in a formal test
function.

We need to check that the simulation of multiple random walks
reproduces the results of `random_walk1D` for the first walk, if the seed is the
same.

We arrive at the test function below.

```python
def test_random_walks1D():
    # For fixed seed, check that scalar and vectorized versions
    # produce the same result
    x0 = 0;  N = 4;  p = 0.5

    # Check that random_walks1D for 1 walk reproduces
    # the walk in random_walk1D
    num_walks = 1
    np.random.seed(10)
    computed = random_walks1D_vec(
        x0, N, p, num_walks, random=np.random)
    np.random.seed(10)
    expected = random_walk1D_vec(
        x0, N, p, random=np.random)
    print("Computed: ", computed[0])
    print("Expected: ", expected)
    assert (computed[0] == expected).all()
```

```python
test_random_walks1D()
```

    Operator `Kernel` run in 0.01 s
    Operator `Kernel` run in 0.01 s

    Computed:  [0. 1. 0. 1. 2.]
    Expected:  [0. 1. 0. 1. 2.]

Such test functions are indispensable for further development of the code
as we can at any time test whether the basic computations remain correct or not.
This is particularly important in stochastic simulations since without
test functions and fixed seeds, we always experience variations from run to
run, and it can be very difficult to spot bugs through averaged statistical
quantities.

A full test suite for this notebook, along with the rest of the functions for random walks, can be found in [`random_walk.py`](https://github.com/devitocodes/devito_book/blob/master/fdm-devito-notebooks/03_diffu/src-diffu/random_walk.py).

## Demonstration of multiple walks

Assuming now that the code works, we can just scale up the number of
steps in each walk and the number of walks. The latter influences the
accuracy of the statistical estimates.  [Figure](#diffu:randomwalk:1D:fig:demo1:EX) shows the impact of the number
of walks on the expectation, which should approach zero.  [Figure](#diffu:randomwalk:1D:fig:demo1:VarX) displays the corresponding
estimate of the variance of the position, which should grow linearly
with the number of steps. mathcal{I}_t does, seemingly very accurately, but
notice that the scale on the $y$ axis is so much larger than for the
expectation, so irregularities due to the stochastic nature of the
process become so much less visible in the variance plots.  The
probability of finding a particle at a certain position at time (or
step) 800 is shown in [Figure](#diffu:randomwalk:1D:fig:demo1:HistX). The dashed red line is the
theoretical distribution ([4](#diffu:randomwalk:1D:pde:dimless:sol))
arising from solving the diffusion equation
([2](#diffu:randomwalk:1D:pde:dimless)) instead. As always, we realize
that one needs significantly more statistical samples to estimate a
histogram accurately than the expectation or variance.

Estimated expected value for 1000 steps, using 100 walks (upper left), 10,000 (upper right), 100,000 (lower left), and 1,000,000 (lower right).

Estimated variance over 1000 steps, using 100 walks (upper left), 10,000 (upper right), 100,000 (lower left), and 1,000,000 (lower right).

Estimated probability distribution at step 800, using 100 walks (upper left), 10,000 (upper right), 100,000 (lower left), and 1,000,000 (lower right).

## Ascii visualization of 1D random walk

If we want to study (very) long time series of random walks, it can be
convenient to plot the position in a terminal window with the time axis
pointing downwards. The module `avplotter` in SciTools has a class `Plotter`
for plotting functions in the terminal window with the aid of ascii symbols
only. Below is the code required to visualize a simple random walk,
starting at the origin, and considered over
when the point $x=-1$ is reached. We use a spacing $\Delta x = 0.05$ (so
$x=-1$ corresponds to $i=-20$).

```python
%matplotlib inline

def run_random_walk():
    from scitools.avplotter import Plotter
    import time, numpy as np
    p = Plotter(-1, 1, width=75)   # Horizontal axis: 75 chars wide
    dx = 0.05
    np.random.seed(10)

    x = 0
    while True:
        random_step = 1 if np.random.random() > 0.5 else -1
        x = x + dx*random_step
        if x < -1:
            break                  # Destination reached!
        print(p.plot(0, x))

        # Allow Ctrl+c to abort the simulation
        try:
            time.sleep(0.1)  # Wait for interrupt
        except KeyboardInterrupt:
            print('Interrupted by Ctrl+c')
            break
```

Observe that we implement an infinite loop, but allow a smooth interrupt
of the program by `Ctrl+c` through Python's `KeyboardInterrupt`
exception. This is a useful recipe that can be used in many occasions!

The output looks typically like

                                    *         |
                                      *       |
                                    *         |
                                      *       |
                                    *         |
                                      *       |
                                        *     |
                                      *       |
                                    *         |
                                  *           |
                                *             |
                              *               |
                                *             |
                              *               |
                             *                |
                           *                  |
                         *                    |
                       *                      |

Positions beyond the limits of the $x$ axis appear with a value.
[A long file](http://bit.ly/1UbULeH)
contains the complete ascii plot corresponding to the
function `run_random_walk` above.

## Random walk as a stochastic equation

The (dimensionless) position in a random walk, $\bar X_k$, can be expressed as
a stochastic difference equation:

$$
\begin{equation}
\bar X_k = \bar X_{k-1} + s, \quad x_0=0,
\label{diffu:randomwalk:1D:ode:x} \tag{5}
\end{equation}
$$

where $s$ is a [Bernoulli variable](https://en.wikipedia.org/wiki/Bernoulli_distribution),
taking on the two values $s=-1$ and $s=1$
with equal probability:

$$
\hbox{P}(s=1)=\frac{1}{2},\quad \hbox{P}(s=-1)=\frac{1}{2}\thinspace .
$$

The $s$ variable in a step is independent of the $s$ variable in other steps.

The difference equation expresses essentially the sum of independent
Bernoulli variables.  Because of the central limit theorem, $X_k$,
will then be normally distributed with expectation $k\hbox{E}\lbrack{s}\rbrack$ and
$k\hbox{Var}\lbrack{s}\rbrack$.  The expectation and variance of a Bernoulli variable with
values $r=0$ and $r=1$ are $p$ and $p(1-p)$, respectively.
The variable $s=2r-1$ then has expectation
$2\hbox{E}\lbrack{r}\rbrack-1=2p-1=0$ and variance $2^2\hbox{Var}\lbrack{r}\rbrack=4p(1-p)=1$.  The position
$X_k$ is normally distributed with zero expectation and variance $k$,
as we found in the section [Statistical considerations](#diffu:randomwalk:1D:EVar).

The central limit theorem tells that as long as $k$ is not small,
the distribution of $X_k$ remains the same if
we replace the Bernoulli variable $s$ by any other stochastic variable with
the same expectation and variance. In particular, we may let $s$ be a
standardized Gaussian variable (zero mean, unit variance).

Dividing ([5](#diffu:randomwalk:1D:ode:x)) by $\Delta t$ gives

$$
\frac{\bar X_k - \bar X_{k-1}}{\Delta t} = \frac{1}{\Delta t} s\thinspace .
$$

In the limit $\Delta t\rightarrow 0$, $s/\Delta t$ approaches a white noise
stochastic process.
With $\bar X(t)$ as the continuous process in the limit
$\Delta t\rightarrow 0$ ($X_k\rightarrow X(t_k)$),
we formally get the stochastic differential equation

$$
\begin{equation}
d\bar X = dW,
\label{_auto1} \tag{6}
\end{equation}
$$

where $W(t)$ is a [Wiener process](https://en.wikipedia.org/wiki/Wiener_process). Then $X$ is also a
Wiener process. It follows from the stochastic ODE $dX=dW$ that the
probability distribution of $X$ is given by the [Fokker-Planck
equation](https://en.wikipedia.org/wiki/Fokker-Planck_equation)
([2](#diffu:randomwalk:1D:pde:dimless)).  In other words, the key
results for random walk we found earlier can alternatively be
derived via a stochastic ordinary differential equation and its
related Fokker-Planck equation.

## Random walk in 2D

The most obvious generalization of 1D random walk to two spatial
dimensions is to allow movements to the north, east, south, and west,
with equal probability $\frac{1}{4}$. This can be intuitively demonstrated using the following scalar implementation:

```python
# %load -s random_walk2D, src-diffu/random_walk.py
def random_walk2D(x0, N, p, random=random):
    """2D random walk with 1 particle and N moves: N, E, W, S."""
    # Store position in step k in position[k]
    d = len(x0)
    position = np.zeros((N+1, d))
    position[0, :] = x0
    current_pos = np.array(x0, dtype=float)
    for k in range(N):
        r = random.uniform(0, 1)
        if r <= 0.25:
            current_pos += np.array([0, 1])   # Move north
        elif 0.25 < r <= 0.5:
            current_pos += np.array([1, 0])   # Move east
        elif 0.5 < r <= 0.75:
            current_pos += np.array([0, -1])  # Move south
        else:
            current_pos += np.array([-1, 0])  # Move west
        position[k+1, :] = current_pos
    return position

```

The left plot in [Figure](#diffu:randomwalk:2D:fig:rect_vs_diag) provides
an example on 200 steps with this kind of walk. We may refer to this walk
as a walk on a *rectangular mesh* as we move from any spatial
mesh point $(i,j)$ to one of its four neighbors in the rectangular directions:
$(i+1,j)$, $(i-1,j)$, $(i,j+1)$, or $(i,j-1)$.

Random walks in 2D with 200 steps: rectangular mesh (left) and diagonal mesh (right).

## Random walk in any number of space dimensions

From a programming point of view, especially when implementing a random
walk in any number of dimensions, it is more natural to consider a walk
in the diagonal directions NW, NE, SW, and SE. On a two-dimensional spatial mesh
it means that we go from $(i,j)$ to either $(i+1,j+1)$, $(i-1,j+1)$,
$(i+1,j-1)$, or $(i-1,j-1)$. We can with such a *diagonal mesh*
(see right plot in [Figure](#diffu:randomwalk:2D:fig:rect_vs_diag))
draw a Bernoulli variable for the step in each spatial direction and
trivially write code that works in any number of spatial directions:

```python
# %load -s random_walkdD, src-diffu/random_walk.py
def random_walkdD(x0, N, p, random=random):
    """Any-D (diagonal) random walk with 1 particle and N moves."""
    # Store position in step k in position[k]
    d = len(x0)
    position = np.zeros((N+1, d))
    position[0, :] = x0
    current_pos = np.array(x0, dtype=float)
    for k in range(N):
        for i in range(d):
            r = random.uniform(0, 1)
            if r <= p:
                current_pos[i] -= 1
            else:
                current_pos[i] += 1
        position[k+1, :] = current_pos
    return position

```

A vectorized version is desired. We follow the ideas from the section [Playing around with some code](#diffu:randomwalk:1D:code1). We need to add an extra `Dimension` to our `TimeFunction`, representing the number of space dimensions. We also need to generate `N*d` random values, which we can do using the `uniform` function from before and specifying the `size` argument as `N*d`. This can be represented as follows:

```python
# %load -s random_walkdD_vec, src-diffu/random_walk.py
def random_walkdD_vec(x0, N, p, random=random):
    """Vectorized version of random_walkdD."""

    # Here, x0 is an array of initial values in each spatial dimension
    d = len(x0)

    x_d = Dimension(name='x_d', spacing=Constant('h_x'))
    # Add space dimensions
    d_d = Dimension(name='d', spacing=Constant('h_d'))
    r = TimeFunction(name='r', dimensions=(x_d, d_d), shape=(N+1, d))
    r.data[0] = x0

    rs = random.uniform(0, 1, size=N*d).reshape(N,d)
    steps = Function(name='steps', dimensions=(x_d, d_d), shape=(N+1, d))
    steps.data[:N] = np.where(rs <= p, -1, 1)

    # The next value is computed using the previous value and the steps function at that point
    eq = Eq(r.forward, r + steps)
    op = Operator(eq)
    op.apply()
    return r.data

```

Four random walks with 5000 steps in 2D.

## Multiple random walks in any number of space dimensions

As we did in 1D, we extend one single walk to a number of walks (`num_walks` in the code). We can take a look at our function for multiple random walks (`random_walks1D`) from the [Implementation of multiple walks](#diffu:randomwalk:1D:multiplewalks) section. The changes we make here are similar to those we made for `random_walkdD_vec`, in that we add an extra `Dimension` to represent the number of space dimensions.

```python
# %load -s random_walksdD_vec, src-diffu/random_walk.py
def random_walksdD_vec(x0, N, p, num_walks=1, num_times=1, random=np.random):
    """Vectorized version of random_walksdD."""
    d = len(x0)
    position  = np.zeros((N+1, d))  # Accumulated positions
    position2 = np.zeros((N+1, d))  # Accumulated positions**2
    # Histogram at num_times selected time points
    pos_hist = np.zeros((num_walks, num_times, d))
    pos_hist_times = [(N//num_times)*i for i in range(num_times)]

    # Create and initialise our TimeFunction r
    x_d = Dimension(name='x_d')
    t_d = Dimension(name='t_d')
    d_d = Dimension(name='d_d')
    r = TimeFunction(name='r', dimensions=(x_d, t_d, d_d), shape=(N+1, num_walks, d))
    # Setting each walk's first element to x0
    r.data[0] = x0

    steps = Function(name='steps', dimensions=(x_d,t_d,d_d), shape=(N+1,num_walks,d))

    # Populating steps with -1 if value in rs <= p at that point and 1 otherwise
    rs = random.uniform(0, 1, size=N*num_walks*d).reshape(num_walks, N, d)
    for n in range(num_walks):
        steps.data[:N, n] = np.where(rs[n] <= p, -1, 1)

    # Creating and applying operator that contains equation for adding steps
    eq = Eq(r.forward, r + steps)
    op = Operator(eq)
    op.apply()

    # Summing over data to give positions
    position = np.sum(r.data, axis=1)               # Accumulated positions
    position2 = np.sum(r.data**2, axis=1)           # Accumulated positions**2

    for k in range(num_walks):
        pos_hist[k,:] = r.data[pos_hist_times,k]

    return position, position2, pos_hist, np.array(pos_hist_times)

```