# Solving Partial Differential Equations using Numerical Methods

### This notebook shows the solutions to the following Partial Differential Equations using Numerical Methods (Forward Euler Method)

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
```

```python
#to be seperated for wave equation and heat equation
class FiniteDiff:

    def __init__(self, I, xmin, xmax, tmin, tmax, c, dx, dt, Heat = False):

        self.dx = dx
        self.xmin = xmin
        self.xmax = xmax
        self.dt = dt
        self.tmin = tmin
        self.tmax = tmax
        self.c = c
        self.F = c*dt/dx
        self.Heat = Heat
        if Heat == True:
            self.F = (c**2)*(dt/(dx**2))
        self.I = I

        self.nx = int((self.xmax-self.xmin)/dx)
        self.nt = int((self.tmax-self.tmin)/dt)

        self.x = np.linspace(self.xmin, self.xmax, (self.nx+1))
        self.t = np.linspace(self.tmin, self.tmax, (self.nt+1))

        self.u = np.zeros((self.nt+1, self.nx+1))

    def do(self):

        for i in range(0, self.nx+1):
            self.u[0, i] = self.I(self.x[i])

        if self.Heat == True:
            for n in range(0, self.nt):
                for i in range(1, self.nx):
                    self.u[n+1, i] = self.u[n,i] + self.F*(self.u[n,i-1] - 2*self.u[n,i] + self.u[n,i+1])
                    self.u[n,0], self.u[n,self.nx] = 0, 0
        else:
            for n in range(0, self.nt):
                for i in range(1, self.nx):
                    self.u[n+1, i] = 2*(1-(self.F**2))*self.u[n,i] - self.u[n-1,i] + (self.F**2)*(self.u[n,i+1]+self.u[n,i-1])
                    self.u[n,0], self.u[n,self.nx] = 0, 0

        self.data = pd.DataFrame(self.u[:,1:-1])

        return self.data

    def plot(self, title):

        xmat, tmat = np.meshgrid(self.x,self.t)
        fig = plt.figure()
        ax = Axes3D(fig)
        surface = ax.plot_surface(tmat,xmat,self.u)
        plt.xlabel('t')
        plt.ylabel('x')
        plt.title(str(title))
        ax.view_init(40,60)
        plt.show()
```

## Wave Equation of a Plucked String

Use the finite difference method to solve the wave equation for a plucked string:

\begin{equation}
\frac{\delta^2u}{\delta t^2} = 4\frac{\delta^2u}{\delta x^2} \ for \ 0\leq x \leq 1 \ and \ 0\leq t \leq 0.5 \\
u(0,t) = 0 \ for \ 0\leq t \leq 0.5 \\
u(1,t) = 0 \ for \ 0\leq t \leq 0.5 \\
u(x,0) = f(x) = x  \ \ \ for \ 0\leq x \leq \frac{3}{5} \\
u(x,0) = f(x) = 1.5 - 1.5x \ \ \ for \ \frac{3}{5}\leq x \leq 1 \\
\frac{\delta u}{\delta u} = g(x) = 0 \ for \ 0\leq x \leq 1 \\
where: \\
\Delta x = 0.1 \\
\Delta t = 0.05 \\
c\frac{\Delta t}{\Delta x} = 1
\end{equation}

```python
xmin, xmax, dx = 0, 1, 0.1
tmin, tmax, dt = 0, 0.5, 0.05
c = 2

I = lambda x: 1.5 - 1.5*x if x>=(3/5) else x

String = FiniteDiff(I, xmin, xmax, tmin, tmax, c, dx, dt, Heat = False)
String.do()
```

      0
      1
      2
      3
      4
      5
      6
      7
      8

      0
      1.000000e-01
      2.000000e-01
      3.000000e-01
      0.40
      5.000000e-01
      0.60
      4.500000e-01
      3.000000e-01
      1.500000e-01

      1
      2.000000e-01
      4.000000e-01
      6.000000e-01
      0.80
      1.000000e+00
      0.95
      9.000000e-01
      6.000000e-01
      3.000000e-01

      2
      3.000000e-01
      6.000000e-01
      9.000000e-01
      1.20
      1.250000e+00
      1.30
      1.100000e+00
      9.000000e-01
      4.500000e-01

      3
      4.000000e-01
      8.000000e-01
      1.200000e+00
      1.35
      1.500000e+00
      1.40
      1.300000e+00
      9.500000e-01
      6.000000e-01

      4
      5.000000e-01
      1.000000e+00
      1.250000e+00
      1.50
      1.500000e+00
      1.50
      1.250000e+00
      1.000000e+00
      5.000000e-01

      5
      6.000000e-01
      9.500000e-01
      1.300000e+00
      1.40
      1.500000e+00
      1.35
      1.200000e+00
      8.000000e-01
      4.000000e-01

      6
      4.500000e-01
      9.000000e-01
      1.100000e+00
      1.30
      1.250000e+00
      1.20
      9.000000e-01
      6.000000e-01
      3.000000e-01

      7
      3.000000e-01
      6.000000e-01
      9.000000e-01
      0.95
      1.000000e+00
      0.80
      6.000000e-01
      4.000000e-01
      2.000000e-01

      8
      1.500000e-01
      3.000000e-01
      4.500000e-01
      0.60
      5.000000e-01
      0.40
      3.000000e-01
      2.000000e-01
      1.000000e-01

      9
      -2.220446e-16
      -2.220446e-16
      -4.440892e-16
      0.00
      -2.220446e-16
      0.00
      2.220446e-16
      2.220446e-16
      -4.440892e-16

      10
      -1.500000e-01
      -3.000000e-01
      -4.500000e-01
      -0.60
      -5.000000e-01
      -0.40
      -3.000000e-01
      -2.000000e-01
      -1.000000e-01

```python
String.plot(title = '1D Wave Equation of a Plucked String')
```

## 1D Wave Equation

\begin{equation}
\frac{\delta^2u}{\delta t^2} = 300\frac{\delta^2u}{\delta x^2} \ for \ 0\leq x \leq 1 \ and \ 0\leq t \leq 0.5 \\
u(0,t) = 0 \ for \ 0\leq t \leq 0.5 \\
u(1,t) = 0 \ for \ 0\leq t \leq 0.5 \\
u(x,0) = f(x) = e^{-k(x-x_0)^2}  \ \ \ for \ 0\leq x \leq \frac{3}{5} \\
\frac{\delta u}{\delta u} = g(x) = 0 \ for \ 0\leq x \leq 1 \\
where: \\
\Delta x = 0.01 \\
\Delta t = \frac{\Delta x}{c} \\
k = 1000 \\
x_0 = 0.3
\end{equation}

```python
xmin, xmax, dx = 0, 1, 0.01
tmin, tmax, dt = 0, 0.5, (0.01/300)
c = 300
k = 1000
x0 = 0.3

I = lambda x: np.exp(-k*((x-x0)**2))

WavE = FiniteDiff(I, xmin, xmax, tmin, tmax, c, dx, dt, Heat = False)
WavE.do()
```

      0
      1
      2
      3
      4
      5
      6
      7
      8
      9
      ...
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98

      0
      2.991122e-37
      8.939487e-35
      2.187420e-32
      4.382208e-30
      7.187782e-28
      9.652456e-26
      1.061260e-23
      9.553161e-22
      7.040660e-20
      4.248354e-18
      ...
      4.508027e-157
      2.506244e-162
      1.140778e-167
      4.251284e-173
      1.297120e-178
      3.240271e-184
      6.627102e-190
      1.109704e-195
      1.521359e-201
      1.707643e-207

      1
      8.939569e-35
      2.187450e-32
      4.382298e-30
      7.188000e-28
      9.652894e-26
      1.061332e-23
      9.554126e-22
      7.041721e-20
      4.249310e-18
      2.099495e-16
      ...
      6.638819e-152
      4.508027e-157
      2.506244e-162
      1.140778e-167
      4.251284e-173
      1.297120e-178
      3.240271e-184
      6.627102e-190
      1.109704e-195
      1.521359e-201

      2
      2.187420e-32
      4.382298e-30
      7.188000e-28
      9.652894e-26
      1.061332e-23
      9.554126e-22
      7.041721e-20
      4.249310e-18
      2.099495e-16
      8.493293e-15
      ...
      8.004538e-147
      6.638819e-152
      4.508027e-157
      2.506244e-162
      1.140778e-167
      4.251284e-173
      1.297120e-178
      3.240271e-184
      6.627102e-190
      1.109704e-195

      3
      4.382208e-30
      7.188000e-28
      9.652894e-26
      1.061332e-23
      9.554126e-22
      7.041721e-20
      4.249310e-18
      2.099495e-16
      8.493293e-15
      2.813285e-13
      ...
      7.901742e-142
      8.004538e-147
      6.638819e-152
      4.508027e-157
      2.506244e-162
      1.140778e-167
      4.251284e-173
      1.297120e-178
      3.240271e-184
      6.627102e-190

      4
      7.187782e-28
      9.652894e-26
      1.061332e-23
      9.554126e-22
      7.041721e-20
      4.249310e-18
      2.099495e-16
      8.493293e-15
      2.813285e-13
      7.630358e-12
      ...
      6.386317e-137
      7.901742e-142
      8.004538e-147
      6.638819e-152
      4.508027e-157
      2.506244e-162
      1.140778e-167
      4.251284e-173
      1.297120e-178
      3.240271e-184

      5
      9.652456e-26
      1.061332e-23
      9.554126e-22
      7.041721e-20
      4.249310e-18
      2.099495e-16
      8.493293e-15
      2.813285e-13
      7.630358e-12
      1.694711e-10
      ...
      4.225900e-132
      6.386317e-137
      7.901742e-142
      8.004538e-147
      6.638819e-152
      4.508027e-157
      2.506244e-162
      1.140778e-167
      4.251284e-173
      1.297120e-178

      6
      1.061260e-23
      9.554126e-22
      7.041721e-20
      4.249310e-18
      2.099495e-16
      8.493293e-15
      2.813285e-13
      7.630358e-12
      1.694711e-10
      3.082510e-09
      ...
      2.289439e-127
      4.225900e-132
      6.386317e-137
      7.901742e-142
      8.004538e-147
      6.638819e-152
      4.508027e-157
      2.506244e-162
      1.140778e-167
      4.251284e-173

      7
      9.553161e-22
      7.041721e-20
      4.249310e-18
      2.099495e-16
      8.493293e-15
      2.813285e-13
      7.630358e-12
      1.694711e-10
      3.082510e-09
      4.592286e-08
      ...
      1.015500e-122
      2.289439e-127
      4.225900e-132
      6.386317e-137
      7.901742e-142
      8.004538e-147
      6.638819e-152
      4.508027e-157
      2.506244e-162
      1.140778e-167

      8
      7.040660e-20
      4.249310e-18
      2.099495e-16
      8.493293e-15
      2.813285e-13
      7.630358e-12
      1.694711e-10
      3.082510e-09
      4.592286e-08
      5.604729e-07
      ...
      3.687840e-118
      1.015500e-122
      2.289439e-127
      4.225900e-132
      6.386317e-137
      7.901742e-142
      8.004538e-147
      6.638819e-152
      4.508027e-157
      2.506244e-162

      9
      4.248354e-18
      2.099495e-16
      8.493293e-15
      2.813285e-13
      7.630358e-12
      1.694711e-10
      3.082510e-09
      4.592286e-08
      5.604729e-07
      5.605436e-06
      ...
      1.096491e-113
      3.687840e-118
      1.015500e-122
      2.289439e-127
      4.225900e-132
      6.386317e-137
      7.901742e-142
      8.004538e-147
      6.638819e-152
      4.508027e-157

      10
      2.098791e-16
      8.493292e-15
      2.813285e-13
      7.630358e-12
      1.694711e-10
      3.082510e-09
      4.592286e-08
      5.604729e-07
      5.605436e-06
      4.596040e-05
      ...
      2.669190e-109
      1.096491e-113
      3.687840e-118
      1.015500e-122
      2.289439e-127
      4.225900e-132
      6.386317e-137
      7.901742e-142
      8.004538e-147
      6.638819e-152

      11
      8.489044e-15
      2.813284e-13
      7.630358e-12
      1.694711e-10
      3.082510e-09
      4.592286e-08
      5.604729e-07
      5.605436e-06
      4.596040e-05
      3.091446e-04
      ...
      5.319795e-105
      2.669190e-109
      1.096491e-113
      3.687840e-118
      1.015500e-122
      2.289439e-127
      4.225900e-132
      6.386317e-137
      7.901742e-142
      8.004538e-147

      12
      2.811185e-13
      7.630354e-12
      1.694711e-10
      3.082510e-09
      4.592286e-08
      5.604729e-07
      5.605436e-06
      4.596040e-05
      3.091446e-04
      1.707518e-03
      ...
      8.680633e-101
      5.319795e-105
      2.669190e-109
      1.096491e-113
      3.687840e-118
      1.015500e-122
      2.289439e-127
      4.225900e-132
      6.386317e-137
      7.901742e-142

      13
      7.621865e-12
      1.694709e-10
      3.082510e-09
      4.592286e-08
      5.604729e-07
      5.605436e-06
      4.596040e-05
      3.091446e-04
      1.707518e-03
      7.755728e-03
      ...
      1.159709e-96
      8.680633e-101
      5.319795e-105
      2.669190e-109
      1.096491e-113
      3.687840e-118
      1.015500e-122
      2.289439e-127
      4.225900e-132
      6.386317e-137

      14
      1.691898e-10
      3.082502e-09
      4.592286e-08
      5.604729e-07
      5.605436e-06
      4.596040e-05
      3.091446e-04
      1.707518e-03
      7.755728e-03
      2.903124e-02
      ...
      1.268492e-92
      1.159709e-96
      8.680633e-101
      5.319795e-105
      2.669190e-109
      1.096491e-113
      3.687840e-118
      1.015500e-122
      2.289439e-127
      4.225900e-132

      15
      3.074880e-09
      4.592258e-08
      5.604729e-07
      5.605436e-06
      4.596040e-05
      3.091446e-04
      1.707518e-03
      7.755728e-03
      2.903124e-02
      8.984073e-02
      ...
      1.135971e-88
      1.268492e-92
      1.159709e-96
      8.680633e-101
      5.319795e-105
      2.669190e-109
      1.096491e-113
      3.687840e-118
      1.015500e-122
      2.289439e-127

      16
      4.575339e-08
      5.604652e-07
      5.605436e-06
      4.596040e-05
      3.091446e-04
      1.707518e-03
      7.755728e-03
      2.903124e-02
      8.984073e-02
      2.309278e-01
      ...
      8.328913e-85
      1.135971e-88
      1.268492e-92
      1.159709e-96
      8.680633e-101
      5.319795e-105
      2.669190e-109
      1.096491e-113
      3.687840e-118
      1.015500e-122

      17
      5.573904e-07
      5.605267e-06
      4.596040e-05
      3.091446e-04
      1.707518e-03
      7.755728e-03
      2.903124e-02
      8.984073e-02
      2.309278e-01
      4.964104e-01
      ...
      4.999773e-81
      8.328913e-85
      1.135971e-88
      1.268492e-92
      1.159709e-96
      8.680633e-101
      5.319795e-105
      2.669190e-109
      1.096491e-113
      3.687840e-118

      18
      5.559513e-06
      4.595732e-05
      3.091444e-04
      1.707518e-03
      7.755728e-03
      2.903124e-02
      8.984073e-02
      2.309278e-01
      4.964104e-01
      9.012478e-01
      ...
      2.457273e-77
      4.999773e-81
      8.328913e-85
      1.135971e-88
      1.268492e-92
      1.159709e-96
      8.680633e-101
      5.319795e-105
      2.669190e-109
      1.096491e-113

      19
      4.539993e-05
      3.090987e-04
      1.707515e-03
      7.755727e-03
      2.903124e-02
      8.984073e-02
      2.309278e-01
      4.964104e-01
      9.012478e-01
      1.401248e+00
      ...
      9.887753e-74
      2.457273e-77
      4.999773e-81
      8.328913e-85
      1.135971e-88
      1.268492e-92
      1.159709e-96
      8.680633e-101
      5.319795e-105
      2.669190e-109

      20
      3.035391e-04
      1.706957e-03
      7.755682e-03
      2.903124e-02
      8.984073e-02
      2.309278e-01
      4.964104e-01
      9.012478e-01
      1.401248e+00
      1.901248e+00
      ...
      3.257489e-70
      9.887753e-74
      2.457273e-77
      4.999773e-81
      8.328913e-85
      1.135971e-88
      1.268492e-92
      1.159709e-96
      8.680633e-101
      5.319795e-105

      21
      1.661557e-03
      7.750122e-03
      2.903068e-02
      8.984068e-02
      2.309278e-01
      4.964104e-01
      9.012478e-01
      1.401248e+00
      1.901248e+00
      2.306085e+00
      ...
      8.786367e-67
      3.257489e-70
      9.887753e-74
      2.457273e-77
      4.999773e-81
      8.328913e-85
      1.135971e-88
      1.268492e-92
      1.159709e-96
      8.680632e-101

      22
      7.446583e-03
      2.898528e-02
      8.983512e-02
      2.309272e-01
      4.964103e-01
      9.012478e-01
      1.401248e+00
      1.901248e+00
      2.306085e+00
      2.571568e+00
      ...
      1.940336e-63
      8.786367e-67
      3.257489e-70
      9.887753e-74
      2.457273e-77
      4.999773e-81
      8.328913e-85
      1.135971e-88
      1.268492e-92
      1.159709e-96

      23
      2.732372e-02
      8.953158e-02
      2.308818e-01
      4.964048e-01
      9.012472e-01
      1.401248e+00
      1.901248e+00
      2.306085e+00
      2.571568e+00
      2.712655e+00
      ...
      3.508210e-60
      1.940336e-63
      8.786367e-67
      3.257489e-70
      9.887753e-74
      2.457273e-77
      4.999773e-81
      8.328913e-85
      1.135971e-88
      1.268492e-92

      24
      8.208500e-02
      2.292202e-01
      4.961012e-01
      9.012018e-01
      1.401242e+00
      1.901247e+00
      2.306085e+00
      2.571568e+00
      2.712655e+00
      2.773464e+00
      ...
      5.193203e-57
      3.508210e-60
      1.940336e-63
      8.786367e-67
      3.257489e-70
      9.887753e-74
      2.457273e-77
      4.999773e-81
      8.328913e-85
      1.135971e-88

      25
      2.018965e-01
      4.886547e-01
      8.995403e-01
      1.400939e+00
      1.901202e+00
      2.306080e+00
      2.571567e+00
      2.712655e+00
      2.773464e+00
      2.794740e+00
      ...
      6.293992e-54
      5.193203e-57
      3.508210e-60
      1.940336e-63
      8.786367e-67
      3.257489e-70
      9.887753e-74
      2.457273e-77
      4.999773e-81
      8.328913e-85

      26
      4.065697e-01
      8.722166e-01
      1.393492e+00
      1.899540e+00
      2.305776e+00
      2.571522e+00
      2.712649e+00
      2.773464e+00
      2.794740e+00
      2.800788e+00
      ...
      6.245371e-51
      6.293992e-54
      5.193203e-57
      3.508210e-60
      1.940336e-63
      8.786367e-67
      3.257489e-70
      9.887753e-74
      2.457273e-77
      4.999773e-81

      27
      6.703200e-01
      1.311407e+00
      1.872217e+00
      2.298329e+00
      2.569860e+00
      2.712346e+00
      2.773418e+00
      2.794734e+00
      2.800788e+00
      2.802186e+00
      ...
      5.073778e-48
      6.245371e-51
      6.293992e-54
      5.193203e-57
      3.508210e-60
      1.940336e-63
      8.786367e-67
      3.257489e-70
      9.887753e-74
      2.457273e-77

      28
      9.048374e-01
      1.670320e+00
      2.216244e+00
      2.542537e+00
      2.704899e+00
      2.771757e+00
      2.794431e+00
      2.800742e+00
      2.802181e+00
      2.802449e+00
      ...
      3.374783e-45
      5.073778e-48
      6.245371e-51
      6.293992e-54
      5.193203e-57
      3.508210e-60
      1.940336e-63
      8.786367e-67
      3.257489e-70
      9.887753e-74

      29
      1.000000e+00
      1.809675e+00
      2.340640e+00
      2.622814e+00
      2.744433e+00
      2.786984e+00
      2.799081e+00
      2.801877e+00
      2.802404e+00
      2.802484e+00
      ...
      1.837814e-42
      3.374783e-45
      5.073778e-48
      6.245371e-51
      6.293992e-54
      5.193203e-57
      3.508210e-60
      1.940336e-63
      8.786367e-67
      3.257489e-70

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      14971
      -6.703200e-01
      -1.311407e+00
      -1.872217e+00
      -2.298329e+00
      -2.569860e+00
      -2.712346e+00
      -2.773418e+00
      -2.794734e+00
      -2.800788e+00
      -2.802186e+00
      ...
      9.858780e-14
      2.575717e-14
      8.348877e-14
      3.552714e-14
      5.861978e-14
      1.776357e-14
      2.930989e-14
      7.993606e-15
      1.154632e-14
      -1.332268e-14

      14972
      -4.065697e-01
      -8.722166e-01
      -1.393492e+00
      -1.899540e+00
      -2.305776e+00
      -2.571522e+00
      -2.712649e+00
      -2.773464e+00
      -2.794740e+00
      -2.800788e+00
      ...
      2.575717e-14
      7.371881e-14
      3.641532e-14
      8.704149e-14
      2.486900e-14
      4.884981e-14
      8.881784e-15
      1.776357e-14
      -3.552714e-15
      -1.332268e-14

      14973
      -2.018965e-01
      -4.886547e-01
      -8.995403e-01
      -1.400939e+00
      -1.901202e+00
      -2.306080e+00
      -2.571567e+00
      -2.712655e+00
      -2.773464e+00
      -2.794740e+00
      ...
      8.171241e-14
      3.641532e-14
      7.727152e-14
      2.575717e-14
      7.727152e-14
      1.598721e-14
      3.730349e-14
      -2.664535e-15
      -7.105427e-15
      9.769963e-15

      14974
      -8.208500e-02
      -2.292202e-01
      -4.961012e-01
      -9.012018e-01
      -1.401242e+00
      -1.901247e+00
      -2.306085e+00
      -2.571568e+00
      -2.712655e+00
      -2.773464e+00
      ...
      2.930989e-14
      8.526513e-14
      2.575717e-14
      6.750156e-14
      1.687539e-14
      6.572520e-14
      4.440892e-15
      1.243450e-14
      1.065814e-14
      6.217249e-15

      14975
      -2.732372e-02
      -8.953158e-02
      -2.308818e-01
      -4.964048e-01
      -9.012472e-01
      -1.401248e+00
      -1.901248e+00
      -2.306085e+00
      -2.571568e+00
      -2.712655e+00
      ...
      9.592327e-14
      1.865175e-14
      7.549517e-14
      1.687539e-14
      5.595524e-14
      5.329071e-15
      4.085621e-14
      1.776357e-14
      2.575717e-14
      8.881784e-16

      14976
      -7.446583e-03
      -2.898528e-02
      -8.983512e-02
      -2.309272e-01
      -4.964103e-01
      -9.012478e-01
      -1.401248e+00
      -1.901248e+00
      -2.306085e+00
      -2.571568e+00
      ...
      4.263256e-14
      8.615331e-14
      9.769963e-15
      6.394885e-14
      5.329071e-15
      3.108624e-14
      1.865175e-14
      5.417888e-14
      7.993606e-15
      1.953993e-14

      14977
      -1.661557e-03
      -7.750122e-03
      -2.903068e-02
      -8.984068e-02
      -2.309278e-01
      -4.964104e-01
      -9.012478e-01
      -1.401248e+00
      -1.901248e+00
      -2.306085e+00
      ...
      1.110223e-13
      3.375078e-14
      7.460699e-14
      -1.776357e-15
      3.907985e-14
      1.865175e-14
      4.440892e-14
      8.881784e-15
      4.796163e-14
      7.105427e-15

      14978
      -3.035391e-04
      -1.706957e-03
      -7.755682e-03
      -2.903124e-02
      -8.984073e-02
      -2.309278e-01
      -4.964104e-01
      -9.012478e-01
      -1.401248e+00
      -1.901248e+00
      ...
      4.085621e-14
      9.947598e-14
      2.220446e-14
      4.973799e-14
      1.154632e-14
      5.240253e-14
      8.881784e-15
      3.819167e-14
      7.993606e-15
      2.842171e-14

      14979
      -4.539993e-05
      -3.090987e-04
      -1.707515e-03
      -7.755727e-03
      -2.903124e-02
      -8.984073e-02
      -2.309278e-01
      -4.964104e-01
      -9.012478e-01
      -1.401248e+00
      ...
      9.237056e-14
      2.930989e-14
      7.460699e-14
      3.552714e-14
      6.306067e-14
      1.776357e-15
      4.618528e-14
      7.993606e-15
      1.865175e-14
      8.881784e-16

      14980
      -5.559513e-06
      -4.595732e-05
      -3.091444e-04
      -1.707518e-03
      -7.755728e-03
      -2.903124e-02
      -8.984073e-02
      -2.309278e-01
      -4.964104e-01
      -9.012478e-01
      ...
      3.508305e-14
      6.750156e-14
      4.263256e-14
      8.792966e-14
      2.575717e-14
      5.684342e-14
      8.881784e-16
      2.664535e-14
      8.881784e-16
      -9.769963e-15

      14981
      -5.573903e-07
      -5.605267e-06
      -4.596039e-05
      -3.091446e-04
      -1.707518e-03
      -7.755728e-03
      -2.903124e-02
      -8.984073e-02
      -2.309278e-01
      -4.964104e-01
      ...
      8.260059e-14
      4.840572e-14
      8.082424e-14
      3.286260e-14
      8.171241e-14
      2.486900e-14
      3.730349e-14
      -6.217249e-15
      -1.776357e-15
      0.000000e+00

      14982
      -4.575325e-08
      -5.604652e-07
      -5.605436e-06
      -4.596040e-05
      -3.091446e-04
      -1.707518e-03
      -7.755728e-03
      -2.903124e-02
      -8.984073e-02
      -2.309278e-01
      ...
      4.396483e-14
      9.592327e-14
      3.863576e-14
      7.460699e-14
      3.197442e-14
      6.217249e-14
      1.776357e-14
      8.881784e-15
      -7.105427e-15
      7.993606e-15

      14983
      -3.074859e-09
      -4.592254e-08
      -5.604728e-07
      -5.605436e-06
      -4.596040e-05
      -3.091446e-04
      -1.707518e-03
      -7.755728e-03
      -2.903124e-02
      -8.984073e-02
      ...
      1.039169e-13
      3.419487e-14
      8.970602e-14
      3.774758e-14
      5.506706e-14
      2.486900e-14
      3.375078e-14
      1.687539e-14
      1.865175e-14
      -7.105427e-15

      14984
      -1.692841e-10
      -3.082492e-09
      -4.592277e-08
      -5.604728e-07
      -5.605436e-06
      -4.596040e-05
      -3.091446e-04
      -1.707518e-03
      -7.755728e-03
      -2.903124e-02
      ...
      4.973799e-14
      9.769963e-14
      3.330669e-14
      7.016610e-14
      3.064216e-14
      2.664535e-14
      2.398082e-14
      4.352074e-14
      1.687539e-14
      1.065814e-14

      14985
      -7.633005e-12
      -1.695213e-10
      -3.082483e-09
      -4.592288e-08
      -5.604728e-07
      -5.605436e-06
      -4.596040e-05
      -3.091446e-04
      -1.707518e-03
      -7.755728e-03
      ...
      1.190159e-13
      4.884981e-14
      7.815970e-14
      2.620126e-14
      4.174439e-14
      2.975398e-14
      3.641532e-14
      2.398082e-14
      3.552714e-14
      2.398082e-14

      14986
      -2.371436e-13
      -7.623235e-12
      -1.696243e-10
      -3.082472e-09
      -4.592268e-08
      -5.604728e-07
      -5.605436e-06
      -4.596040e-05
      -3.091446e-04
      -1.707518e-03
      ...
      3.153033e-14
      9.947598e-14
      4.174439e-14
      4.973799e-14
      2.531308e-14
      5.151435e-14
      2.975398e-14
      2.842171e-14
      3.108624e-14
      2.486900e-14

      14987
      9.769963e-15
      -3.401723e-13
      -7.612577e-12
      -1.694263e-10
      -3.082448e-09
      -4.592281e-08
      -5.604728e-07
      -5.605436e-06
      -4.596040e-05
      -3.091446e-04
      ...
      8.704149e-14
      2.442491e-14
      7.105427e-14
      4.085621e-14
      5.950795e-14
      2.531308e-14
      4.352074e-14
      3.685940e-14
      1.776357e-14
      7.105427e-15

      14988
      -1.030287e-13
      2.042810e-14
      -1.421085e-13
      -7.588596e-12
      -1.695533e-10
      -3.082419e-09
      -4.592272e-08
      -5.604728e-07
      -5.605436e-06
      -4.596040e-05
      ...
      4.218847e-14
      5.861978e-14
      2.353673e-14
      8.082424e-14
      4.085621e-14
      5.151435e-14
      3.241851e-14
      3.286260e-14
      1.287859e-14
      -7.105427e-15

      14989
      1.065814e-14
      9.503509e-14
      4.440892e-14
      -2.691181e-13
      -7.559287e-12
      -1.694644e-10
      -3.082406e-09
      -4.592275e-08
      -5.604728e-07
      -5.605436e-06
      ...
      6.661338e-14
      4.130030e-14
      6.838974e-14
      2.353673e-14
      7.283063e-14
      4.796163e-14
      4.085621e-14
      8.437695e-15
      7.993606e-15
      5.773160e-15

      14990
      1.980638e-13
      3.463896e-14
      -3.197442e-14
      7.371881e-14
      -1.803002e-13
      -7.546852e-12
      -1.694982e-10
      -3.082421e-09
      -4.592262e-08
      -5.604728e-07
      ...
      3.597123e-14
      7.638334e-14
      4.130030e-14
      6.039613e-14
      3.064216e-14
      6.217249e-14
      2.398082e-14
      1.598721e-14
      1.332268e-15
      1.509903e-14

      14991
      2.398082e-14
      7.105427e-14
      6.394885e-14
      5.684342e-14
      8.615331e-14
      -2.140510e-13
      -7.561063e-12
      -1.693623e-10
      -3.082425e-09
      -4.592266e-08
      ...
      9.237056e-14
      3.597123e-14
      6.838974e-14
      4.840572e-14
      4.973799e-14
      6.661338e-15
      3.730349e-14
      1.687539e-14
      2.309264e-14
      -4.440892e-15

      14992
      -1.270095e-13
      5.329071e-14
      1.598721e-13
      7.638334e-14
      2.309264e-14
      7.194245e-14
      -7.815970e-14
      -7.565504e-12
      -1.694112e-10
      -3.082429e-09
      ...
      5.018208e-14
      8.437695e-14
      4.307665e-14
      5.773160e-14
      2.442491e-14
      2.486900e-14
      -4.440892e-16
      4.440892e-14
      1.110223e-14
      7.993606e-15

      14993
      2.930989e-14
      -3.819167e-14
      6.572520e-14
      1.261213e-13
      6.217249e-14
      1.589839e-13
      6.750156e-14
      -1.270095e-13
      -7.569056e-12
      -1.692650e-10
      ...
      1.114664e-13
      5.728751e-14
      7.371881e-14
      1.909584e-14
      3.286260e-14
      1.731948e-14
      3.197442e-14
      -6.217249e-15
      2.930989e-14
      1.554312e-14

      14994
      8.881784e-14
      4.174439e-14
      -7.194245e-14
      5.151435e-14
      2.620126e-13
      5.773160e-14
      1.101341e-13
      6.394885e-14
      1.909584e-14
      -7.542411e-12
      ...
      6.705747e-14
      1.008083e-13
      3.330669e-14
      4.884981e-14
      1.199041e-14
      3.996803e-14
      1.154632e-14
      1.687539e-14
      -1.776357e-15
      2.131628e-14

      14995
      1.243450e-14
      5.506706e-14
      2.753353e-14
      6.394885e-14
      4.707346e-14
      2.131628e-13
      5.417888e-14
      2.562395e-13
      9.059420e-14
      -8.748557e-14
      ...
      9.237056e-14
      4.307665e-14
      7.593925e-14
      2.620126e-14
      5.595524e-14
      6.217249e-15
      2.486900e-14
      1.598721e-14
      8.881784e-15
      -1.731948e-14

      14996
      -3.375078e-14
      -1.776357e-15
      1.909584e-13
      2.309264e-14
      1.509903e-14
      4.352074e-14
      3.592682e-13
      8.082424e-14
      1.496581e-13
      1.065814e-13
      ...
      6.128431e-14
      6.750156e-14
      3.597123e-14
      8.304468e-14
      2.042810e-14
      4.085621e-14
      1.065814e-14
      1.687539e-14
      4.440892e-16
      -1.243450e-14

      14997
      -1.421085e-14
      1.021405e-13
      -6.217249e-15
      1.421085e-13
      1.953993e-14
      1.612044e-13
      7.016610e-14
      2.526868e-13
      9.681145e-14
      3.219647e-13
      ...
      7.238654e-14
      5.417888e-14
      7.460699e-14
      3.019807e-14
      6.794565e-14
      2.486900e-14
      3.286260e-14
      -4.884981e-15
      -4.440892e-15
      1.776357e-14

      14998
      1.358913e-13
      -1.865175e-14
      5.329071e-14
      -9.769963e-15
      2.882139e-13
      4.618528e-14
      5.462297e-14
      8.615331e-14
      4.249934e-13
      1.145750e-13
      ...
      4.130030e-14
      7.949197e-14
      4.840572e-14
      5.950795e-14
      3.463896e-14
      5.995204e-14
      9.325873e-15
      1.154632e-14
      1.243450e-14
      7.993606e-15

      14999
      -4.440892e-15
      8.704149e-14
      -2.220446e-14
      1.993961e-13
      1.687539e-14
      1.816325e-13
      6.217249e-14
      2.269296e-13
      1.039169e-13
      3.357314e-13
      ...
      9.725554e-14
      3.552714e-14
      6.439294e-14
      5.284662e-14
      5.151435e-14
      1.909584e-14
      3.863576e-14
      2.664535e-14
      2.398082e-14
      -5.329071e-15

      15000
      -4.884981e-14
      -7.993606e-15
      2.331468e-13
      4.440892e-15
      9.281464e-14
      3.286260e-14
      3.539391e-13
      7.993606e-14
      1.376677e-13
      1.074696e-13
      ...
      5.506706e-14
      8.215650e-14
      3.996803e-14
      5.639933e-14
      3.730349e-14
      3.019807e-14
      3.641532e-14
      5.107026e-14
      8.881784e-15
      1.598721e-14

15001 rows × 99 columns

```python
WavE.plot(title='1D Wave Equation')
```

## Heat Equation

\begin{equation}
\frac{\delta^2u}{\delta t^2} = \frac{\delta^2u}{\delta x^2} \ for \ 0\leq x \leq 1 \ and \ 0\leq t \leq 0.2 \\
u(0,t) = g_1(t) = 0 \ for \ 0\leq t \leq 0.2 \\
u(1,t) = g_2(t) = 0 \ for \ 0\leq t \leq 0.2 \\
u(x,0) = f(x) = 4x - 4x^2  \ \ \ for \ 0\leq x \leq 1 \\
where: \\
\Delta x = 0.2 \\
\Delta t = 0.02 \\
c^2\frac{\Delta t}{(\Delta x)^2} = 0.5
\end{equation}

```python
xmin, xmax, dx = 0, 1, 0.2
tmin, tmax, dt = 0, 0.2, 0.02
c = 1

I = lambda x: 4*x - 4*(x**2)

Heat = FiniteDiff(I, xmin, xmax, tmin, tmax, c, dx, dt, Heat = True)
Heat.do()
```

      0
      1
      2
      3

      0
      0.640000
      0.960000
      0.960000
      0.640000

      1
      0.480000
      0.800000
      0.800000
      0.480000

      2
      0.400000
      0.640000
      0.640000
      0.400000

      3
      0.320000
      0.520000
      0.520000
      0.320000

      4
      0.260000
      0.420000
      0.420000
      0.260000

      5
      0.210000
      0.340000
      0.340000
      0.210000

      6
      0.170000
      0.275000
      0.275000
      0.170000

      7
      0.137500
      0.222500
      0.222500
      0.137500

      8
      0.111250
      0.180000
      0.180000
      0.111250

      9
      0.090000
      0.145625
      0.145625
      0.090000

      10
      0.072813
      0.117812
      0.117813
      0.072812

```python
Heat.plot(title = '1D Heat Equation')
```


# M/M/s/$\infty$ queues

**In this lab you will learn:**

* How to convert M/M/S queuing equations into python functions
* How to analyse M/M/s queuing systems to inform health system improvement and design.

```python
from scipy import math
import numpy as np
import pandas as pd
```

An **M/M/s** system is a queuing process having Poisson arrival pattern, $s$ servers with $s$ i.i.d expeonential service times.  Service times do not depend on the state of the system.  The system (i.e. queue + service) has infinite capacity and a FIFO queue discipline.

#### Traffic Intensity
\begin{equation*}
\rho = \frac{\lambda}{s\mu}
\label{eq:rho} \tag{1}
\end{equation*}

#### Inference about the number of patients in the system

\begin{equation*}
P_0 = \left[ \sum_{n=0}^{s-1} \frac{\left(\lambda/ \mu \right)^n}{n!} + \frac{\left( \lambda / \mu \right)^s}{s!\left(1-\rho\right)}  \right]^{-1}
\label{eq:p0} \tag{2}
\end{equation*}

\begin{equation}
  P_n = \left\{
    \begin{array}{l}
      \dfrac{\left( \lambda / \mu \right)^n}{n!}p_0,   \>\>\>\>  n \leq s\\ \\
      \dfrac{\left( \lambda / \mu \right)^n}{s!s^{n-s}}p_0,  \>\>\>\> n > s
    \end{array}
  \right.
\label{eq:pn} \tag{3}
\end{equation}

#### Expected number of customers in the queue for service

\begin{equation}
L_q = \frac{p_0\left(\lambda / \mu \right)^s \rho}{s!\left(1 - \rho\right)}
\tag{4}
\end{equation}

#### Little's Formula

\begin{equation}
L_s = \lambda W_s  \\ L_q = \lambda W_q
\tag{5a, 5b}
\end{equation}

\begin{equation*}
    W_s = W_q + \dfrac{1}{\mu}
    \tag{6}
\end{equation*}

\begin{equation*}
    L_s = L_q + \dfrac{\lambda}{\mu}
    \tag{7}
\end{equation*}

# Hospital Pharmacy example

During the afternoon, a pharmacy based in a large hospital has 3 trained pharmacists on duty to check and fullfill patient prescriptions for drugs to take home with them at discharge. They are are able to handle 15 transactions per hour. The service times are exponentially distributed. During this busy period, prescriptions arrive at the pharmacy according to a Possion process, at a mean rate of 40 per hour.

**Questions**

1. What is the probability that there are more than 3 prescriptions in the pharmacy at any one time
2. Calculate the expected number of drug prescriptions waiting to be fulfilled
3. Calcluate the expected number of drug prescriptions in the system
4. Calculate the expected prescription turnaround time

### Example Solution:

This is a M/M/3 system with $\lambda=40$ and $\mu = 15$

#### Is the system in control?

Let's first check that steady state conditions hold by calculating the traffic intensity $\rho$.

\begin{equation}
\rho = \frac{\lambda}{s\mu}
\label{eq:rho} \tag{1}
\end{equation}

Steady state conditions hold if $\rho < 1$

```python
def traffic_intensity(_lambda, mu, s):
    '''
    calculate the traffic intensity (server utilisation)
    of an M/M/s queue
    '''
    return _lambda / (s * mu)
```

```python
#calculate traffic intensity
LAMBDA = 40
MU = 15
S = 3

rho = traffic_intensity(LAMBDA, S, MU)
rho
```

    0.8888888888888888

**Conclusion**: $\rho < 1$ steady state conditions will hold.

### 1. Calculate the probability that there are 3 drug orders in the pharmacy at any one time

Steady state probabilities are given by

\begin{equation*}
P_0 = \left[ \sum_{n=0}^{s-1} \frac{\left(\lambda/ \mu \right)^n}{n!} + \frac{\left( \lambda / \mu \right)^s}{s!\left(1-\rho\right)}  \right]^{-1}
\label{eq:p0} \tag{2}
\end{equation*}

\begin{equation}
  P_n = \left\{
    \begin{array}{l}
      \dfrac{\left( \lambda / \mu \right)^n}{n!}p_0,   \>\>\>\>  n \leq s\\ \\
      \dfrac{\left( \lambda / \mu \right)^n}{s!s^{n-s}}p_0,  \>\>\>\> n > s
    \end{array}
  \right.
\label{eq:pn} \tag{3}
\end{equation}

```python
def prob_system_empty(_lambda, mu, s):
    '''
    The probability that a M/M/s/infinity queue is empty
    '''
    p0 = 0.0
    rho = traffic_intensity(_lambda, mu, s)

    for n in range(s):
        p0 += ((_lambda / mu) ** n) / math.factorial(n)

    p0 += ((_lambda / mu) ** s) / (math.factorial(s) * (1 - rho))
    return p0**-1
```

```python
p0 = prob_system_empty(LAMBDA, MU, S)
print(f'p0 = {p0:.2f}')
```

    p0 = 0.03

```python
def prob_n_in_system(n, _lambda, mu, s, return_all_solutions=True):
    '''
    Calculate the probability that n customers
    in the system (queuing + service)

    Parameters:
    --------
    n: int,
        Number of customers in the system

    _lambda: float
        Mean arrival rate to system

    mu: float
        Mean service rate

    s: int
        number of servers

    return_all_solutions: bool, optional (default=True)
        Returns all solutions for 0,1 ... n

    Returns:
    ------
        np.ndarray of solutions

    '''
    p0 = prob_system_empty(_lambda, mu, s)
    probs = [p0]

    #for n <= s
    for i in range(1, min(s+1, n+1)):
        pn = (((_lambda / mu)**i) / math.factorial(i)) * p0
        probs.append(pn)

    #for n > s
    for i in range(s+1, n+1):
        pn = (((_lambda / mu)**i) / (math.factorial(s) * (s**(n-s)))) * p0
        probs.append(pn)

    if return_all_solutions:
        return np.array(probs)
    else:
        return probs[:-1]
```

```python
prob = prob_n_in_system(3, LAMBDA, MU, S)

#returns: [p0, p1, p2, p3] => probabilities of 3 or less drug orders
prob.sum()
```

    0.29110418830045015

```python
#prob.sum() => p(X <=3)
more_than_three = 1 - prob.sum()
print(f'P(X > 3) = {more_than_three:.2f}')
```

    P(X > 3) = 0.71

### 2. Expected number of drug prescriptions waiting to be fullfilled

$L_q$ = Expected number of customers in the queue for service

\begin{equation}
L_q = \frac{p_0\left(\lambda / \mu \right)^s \rho}{s!\left(1 - \rho\right)^2}
\tag{4}
\end{equation}

```python
def mean_queue_length(_lambda, mu, s):
    '''
    Mean length of queue Lq
    '''
    p0 = prob_system_empty(_lambda, mu, s)
    rho = traffic_intensity(_lambda, mu, s)

    lq = (p0 * ((_lambda / mu)**s) * rho) / (math.factorial(s) * (1 - rho)**2)
    return lq
```

```python
lq = mean_queue_length(LAMBDA, MU, S)
```

```python
print(f'lq = {lq:.2f}')
```

    lq = 6.38

### 3. Expected number of drug prescriptions in the system

$L_s$ = Expected number of customers in the queue

We have already calculated $L_q$ therefore we will use

\begin{equation}
L_s = L_q + \frac{\lambda}{\mu}
\tag{5}
\end{equation}

```python
ls = lq + (LAMBDA / MU)
```

```python
print(f'Ls = {ls:.2f}')
```

    Ls = 9.05

### 4. Expected prescription turnaround time

Using:

\begin{equation}
L_s = \lambda W_s
\tag{5}
\end{equation}

\begin{equation}
\frac{L_s}{\lambda} = W_s
\end{equation}

```python
ws = ls / LAMBDA
```

```python
print(f'Ws = {ws:.2f}')
```

    Ws = 0.23

## MMsQueue Class

A somewhat cleaner way of analytic modelling of queues is to implement a class.  An example implementation is below.

```python
class MMsQueue(object):
    '''
    M/M/S/inf/inf/FIFO system
    '''
    def __init__(self, _lambda, mu, s):
        '''
        Constructor

        Parameters:
        -------
        _lambda: float
            The arrival rate of customers to the facility

        mu: float
            The service rate of the facility

        s: int
            The number of servers
        '''
        self._lambda = _lambda
        self.mu = mu
        self.s = s
        self.rho = self._get_traffic_intensity()

        #create a dict of performance metrics
        #solve for L_q then use little's law to calculate remaining KPIs
        self.metrics = {}
        self.metrics[r'$\rho$'] = self.rho
        self.metrics[r'$L_q$'] = self._get_mean_queue_length()
        self.metrics[r'$L_s$'] = self.metrics[r'$L_q$'] + (_lambda / mu)
        self.metrics[r'$W_s$'] = self.metrics[r'$L_s$'] / _lambda
        self.metrics[r'$W_q$'] = self.metrics[r'$W_s$'] - (1 / mu)

    def _get_traffic_intensity(self):
        '''
        calculate the traffic intensity (server utilisation)
        of an M/M/s queue
        '''
        return self._lambda / (self.s * self.mu)

    def _get_mean_queue_length(self):
        '''
        Mean length of queue Lq
        '''
        p0 = self.prob_system_empty()

        lq = (p0 * ((self._lambda / self.mu)**self.s) *
              self.rho) / (math.factorial(self.s) * (1 - self.rho)**2)
        return lq

    def prob_system_empty(self):
        '''
        The probability that a M/M/s/infinity queue is empty
        '''
        p0 = 0.0

        for n in range(self.s):
            p0 += ((self._lambda / self.mu) ** n) / math.factorial(n)

        p0 += ((self._lambda / self.mu) ** self.s) / (math.factorial(self.s)
                                                      * (1 - self.rho))
        return p0**-1

    def prob_n_in_system(self, n, return_all_solutions=True, as_frame=True):
        '''
        Calculate the probability that n customers
        in the system (queuing + service)

        Parameters:
        --------
        n: int,
            Number of customers in the system

        return_all_solutions: bool, optional (default=True)
            Returns all solutions for 0,1 ... n

        as_frame: bool, optional (default=True)
            If True, returns all solutions in a pd.DataFrame
            else returns all solutions as np.ndarray
            has no effect is return_all_solutions == False

        Returns:
        ------
            np.ndarray of solutions

        '''
        p0 = self.prob_system_empty()
        probs = [p0]

        #for n <= s
        for i in range(1, min(self.s+1, n+1)):
            pn = (((self._lambda / self.mu)**i) / math.factorial(i)) * p0
            probs.append(pn)

        #for n > s
        for i in range(self.s+1, n+1):
            pn = (((self._lambda / self.mu)**i) / (math.factorial(self.s)
                                                   * (self.s**(n-self.s)))) * p0
            probs.append(pn)

        if return_all_solutions:
            results = np.array(probs)
            if as_frame:
                return pd.DataFrame(results, columns=['P(X=n)'])
            else:
                return results
        else:
            return probs[:-1]

    def summary_frame(self):
        '''
        Return performance metrics

        Returns:
        ---------
        pd.DataFrame
        '''
        df = pd.Series(self.metrics).to_frame()
        df.columns = ['performance']
        return df
```

```python
model = MMsQueue(LAMBDA, MU, S)
model.summary_frame()
```

      performance

      $\rho$
      0.888889

      $L_q$
      6.380062

      $L_s$
      9.046729

      $W_s$
      0.226168

      $W_q$
      0.159502

```python
model.prob_n_in_system(5)
```

      P(X=n)

      0
      0.028037

      1
      0.074766

      2
      0.099688

      3
      0.088612

      4
      0.026255

      5
      0.070014

```python
#county hospital example
model = MMsQueue(2, 3, 2)
model.summary_frame()
```

      performance

      $\rho$
      0.333333

      $L_q$
      0.083333

      $L_s$
      0.750000

      $W_s$
      0.375000

      $W_q$
      0.041667

```python
model.prob_n_in_system(2)
```

      P(X=n)

      0
      0.500000

      1
      0.333333

      2
      0.111111


# 수학 기초

## Day 2 미분

```python
import sympy as sp
```

```python
x, a, c, h, n = sp.symbols('x a c h n')
```

```python
fx = (x**2 - x - 6) / (x - 3)
```

```python
sp.limit(fx, x, 3)
```

$\displaystyle 5$

```python
sp.limit(fx, x, 3, '+')
```

$\displaystyle 5$

```python
sp.limit(fx, x, 3, '-')
```

$\displaystyle 5$

```python
from sympy import Derivative
```

```python
fx = x**2
```

```python
Derivative(fx, x)
```

$\displaystyle \frac{d}{d x} x^{2}$

```python
Derivative(fx, x).doit()
```

$\displaystyle 2 x$

```python
Derivative(fx, x).doit().subs({x: 2})
```

$\displaystyle 4$

```python
fxr = -x + 2
fxl = x**2 - x + 2
```

```python
fx0 = fxl.subs({x: 0})
```

```python
fx0
```

$\displaystyle 2$

```python
sp.limit(fxr, x, 0, '+')
```

$\displaystyle 2$

```python
sp.limit(fxl, x, 0, '-')
```

$\displaystyle 2$

```python
sp.limit( (fxr - fx0) / (x-0), x, 0, '+')
```

$\displaystyle -1$

```python
sp.limit( (fxr - fx0) / (x-0), x, 0, '-')
```

$\displaystyle -1$

```python
fx = c
fxh = fx.subs({x: x+h})
sp.limit( (fxh - fx) / h, h, 0)
```

$\displaystyle 0$

```python
Derivative(fx, x).doit()
```

$\displaystyle 0$

```python
fx = x**3
fxh = fx.subs({x: x+h})
sp.limit( (fxh - fx) / h, h, 0)
```

$\displaystyle 3 x^{2}$

```python
Derivative(fx, x).doit()
```

$\displaystyle 3 x^{2}$

```python
fx = sp.log(x)
fxh = fx.subs({x: x+h})
sp.limit( (fxh - fx) / h, h, 0)
```

$\displaystyle \frac{1}{x}$

```python
Derivative(fx, x).doit()
```

$\displaystyle \frac{1}{x}$

```python
fx = sp.log(x, 10)
fxh = fx.subs({x: x+h})
sp.limit( (fxh - fx) / h, h, 0)
```

$\displaystyle \frac{1}{x \log{\left(10 \right)}}$

```python
Derivative(fx, x).doit()
```

$\displaystyle \frac{1}{x \log{\left(10 \right)}}$

```python
fx = sp.exp(x)
fxh = fx.subs({x: x+h})
sp.limit( (fxh - fx) / h, h, 0)
```

$\displaystyle e^{x}$

```python
Derivative(fx, x).doit()
```

$\displaystyle e^{x}$

```python
fx = a**x
fxh = fx.subs({x: x+h})
sp.limit( (fxh - fx) / h, h, 0)
```

$\displaystyle a^{x} \log{\left(a \right)}$

```python
Derivative(fx, x).doit()
```

$\displaystyle a^{x} \log{\left(a \right)}$

```python

```

```python

```

```python

```

```python

```

```python

```


# Gradient descent demonstration

In this notebook I am going to demonstrate how the gradient descent optimization algorithm works.

I will use a linear regression as an example model.

```python
import numpy as np
import pandas as pd
import seaborn as sns
from matplotlib import pyplot as plt
from sklearn.metrics import mean_squared_error
```

First, let's generate some data for our model with a linear function.

```python
sns.set()
N = 100

def f(x):
    return 3*x + 2 + 10 * x * np.random.random()

# generating the dataset
X = np.array([x for x in range(N)])
y = np.array([f(x) for x in X])

plt.scatter(X, y, cmap='Dark2', marker='x', alpha=.7)
plt.show()
```

After generating the model, we can start to think about gradient descent.

The goal of linear regression is to find a ```slope``` and an ```intercept``` which satisfies the equation:

\begin{align}
\textbf{Y} = a\textbf{X} + b
\end{align}

Where ```a``` is the slope, ```b``` is the intercept as real numbers, ```Y``` is the target, and ```X``` is the input vector or matrix.

Since satisfying this equation is impossible because of the irreducible error, the linear regression tries to find ```a``` and ```b``` such that the ```MSE = mean squared error```, or in this case the loss function is minimized.

\begin{align}
\textbf{MSE} = \frac{1}{N}\sum_{i=1}^N{(y_i - \hat{y})^2}
\end{align}

We can substitute in the linear equation for the `y_hat` prediction:

\begin{align}
\textbf{MSE} = \frac{1}{N}\sum_{i=1}^N{(y_i - (a\textbf{X} + b))^2}
\end{align}

Since we want to minimize the function we should find it's global minimum, therefore I am going to differentiate the `MSE` function in order to know the slope of the function in a given `X` point.

There are two variables in MSE so I will get it's gradient, or the function's all partial derivative.

\begin{align}
\frac{\partial E}{\partial X} = \frac{\partial MSE}{\partial X} = -2a(y_i - a\textbf{X} - b)
\end{align}

\begin{align}
\frac{\partial E}{\partial y} = \frac{\partial MSE}{\partial y} = 2(y_i - a\textbf{X} - b)
\end{align}

So, we will do these equations for every data point and then get the mean of the derived functions, this is the value we will use to update the `a` and `b` values.

Since the derivative shows the slope of the function in a point we should get it's inverse in order to go towards the linear `f` function's minima.

The gradient give really big numbers, so we will need a number called alpha to regularize it.

\begin{align}
\Delta{a} = -\alpha\frac{\partial E}{\partial X}\frac{1}{N}
\end{align}

\begin{align}
\Delta{b} = -\alpha\frac{\partial E}{\partial y}\frac{1}{N}
\end{align}

Where

\begin{align}
\newcommand\Inn{%
  \mathrel{\ooalign{$\subset$\cr\hfil\scalebox{0.8}[1]{$=$}\hfil\cr}}%
}
\alpha \in [-1;1]
\end{align}

We do this for a given number of iterations called `epoch` and then try to visualize the results.

```python
epochs = 1000  # number of iterations
alpha = 0.0005  # a number to regularize the gradient

a, b = 2*np.random.random()-1, 2*np.random.random()-1
N = len(X)

for epoch in range(epochs):
    dE_dX = 0
    dE_dy = 0

    for i in range(N):
        dE_dX += -2*a*(y[i] - a*X[i] - b)
        dE_dy += 2*(y[i] - a*X[i] - b)

    a += -alpha*dE_dX / N
    b += -alpha*dE_dy / N

print('a = ' + str(a))
print('b = ' + str(b))
```

    a = 8.708114029129074
    b = -6.199104122237881

```python
plt.scatter(X, y, cmap='Dark2', marker='x', alpha=.7)
plt.plot(X, a*X + b, color='red', alpha=.9)
plt.title('Linear regression with custrom gradent descent optimization')
plt.show()
```

```python

```


## Multi-Armed Bandits

Suppose you have $N$ slot machines (a multi-armed bandit) with each bandit having an unkwown probability of distributing a prize. Assuming the prizes are the same for each bandit and only their probabilities of winning are different, our goal is to develop a strategy to maximize our winnings with fewest trials. If we knew which of the bandits results in the highest probability of winning, we would exercise that bandit all the time. However, a sub-optimal bandit can generate a winning sequence purely by chance and even if we found a good enough bandit, do we keep using it or explore in the hope of finding an even higher probability of winning.

A Bayesian solution to the multi-armed bandit problem involves updating our prior belief on the probability of success for each bandit based on the observed draws. A beta-binomial model provides a conjugate likelihood and prior with closed form posterior updates. Thus, we can assume an uninformative $Beta(1,1)$ prior for each of the $N$ bandits. Thus, our bayesian bandits algorithm can be summarized as follows:

1. Draw a sample $X_b$ from the prior of bandit $b$ for all $b$
2. Select the bandit with the largest sample: $B = \arg \max X_b$
3. Observe the result of pulling banding $B$ and update your prior on bandit $B$
4. Repeat

This algorithm suggests that we shouldn't discard low success probability bandits but instead that we should pick them at a decreasing rate as we gather more confidence that there exist better bandits.

```
%matplotlib inline
import numpy as np
import pandas as pd
import scipy.stats as stats

import seaborn as sns
import matplotlib.pyplot as plt

```

```
class Bandits(object):
    """
    This class represents N bandits machines.

    parameters:
        p_array: a (n,) Numpy array of probabilities >0, <1.

    methods:
        pull( i ): return the results, 0 or 1, of pulling
                   the ith bandit.
    """
    def __init__(self, p_array):
        self.p = p_array
        self.optimal = np.argmax(p_array)

    def pull(self, i):
        #i is which arm to pull
        return np.random.rand() < self.p[i]

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

```

```
class BayesianStrategy(object):
    """
    Implements a online, learning strategy to solve
    the Multi-Armed Bandit problem.

    parameters:
        bandits: a Bandit class with .pull method

    methods:
        sample_bandits(n): sample and train on n pulls.

    attributes:
        N: the cumulative number of samples
        choices: the historical choices as a (N,) array
        bb_score: the historical score as a (N,) array
    """

    def __init__(self, bandits):

        self.bandits = bandits
        n_bandits = len(self.bandits)
        self.wins = np.zeros(n_bandits)
        self.trials = np.zeros(n_bandits)
        self.N = 0
        self.choices = []
        self.bb_score = []

    def sample_bandits(self, n=1):

        bb_score = np.zeros(n)
        choices = np.zeros(n)

        for k in range(n):
            #sample from the bandits's priors, and select the largest sample
            choice = np.argmax(np.random.beta(1 + self.wins, 1 + self.trials - self.wins))

            #sample the chosen bandit
            result = self.bandits.pull(choice)

            #update priors and score
            self.wins[choice] += result
            self.trials[choice] += 1
            bb_score[k] = result
            self.N += 1
            choices[k] = choice

        self.bb_score = np.r_[self.bb_score, bb_score]
        self.choices = np.r_[self.choices, choices]
        return

```

```
def plot_priors(bayesian_strategy, prob, lw = 3, alpha = 0.2, plt_vlines = True):
    ## plotting function
    wins = bayesian_strategy.wins
    trials = bayesian_strategy.trials
    for i in range(prob.shape[0]):
        y = beta(1+wins[i], 1 + trials[i] - wins[i])
        p = plt.plot(x, y.pdf(x), lw = lw)
        c = p[0].get_markeredgecolor()
        plt.fill_between(x,y.pdf(x),0, color = c, alpha = alpha,
                         label="underlying probability: %.2f" % prob[i])
        if plt_vlines:
            plt.vlines(prob[i], 0, y.pdf(prob[i]) ,
                       colors = c, linestyles = "--", lw = 2)
        plt.autoscale(tight = "True")
        plt.title("Posteriors After %d pull" % bayesian_strategy.N +\
                    "s"*(bayesian_strategy.N > 1))
        plt.autoscale(tight=True)
    return

```

```
def regret(probabilities, choices):
    w_opt = probabilities.max()
    return (w_opt - probabilities[choices.astype(int)]).cumsum()

```

```
rand = np.random.rand
beta = stats.beta
x = np.linspace(0.001,.999,200)
```

```
#bandits success probability
hidden_prob = np.array([0.85, 0.60, 0.75])

#instantiate multi-armed bandit class
bandits = Bandits(hidden_prob)
bayesian_strat = BayesianStrategy(bandits)
```

```
#generate plots
draw_samples = [1, 1, 3, 10, 10, 25]
for j,i in enumerate(draw_samples):
    plt.subplot(3, 2, j+1)
    bayesian_strat.sample_bandits(i)
    plot_priors(bayesian_strat, hidden_prob)
    #plt.legend()
    plt.autoscale(tight = True)
plt.tight_layout()
plt.show()
```

In the above figure we are showing the posterior distributions for a mutli-armed bandit with $N=3$ arms for multiple draws with hidden winning probability marked by the dashed lines. We can see that all three bandits were exercised with red and blue giving the highest returns. The mode of the posterior distribution shows that green and red bandits produced returns higher then their hidden probability purely by chance. Nonetheless the blue bandit was chosen the greatest number of time leading to the highest reward probability.

```
regret_bound = regret(hidden_prob, bayesian_strat.choices)

plt.figure()
plt.plot(regret_bound, lw=3)
plt.xlabel('number of trials')
plt.ylabel('regret')
plt.show()
```

We can measure the performance of our on-line algorithm using a regret bound as shown in the figure above. The best we can do is to always pick the bandit with the largest probability of winning ($w_opt$). We can make our score relevant to the optimum bandit probability:
\begin{equation}
    R_T = \sum_{t=1}^{T} (w_{opt} - w_{B(i)})
\end{equation}
where $w_{B(i)}$ is the probability of winning for a chosen bandit in $i$-th round. A regret of zero means the strategy is matching the best possible score. Ideally, our strategy should flatten as it learns the best bandit.


# Lecture 13

## Linear Algebra II:

### Eigenvalues and Eigenvectors

```python
import numpy as np
import sympy as sp
import scipy.integrate
sp.init_printing()
##################################################
##### Matplotlib boilerplate for consistency #####
##################################################
from ipywidgets import interact
from ipywidgets import FloatSlider
from matplotlib import pyplot as plt

%matplotlib inline

from IPython.display import set_matplotlib_formats
set_matplotlib_formats('svg')

global_fig_width = 10
global_fig_height = global_fig_width / 1.61803399
font_size = 12

plt.rcParams['axes.axisbelow'] = True
plt.rcParams['axes.edgecolor'] = '0.8'
plt.rcParams['axes.grid'] = True
plt.rcParams['axes.labelpad'] = 8
plt.rcParams['axes.linewidth'] = 2
plt.rcParams['axes.titlepad'] = 16.0
plt.rcParams['axes.titlesize'] = font_size * 1.4
plt.rcParams['figure.figsize'] = (global_fig_width, global_fig_height)
plt.rcParams['font.sans-serif'] = ['Computer Modern Sans Serif', 'DejaVu Sans', 'sans-serif']
plt.rcParams['font.size'] = font_size
plt.rcParams['grid.color'] = '0.8'
plt.rcParams['grid.linestyle'] = 'dashed'
plt.rcParams['grid.linewidth'] = 2
plt.rcParams['lines.dash_capstyle'] = 'round'
plt.rcParams['lines.dashed_pattern'] = [1, 4]
plt.rcParams['xtick.labelsize'] = font_size
plt.rcParams['xtick.major.pad'] = 4
plt.rcParams['xtick.major.size'] = 0
plt.rcParams['ytick.labelsize'] = font_size
plt.rcParams['ytick.major.pad'] = 4
plt.rcParams['ytick.major.size'] = 0
##################################################
```

## Recap from Part I

- Matrix representation of simultaneous equations
- Matrix-vector multiplication
- Matrix-matrix multiplication
- Determinant ($2\times2$ matrices)
- Inverting matrices
- Transpose
- Python

## Using Python to calculate the inverse
To find $A^{-1}$ for
$$A = \left(\begin{matrix}3&0&2\\ 3&0&-3\\ 0&1&1\end{matrix}\right).$$

```python
A = np.array([[3, 0, 2], [3, 0, -3], [0, 1, 1]])
np.linalg.inv(A)

```

```python
A = sp.Matrix([[3, 0, 2], [3, 0, -3], [0, 1, 1]])
A.inv()

```

## It doesn't always work

Consider the following system

$$\begin{array}{cccccccc}
eq1: &  x & + & y & + & z & =  & a \\
eq2: & 2x & + & 5y & + &  2z &  = & b \\
eq3: & 7x & +&  10y  & + & 7z &  = &  c
\end{array} $$

```python
A = np.array([[1, 1, 1], [2, 5, 2], [7, 10, 7]])
np.linalg.inv(A)

```

## Singular matrices

The **rank** of an $\;n\,\times\,n\;$ matrix $\;A\;$ is the number of linearly independent rows in $\;A\;$ (rows not combinations of other rows).

When $\;\mbox{rank}(A) < n\;$ then

- The system $\;A\textbf{x} = \textbf{b}\;$ has *fewer* equations than unknowns
- The matrix is said to be singular
- $A\;$ has no inverse
- The determinant of $\;A\;$ is 0
- The equation $\;A\textbf{u} = \textbf{0}\;$ has non-trivial solutions (solutions where $\textbf{u} \neq \textbf{0}$)

## Singular matrix example

An underdetermined system (fewer equations than unknowns) may mean that there are **many solutions** or that there are **no solutions**.

An example  with many solutions is
\begin{eqnarray*} x+y &=& 1,\\ 2x+2y &=& 2,\end{eqnarray*}

has infinitely many solutions (x=0, y=1; x=-89.3, y=90.3...)

An example with no solutions is

\begin{eqnarray*} x+y &=& 1,\\ 2x+2y &=& 0,\end{eqnarray*}

where the second equation is inconsistent with the first.

 These examples use the matrix
$$ \left(\begin{matrix}1&1\\ 2&2\end{matrix}\right),$$
 which has rank 1.

## Null space
When a matrix is singular we can find non-trivial solutions to $\;A\textbf{u} = \textbf{0}$.

These are vectors which form the **null space** for $\;A$.

Any vector in the null space makes no difference to the effect that $A$ is having:

$$
 A(\textbf{x} + \textbf{u}) =  A\textbf{x} + A\textbf{u} =
 A\textbf{x} + \textbf{0} =   A\textbf{x}.
$$

Note that any combination or scaling of vectors in the null space is also in the null space.
That is, if $\;A\textbf{u} = \textbf{0}\;$ and $\;A\textbf{v} = \textbf{0}\;$ then

$$ A(\alpha\textbf{u} + \beta\textbf{v}) = \textbf{0} $$

The number of linearly independent vectors in the null space is denoted $~\mbox{null}(A)~$ and

$$\mbox{null}(A)  + \mbox{rank}(A) = \mbox{order}(A).$$

## Null space example

Previous example of a singular system:
$$A = \left(\begin{matrix} 1& 1& 1\\ 2& 5& 2\\ 7& 10&7 \end{matrix}\right)$$

```python
A = np.array([[1, 1, 1], [2, 5, 2], [7, 10, 7]])
np.linalg.matrix_rank(A)

```

```python
import scipy.linalg
scipy.linalg.null_space(A)

```

remember, scaled vectors in the null space are also in the null space, for example, $\;x=1, y=0, z=-1\;$ is in the null space.

Try it:

 $\left(\begin{matrix} 1& 1& 1\\ 2& 5& 2\\ 7& 11&7 \end{matrix}\right)
  \left(\begin{matrix} -1000\\ 0 \\ 1000 \end{matrix}\right) = \quad ?$

```python
np.matmul(A,np.array([-1000,0,1000]))

```

## Eigenvectors: motivation

The **eigenvalues** and **eigenvectors** give an indication of how much effect the matrix has, and in what direction.

$$A=\left(\begin{matrix} \cos(45)&-\sin(45)\\ \sin(45)&\cos(45)\\\end{matrix}\right)\qquad\mbox{has no scaling effect.}$$

$$B=\left(\begin{matrix} 2& 0 \\ 0&\frac{1}{2}\\\end{matrix}\right)\qquad\qquad\mbox{doubles in }x\mbox{, but halves in }y\mbox{.}$$

Repeated applications of $\;A\;$ stay the same distance from the origin, but repeated applications of $\;B\;$ move towards $\;(\infty, 0).$

- Transitions with probability
- Markov chains
- Designing bridges
- Solution of systems of linear ODEs
- Stability of systems of nonlinear ODEs

## Eigenvectors and Eigenvalues

$A\;$ is a matrix, $\;\textbf{v}\;$ is a **non-zero** vector,
$\;\lambda\;$ is a scalar,

If

$$A \textbf{v} = \lambda \textbf{v}$$

then $\;\textbf{v}\;$ is called an **eigenvector** and $\;\lambda\;$ is the corresponding **eigenvalue**.

Note that if $\;\textbf{v}\;$ is a solution, then so is a scaling $\;a\textbf{v}$:

$$A (a \textbf{v}) = \lambda (a \textbf{v}).$$

## Finding Eigenvalues
Another way to write previous equation:

\begin{eqnarray*}
A \textbf{v} &=& \lambda \textbf{v},\\
A \textbf{v} -  \lambda I \textbf{v}&=& \textbf{0},\\
(A  -  \lambda I) \textbf{v}&=& \textbf{0}.
\end{eqnarray*}

Consider the equation:
$$B\textbf{x}=\textbf{0}$$

where $\;B\;$ is a matrix and $\;\textbf{x}\;$ is a non-zero column vector.

Assume $\;B\;$ is nonsingular:
$$B^{-1}(B\textbf{x})=B^{-1}\textbf{0}=\textbf{0}$$

But:
$$B^{-1}(B\textbf{x})=(B^{-1}B)\textbf{x}=I\textbf{x}=\textbf{x}$$

This means that:
$$B^{-1}(B\textbf{x})=\textbf{0}=\textbf{x}$$

but we stated above that $\;\textbf{x}\neq\textbf{0}\;$ so our assumption that $\;B\;$ is nonsingular must be false: $\;B\;$ is singular.

We can state that:

$$(A-\lambda I)\textbf{v} = \textbf{0}  \quad \mbox{with} \quad \textbf{v}\neq \textbf{0},$$

so $(A-\lambda I)$ must be singular:

$$|A-\lambda I|=0.$$

## Example
What are the eigenvalues for this matrix?

$$A=\left(\begin{matrix}-2&-2\\ 1&-5\\\end{matrix}\right)$$

$$|A-\lambda I|=\left\vert\begin{matrix}-2-\lambda&-2\\ 1&-5-\lambda\end{matrix}\right\vert=(-2-\lambda)(-5-\lambda)-(-2)$$

$$=10+5\lambda+\lambda^2+2\lambda+2=\lambda^2+7\lambda+12=(\lambda+3)(\lambda+4)=0$$

So the eigenvalues are $\lambda_1=-3$ and $\lambda_2=-4$.

## Eigenvalues using Python

Numpy:

```python
A = np.array([[-2, -2], [1, -5]])
np.linalg.eig(A)[0]

```

SymPy:

```python
A2 = sp.Matrix([[-2, -2], [1, -5]])
A2.eigenvals()

```

## Finding Eigenvectors

For an eigenvalue, the corresponding vector comes from substitution into $\;A \textbf{v} = \lambda \textbf{v}$:

### Example
What are the eigenvectors for

$$A=\left(\begin{matrix}-2&-2\\ 1&-5\\\end{matrix}\right)?$$

The eigenvalues are $\;\lambda_1=-3\;$ and $\;\lambda_2=-4.\;$
The eigenvectors are $\;\textbf{v}_1\;$ and $\;\textbf{v}_2\;$ where:

$$A\textbf{v}_1=\lambda_1 \textbf{v}_1.$$

$$\begin{eqnarray*}
\left(\begin{matrix}-2&-2\\ 1&-5\\\end{matrix}\right)
\left(\begin{matrix}u_1\\ v_1\\\end{matrix}\right)
&=&
\left(\begin{matrix}-3u_1\\ -3v_1\\\end{matrix}\right),\\
u_1 &=& 2v_1.  \mbox{  (from the top or bottom equation)}\\
\left(\begin{matrix}u_1\\ v_1\\\end{matrix}\right)
&=&
\left(\begin{matrix}2 \\ 1\\\end{matrix}\right),
\left(\begin{matrix}1 \\ 0.5\\\end{matrix}\right),
\left(\begin{matrix}-4.4 \\ -2.2\\\end{matrix}\right),
\left(\begin{matrix}2\alpha \\ \alpha\\\end{matrix}\right)\ldots
\end{eqnarray*}$$

## Eigenvectors in Python

Numpy:

```python
A = np.array([[-2, -2], [1, -5]])
np.linalg.eig(A)[1]

```

SymPy:

```python
c = sp.symbols('c')
A2 = sp.Matrix([[-2, c], [1, -5]])
A2.eigenvects()

```

## Diagonalising matrices

Any nonsingular matrix $A$ can be rewritten as a product of eigenvectors and eigenvalues.

If $\;A\;$ has eigenvalues $\;\lambda_1\;$ and $\;\lambda_2\;$ with corresponding eigenvectors  $\;\left(\begin{matrix}u_1\\ v_1\\\end{matrix}\right)\;$ and
$\left(\begin{matrix}u_2\\ v_2\\\end{matrix}\right)$ then

$$\begin{eqnarray}
A =
\left(\begin{matrix}u_1 & u_2\\v_1 & v_2\\\end{matrix}\right)
\left(\begin{matrix}\lambda_1 & 0\\0 & \lambda_2\\\end{matrix}\right)
\left(\begin{matrix}u_1 & u_2\\v_1 & v_2\\\end{matrix}\right)^{-1}. \label{eq:diag}
\end{eqnarray}$$

This is like a scaling surrounded by rotations and separates how much effect the matrix has $\;(\lambda_i)\;$ from the directions $\;(\textbf{v}_i).$

## For example

$$A=\left(\begin{matrix}-2&-2\\ 1&-5\\\end{matrix}\right)$$

```python
A = np.array([[-2, -2], [1, -5]])
w, v = np.linalg.eig(A)
inv_v = np.linalg.inv(v)

np.matmul( np.matmul(v, np.diag(w)) , inv_v )

```

## Orthogonal eigenvectors
If $\;\textbf{x}_1\;$ and $\;\textbf{x}_2\;$ are perpendicular or **orthogonal** vectors, then the scalar/dot product is zero.

$$\textbf{x}_1.\textbf{x}_2=0$$

e.g.

$$\textbf{x}_1.\textbf{x}_2=\left(\begin{matrix}2\\ -1\\ 3\end{matrix}\right).\left(\begin{matrix}2\\ 7\\ 1\end{matrix}\right)=2\times 2+(-1\times 7)+(3\times1)=4-7+3=0.$$

Since this dot product is zero, the vectors $\textbf{x}_1$ and $\textbf{x}_2$ are orthogonal.

## Symmetric matricies

Symmetric matricies have orthogonal eigenvectors, e.g.

$$A=\left(\begin{matrix}19&20&-16\\ 20&13&4 \\ -16&4&31\\\end{matrix}\right)$$

```python
A = np.array([[19, 20, -16], [20, 13, 4], [-16, 4, 31]])
w, v = np.linalg.eig(A)
print(v)
print('\ndot products of eigenvectors:\n')
print(np.dot(v[:,0],v[:,1]))
print(np.dot(v[:,0],v[:,2]))
print(np.dot(v[:,1],v[:,2]))

```

## Normalised eigenvectors
If

$$\left(\begin{matrix}x\\ y\\ z\\\end{matrix}\right),$$

is an eigenvector, then

$$\left(\begin{matrix}\frac{x}{\sqrt{x^2+y^2+z^2}}\\ \frac{y}{\sqrt{x^2+y^2+z^2} }\\ \frac{z}{\sqrt{x^2+y^2+z^2} }\end{matrix}\right)$$

is the corresponding normalised vector: a vector of unit length (magnitude).

## Orthogonal matrices
A matrix is orthogonal if its columns are normalised orthogonal vectors.
One can prove that if $\;M\;$ is orthogonal then:

  $$M^TM=I\qquad M^T=M^{-1}$$

Note that if the eigenvectors are written in orthogonal form then the diagonalising equation
is simplified:

\begin{eqnarray*}
A
&=&
\left(\begin{matrix}u_1 & u_2\\v_1 & v_2\\\end{matrix}\right)
\left(\begin{matrix}\lambda_1 & 0\\0 & \lambda_2\\\end{matrix}\right)
\left(\begin{matrix}u_1 & u_2\\v_1 & v_2\\\end{matrix}\right)^{-1}
\\
&=&
\left(\begin{matrix}u_1 & u_2\\v_1 & v_2\\\end{matrix}\right)
\left(\begin{matrix}\lambda_1 & 0\\0 & \lambda_2\\\end{matrix}\right)
\left(\begin{matrix}u_1 & v_1\\u_2 & v_2\\\end{matrix}\right).
\end{eqnarray*}

## Summary

- Matrix representation of simultaneous equations
- Matrix-vector and matrix-matrix multiplication
- Determinant, inverse and transpose
- Null space of singular matrices
- Finding eigenvalues and eigenvectors
- Python for solving systems, finding inverse, null space and eigenvalues/vectors
- Diagonalising matrices (we will use this for systems of differential equations)


## The SIR model with two age groups

The partitioning of the population can be refined to include other attributes relevant to the disease. One of the most important of these is the age. Let us assume we partition the population into two age groups, children and adults, and label them by the index $i=1,2$. Children can catch the infection from other children or from adults; likewise, adults can catch the infection from other adults or from children. Calling their respective rates of infection $\lambda_1(t)$ and $\lambda_2(t)$ we get
\begin{align}
\lambda_1(t) = \beta(C_{11}\frac{I_1}{N_1} + C_{12}\frac{I_2}{N_2})S_1\\
\lambda_2(t) = \beta(C_{21}\frac{I_1}{N_1} + C_{22}\frac{I_2}{N_2})S_2
\end{align}
where $C_{ij}$ are contact matrices, quantifying how much each age group interacts with the other. The ordinary differential equations of this age-structured SIR model are

\begin{align}
\dot S_i &= -\lambda_i(t)S_i \\
\dot I_i &= \lambda(t)_iI_i - \gamma I_i \\
\dot R_i &= \gamma I_i
\end{align}

Again, for each $i$ the sum $N_i = S_i + I_i + R_i$ remains constant. What do we expect qualitatively ? The group that has a greater rate will catch the disease faster and catch more of it. This depends on how the entries of the contact matrix are distributed.

This example integrates the above equations to **epidemic curve** for both the children and the adults. We see that they have unequal rates of infection.

```python
%%capture
## compile PyRoss for this notebook
import os
owd = os.getcwd()
os.chdir('../')
%run setup.py install
os.chdir(owd)
```

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

```python
M  = 2                  # the population has two age groups
N  =  1000000           # and this is the total population

beta  = 0.0131          # infection rate
gIa   = 1./7            # recovery rate of asymptomatic infectives
gIs   = 1./7            # recovery rate of asymptomatic infectives
alpha = 0               # fraction of asymptomatic infectives
fsa   = 1               # the self-isolation parameter

Ni = np.zeros((M))      # population in each group
fi = np.zeros((M))      # fraction of population in age age group

# set the age structure
fi = np.array((0.25, 0.75))
for i in range(M):
    Ni[i] = fi[i]*N

# set the contact structure
C = np.array(([18., 9.], [3., 12.]))

Ia_0 = np.array((1,1))  # each age group has asymptomatic infectives
Is_0 = np.array((1,1))  # and also symptomatic infectives
R_0  = np.array((0,0))  # there are no recovered individuals initially
S_0  = Ni - (Ia_0 + Is_0 + R_0)

# matrix for linearised dynamics
L = np.zeros((M, M))
for i in range(M):
    for j in range(M):
        L[i,j]=C[i,j]*Ni[i]/Ni[j]

L = (alpha*beta/gIs)*L

# the basic reproductive ratio
r0 = np.max(np.linalg.eigvals(L))

print("The basic reproductive ratio for these parameters is", r0)
```

    The basic reproductive ratio for these parameters is 0.0

```python
# duration of simulation and data file
Tf=200; Nf=2000; filename='this.mat'

# the contact structure is independent of time
def contactMatrix(t):
    return C

# instantiate model
parameters = {'alpha':alpha, 'beta':beta, 'gIa':gIa, 'gIs':gIs,'fsa':fsa}
model = pyross.models.SIR(parameters, M, Ni)

# simulate model
data=model.simulate(S_0, Ia_0, Is_0, contactMatrix, Tf, Nf, filename)
```

```python
IK = data.get('X')[:,2*M].flatten()
IA = data.get('X')[:,2*M+1].flatten()
t  = data.get('t')
```

```python
fig = plt.figure(num=None, figsize=(10, 8), dpi=80, facecolor='w', edgecolor='k')
plt.rcParams.update({'font.size': 22})

plt.fill_between(t, 0, IK/Ni[0], color="#348ABD", alpha=0.3)
plt.plot(t, IK/Ni[0], '-', color="#348ABD", label='$Children$', lw=4)

plt.fill_between(t, 0, IA/Ni[1], color='#A60628', alpha=0.3)
plt.plot(t, IA/Ni[1], '-', color='#A60628', label='$Adults$', lw=4)

plt.legend(fontsize=26); plt.grid()
plt.autoscale(enable=True, axis='x', tight=True)
```


# Radio Tutorial 2: Density models and radio burst speeds

### The goal for this tutorial

The goal will be to plot a variety of 1 dimensional electron density models of the solar atmosphere, both theoretical and empirical. The empirical models that we will use (Newkirk and Saito) are often used for estimates of radio burst altitude and speed in the corona. We'll be plotting electrons density as a function of height $n(r)$.

N.B. When I use the term height, altitude or distance I mean heliocnetric distance i.e., 1 R$_{\odot}$ is the solar surface.

### First let's start with the hydrostatic plane parallel model

Start with plotting the following equation from 1-10 solar radii
\begin{equation}
n(r) = n_0\mathrm{exp}\bigg(\frac{-r}{H}\bigg)
\end{equation}

where $n_0$ is the density at the base of the corona and $r$ is the distance into the solar atmosphere. $H$ is known as the scale height. And is given by

\begin{equation}
H = \frac{m_p g_{\odot}}{k_B T}
\end{equation}

where $k_B$ is Boltzmann's constant, $T$ is the temperature, $m_p$ is the proton mass, and $g_{\odot}$ is solar gravity. What is the significance of the scale height?

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

### First define your constants here

```python
n0 = None       # Electron density at 1 Rsun (cm^-3)
T = None        # MK
kb = None       # Boltzman constant J/K
mp = None       # Proton mass kg
Msun = None     # Solar mass kg
rsun = None     # Solar radius m
G = None        # Universal gravitational constant (m3 kg-1 s-2)
gsun = None     # Solar gravity (m/s/s)
```

### First build a 1D distance grid

```python
# Build a 1D array of distance values in units of Rsun.
# Choose a reasonable resolution e.g., <0.1 Rsun.
```

### Let's look at a plane-parallel hydrostatic model

```python
# Start with defining the scale height.
# Print its value in Mm. Does this make sense? What is the meaning of this value?
```

```python
# Now use your distance grid and scale height to get the density values, using Equation 1 above.
```

```python
# And plot the result...
'''
fig, ax = plt.subplots()
ax.plot(radius, npp)
ax.set_xlabel('Heliocentric Distance (R$_{\odot}$)')
ax.set_ylabel('Electron density (cm$^{-3})$')
plt.show()
'''
```

    "\nfig, ax = plt.subplots()\nax.plot(radius, npp)\nax.set_xlabel('Heliocentric Distance (R$_{\\odot}$)')\nax.set_ylabel('Electron density (cm$^{-3})$')\nplt.show()\n"

### Increasing the temperature of the atmosphere

Now let's see what happens when we increase the temperature of the atmosphere. Make a scale height for an atmosphere of 1 MK, 5 MK, and 10 MK. What would you expect to happen?

```python
# The most convenient way to do this is to build a function that takes your distance grid
# and temperature as input plots the density model. Fill in the function template below.

fig, ax = plt.subplots()
def plothydrostat(radius, temp, label=None):

    # Inside here calulate scale height H
    # Then get the density and plot the result

    #ax.plot(radius, n, label=label)

    return #ax

'''
plothydrostat(radius, T1, label='1 MK')
plothydrostat(radius, T2, label='5 MK')
ax = plothydrostat(radius, T3, label='10 MK')
ax.set_yscale('log')
ax.set_xlabel('Heliocentric Distance (R$_{\odot}$)')
ax.set_ylabel('Electron density (cm$^{-3})$')
plt.legend()
plt.show()
'''
```

### Now let's plot the spherically symmetric solution

Now we'll do something slightly more realistic. Gravity in the solar atmosphere is of course not a constant, but decreases with increasing altitude. The hydrostatic model that's accounts for this is similar:

\begin{equation}
n = n_0\mathrm{exp}\bigg( \frac{1}{H}\bigg\{\frac{1}{r}-1\bigg\} \bigg)
\end{equation}

where $H$ takes on the slightly different form

\begin{equation}
H = \frac{2 k_B T R_{\odot}^2}{G M_{\odot}m_p}
\end{equation}

```python
# Define these equations here. Take care with your units, units of Rsun or in meters etc.

# the plot up the result, and also compare to the plane parallel solution you calculated previously.
'''
plt.plot(radius, nss, label='Plane parallel')
plt.plot(radius, npp, label='Spherically symmetric')
plt.yscale('log')
plt.ylim(1e4,1e10)
plt.xlabel('Heliocentric Distance (R$_{\odot}$)')
plt.ylabel('Electron density (cm$^{-3})$')
plt.legend()
plt.show()
'''
```

    "\nplt.plot(radius, nss, label='Plane parallel')\nplt.plot(radius, npp, label='Spherically symmetric')\nplt.yscale('log')\nplt.ylim(1e4,1e10)\nplt.xlabel('Heliocentric Distance (R$_{\\odot}$)')\nplt.ylabel('Electron density (cm$^{-3})$')\nplt.legend()\nplt.show()\n"

### What does this look like in terms of plasma frequency?

```python
# Use the plasma frequency formula from the lectures to convert from density to frequency in MHz
# Then plot as a function of radius.

# What is the radius at which you expect 100 MHz?

'''
plt.plot(radius, fplasma_ss)
plt.xlabel('Heliocentric Distance (R$_{\odot}$)')
plt.ylabel('Plasma Frequency (MHz)')
plt.yscale('log')
plt.ylim(1,300)
plt.show()
'''

```

    "\nplt.plot(radius, fplasma_ss)\nplt.xlabel('Heliocentric Distance (R$_{\\odot}$)')\nplt.ylabel('Plasma Frequency (MHz)')\nplt.yscale('log')\nplt.ylim(1,300)\nplt.show()\n"

### Now let's plot some empirical density models

Radio bursts in solar physics are most often analysed with 1D empircal density models. There are many: Newkirk, Saito, Leblanc, Baumbach Allen, Mann. This list goes on. Each are suitable to different parts of the corona. For example, the Newkirk and Saito can be used for the low corona, in the early stages of eruption typically below a few solar radii (although the Newkirk model is known to be slightly higher than epxected densities). The Leblanc and Mann models is for distances further into the heliosphere.

Here we'll take a look at the Newkirk and Saito models. Firtly the newkirk model

\begin{equation}
n(r) = 4.2\times10^4 \times 10^{4.32/r}
\end{equation}

where $r$ is units of solar radii. Plot this model along side the spherically symmetric model from above and compare them.

```python
# Plot the the Newkirk and spherically symmetric model

```

Now plot the  Saito model using the following formula

\begin{equation}
n(r) = c_1r^{-d_1} + c_2r^{-d_2}
\end{equation}

where $r$ is units of solar radii and $c1=1.38\times10^6$, $c2=1.68\times10^8$, $d1=2.14$, $d2=6.13$.

```python
# Use this formula to plot the Saito, overplotting Newkirk, and the hydrostatic model.
```

### Now let's get the speed of the shock that caused the type II

```python
# Firstly write a function of frequency to density

def freq2dens(freq):
    ne = None
    return ne # cm^-3
```

```python
import numpy as np

result = np.load('/Users/eoincarley/Desktop/WP4-T1/STELLAR_SSW_tutorials/day2_radio_session/ft_coords.npz', allow_pickle=True)

# Read in the frequency time points from Pearse's tutorial.

#freq = np.array([150e6, 100e6, 40e6, 30e6, 20e6])
#tsec = np.array([0, 120, 200, 360, 520])

# Convert the frequencies to densities.
# density = freq2dens(freq)
list(result.keys())
time = result['arr_0']
```

```python
# Now use one of the density models to map density to radius.
# You can rearrange the Newkirk model to get the density directly.
# However, with a polynomial like Saito, the most convient would
# be to interpolate between density and radius
# Hint. Use numpy.interp.

# The results should be an array of heliocentric distances.
```

```python
# Plot the results of radii vs time and fit a line to the data points
# Use scipy.stats.linregress
```

### Now estimate the speed using a Saito model.

Follow the same procedure for the Saito model. Compare the distance values that you get with Saito. Compare the the speed given by the Newkirk model and Saito. Which one would you trust more?

```python

```


```python
from statsmodels.tsa.seasonal import seasonal_decompose
import matplotlib.pyplot as plt
import scipy.stats as st
import seaborn as sns
import pandas as pd
from scipy.stats import norm, uniform, expon, t, probplot
import scipy.stats as st
from scipy.integrate import quad
from sympy.solvers import solve
from sympy import Symbol
import numpy as np
from pandas import Series, DataFrame
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.tsa.stattools import acf
from pandas.plotting import lag_plot
```

```python
w = DataFrame(np.random.normal(size=1000))
MA = DataFrame(w.rolling(window=3).mean()).dropna()
plot_acf(MA, lags=12, c="C1")
plt.vlines(x=2.1, ymin=0, ymax=1/3, color="red", linestyle='--', label="Geschaetzt")
plt.vlines(x=1.1, ymin=0, ymax=2/3, color="red", linestyle='--')
plt.vlines(x=0.1, ymin=0, ymax=1, color="red", linestyle='--')
plt.legend()

```

```python
df = pd.read_table("wave.dat")
```

```python
df.head()
```

      waveht

      0
      367

      1
      407

      2
      -255

      3
      -515

      4
      -500

```python
df.plot()
plt.xlabel("Time")
plt.ylabel("Wave height (mm)")
plt.show()
```

```python
df.loc[0:59,:].plot()
plt.xlabel("Time")
plt.ylabel("Wave height (mm)")
plt.show()
```

```python
from statsmodels.tsa.stattools import acf, plot_acf
acf(df["waveht"])[1]
```

    0.47025639618837917

```python

```

```python
lag_plot(df,1)
plt.xlabel("x_t")
plt.ylabel("x_(t+k)")
plt.show()
```

```python
acf(df["waveht"])[[2,3,5,10]]
```

    C:\Users\flori\Anaconda3\lib\site-packages\statsmodels\tsa\stattools.py:541: FutureWarning: fft=True will become the default in a future version of statsmodels. To suppress this warning, explicitly set fft=False.
      warnings.warn(msg, FutureWarning)

    array([-0.26291153, -0.49891702, -0.21499293, -0.07431329])

```python
plt.subplot(221)
lag_plot(df,2)
plt.xlabel("x_t")
plt.ylabel("x_(t+k)")
plt.subplot(222)
lag_plot(df,3)
plt.xlabel("x_t")
plt.ylabel("x_(t+k)")
plt.subplot(223)
lag_plot(df,5)
plt.xlabel("x_t")
plt.ylabel("x_(t+k)")
plt.subplot(224)
lag_plot(df,10)
plt.xlabel("x_t")
plt.ylabel("x_(t+k)")
plt.show()
```

```python
plot_acf(df["waveht"])
```

```python
AirP = pd.read_csv("AirPassengers.csv")
AirP.head()
AirP["TravelDate"] = pd.to_datetime(AirP["TravelDate"])
AirP.set_index("TravelDate", inplace = True)
AirP.head()
plot_acf(AirP["Passengers"])
```

```python
from statsmodels.tsa.seasonal import seasonal_decompose
remainder = seasonal_decompose(AirP["Passengers"], model="multiplicative").resid[6:138]
```

```python
remainder.plot()
```

```python
plot_acf(remainder, lags = 21)
```

```python
print(AirP["Passengers"][6:138].std())
trend = seasonal_decompose(AirP["Passengers"], model="multiplicative").trend[6:138]
print((AirP["Passengers"][6:138]-trend).std())
decomp = seasonal_decompose(AirP["Passengers"])
seasonal_decompose(AirP["Passengers"], model="multiplicative").plot()
print((decomp.observed - decomp.trend)[6:138].std())
print(seasonal_decompose(AirP["Passengers"]).resid[6:138].std())
```

```python
], model="multiplicative")
```