# Name: E.M.D.A. Bandara
# Index No: 190072E

```python
for i in range(1, 6):
  print("%d : %d"%(i, i**2))
```

    1 : 1
    2 : 4
    3 : 9
    4 : 16
    5 : 25

```python
import sympy
for i in range(1,6):
  if not sympy.isprime(i):
    print(i,' :', i**2)
```

    1  : 1
    4  : 16

```python
squares = [i**2 for i in range(1,6)]
for i in range(5):
  print(i+1, ": ", squares[i])
```

    1 :  1
    2 :  4
    3 :  9
    4 :  16
    5 :  25

```python

print('H')
```

    H

```python
import numpy as np

matA = [[1, 2],[3, 4],[5, 6]]
matB = [[7, 8, 9, 1], [1, 2, 3, 4]]

matA = np.array(matA)
matB = np.array(matB)

print(matA.shape)
print(matB.shape)

matMul = np.matmul(matA, matB)
print(matMul)

```

    (3, 2)
    (2, 4)
    [[ 9 12 15  9]
     [25 32 39 19]
     [41 52 63 29]]

```python
matA = [[1, 2], [3, 4], [5, 6]]
matB = [[3, 2], [5, 4], [3, 1]]

matDot = np.multiply(matA, matB)
print(matDot)
```

    [[ 3  4]
     [15 16]
     [15  6]]

```python
A = np.random.randint(10, size=(5, 7))
print(A)
```

    [[0 8 1 6 2 2 6]
     [9 7 5 0 0 7 6]
     [7 2 8 7 5 9 4]
     [0 1 6 1 5 7 9]
     [9 6 3 5 7 4 5]]

```python
import matplotlib.pyplot as plt
from numpy import linalg

m, c = 2, -4
N = 100
x = np.linspace(0, N-1, N).reshape(N, 1)
sigma = 10
y = m*x + c + np.random.normal(0, sigma, (N, 1))

plt.scatter(x, y)

x = np.append(np.ones((N, 1)), x, axis = 1)

w = linalg.inv(x.T @ x) @ x.T @ y
```

```python
import cv2 as cv

```

```python

```


```python
import numpy as np
import sympy as sp
import scipy as sc
import matplotlib.pyplot as plt
from scipy.optimize import fsolve
import scipy.optimize as op
from mpl_toolkits.mplot3d import axes3d
from scipy.io import loadmat
import librosa
import plotly.graph_objects as go
from IPython.display import Audio
import plotly.express
from audio2numpy import open_audio
from librosa import display
%matplotlib notebook
```

$PROBLEM$ $1$

$Part$ $A$

```python
#plotting the sawtooth function for x between 0 and 2
resolution = 0.0001
x = np.arange(0,2,resolution)
square = np.zeros_like(x)
squarefft = np.zeros_like(x)
square[:int(x.size/2)] = x[:10000]
square[int(x.size/2):]= 0
print("The Original Sawtooh Function")
plt.figure()
plt.plot(x,square)
plt.show()
```

    The Original Sawtooh Function

    <IPython.core.display.Javascript object>

$Part$ $B$

```python
#approximating the sawtooth function with fourier series.
#for k=1,3,5,10,25,100 all plotted under one graph with different colors
resolution = 0.0001
x = np.arange(0,2,resolution)
square = np.zeros_like(x)
squarefft = np.zeros_like(x)
square[:int(x.size/2)] = x[:10000]
square[int(x.size/2):]= 0
plt.figure()
plt.plot(x,square,label='Original Function')
a0=np.trapz(square,x)/np.pi/2
squarefft=squarefft+a0
n=100
for i in range(n):
    i=i+1
    a=np.trapz(square*np.sin(i*x),x)/np.pi
    b=np.trapz(square*np.cos(i*x),x)/np.pi
    squarefft=squarefft+a*np.sin(i*x)+b*np.cos(i*x)
    if i==1: plt.plot(x,squarefft,label='For K=1')
    if i==3: plt.plot(x,squarefft, label='For K=3')
    if i==5: plt.plot(x,squarefft, label='For K=5')
    if i==10: plt.plot(x,squarefft, label='For K=10')
    if i==25: plt.plot(x,squarefft, label='For K=25')
    if i==100: plt.plot(x,squarefft, label='For K=100')
#we can see that for K=100 light pink approximates better than all for the function
plt.legend()
plt.show()
```

    <IPython.core.display.Javascript object>

$PROBLEM$ $2$

$Part$ $A$

```python
from scipy.io import loadmat
data = loadmat('dataF.mat')
s=data['s'].reshape((-1,1))
N=len(s)
dt=1/Fs
T=N*dt
print("Sample Freq =",Fs,", N =",len(s),", dt=",dt)
t=np.linspace(0,T,N)
#from graph we can clearly see from graph the Period (T) is 0.1 seconds
#frequency is 1/T
Freq=1/0.1
print("Frequency is",Freq,"Hz")
plt.figure()
plt.ylabel("Amplitude")
plt.xlabel("Time [s]")
plt.plot(t,t*0,t,s)
plt.show()

```

    Sample Freq = 2000 , N = 1000 , dt= 0.0005
    Frequency is 10.0 Hz

    <IPython.core.display.Javascript object>

$Part$ $B$

```python
#the first 7 frequencies in the fourie series
for k in range(7):
    print("f",k,"=",(Fs*k)/N,"Hz",sep='')
```

    f0=0.0Hz
    f1=2.0Hz
    f2=4.0Hz
    f3=6.0Hz
    f4=8.0Hz
    f5=10.0Hz
    f6=12.0Hz

$Part$ $C$

```python
#calculating Coefficients by formula
i=1j
t=np.linspace(0,dt,N)
fre=np.array([0,2,4,6,8,10,12]).reshape((-1,1))
exp=np.zeros(7,dtype=np.complex_)
c=np.zeros(7,dtype=np.complex_)
for m in range(7):
    exp[m]=np.exp(-i*2*np.pi*fre[m]*t[m])

for m in range(7):
    c[m]=np.sum(s[:,0]*exp[m])

for  m in range (7):
     print("c",m,"=",c[m],sep='')
```

    c0=(-2.1316282072803006e-14+0j)
    c1=(-2.842170943040401e-14+3.2526065174565133e-19j)
    c2=(-4.973799150320701e-14+1.3010426069826053e-18j)
    c3=(-4.263256414560601e-14+2.6020852139652106e-18j)
    c4=(-2.842170943040401e-14+1.734723475976807e-18j)
    c5=(-6.394884621840902e-14+8.673617379884035e-18j)
    c6=(-4.973799150320701e-14+8.673617379884035e-18j)

$Part$ $D$

```python
#power Energy is
for k in range(7):
    print("P",k,"=",np.absolute(C[k])**2/N,sep='')
```

    P0=2.3322139296651962e-36
    P1=5.553167363672771e-37
    P2=1.3150716592339733e-35
    P3=1.6461430351997995e-35
    P4=1.6932868921805134e-36
    P5=0.00025
    P6=6.348605483009806e-36

$Part$ $E$

```python
#the frequencey with max energy is f5 with P5=.00025, we can also confirm this in part g graph
print("The frequency with max enegy is f5=10Hz")
```

    The frequency with max enegy is f5=10Hz

$Part$ $F$

```python
C=(np.fft.fft(s[:,0]))/N
for k in range(7):
    print("C",k,"=",C[k],sep='')
```

    C0=(-4.829300083516447e-17+0j)
    C1=(9.435194973283991e-18+2.1593837828959295e-17j)
    C2=(-8.379431550806593e-17+7.82893944342044e-17j)
    C3=(-1.0741577836851374e-16-7.01660951563099e-17j)
    C4=(3.174901277019806e-17-2.6177988469290647e-17j)
    C5=(-1.511585655387449e-16-0.5j)
    C6=(5.6787477842867814e-18-7.947551388076873e-17j)

$Part$ $G$

```python
dt=1/2000
n=1000
C=np.fft.fft(s[:,0])/n
freq=np.linspace(0,1/dt,n)
freq=np.arange(0,1000,2).reshape((-1,1))
#the the peak at the right value about 10HZ
print("The peak which has most presence is about 10Hz from graph")
plt.figure()
plt.ylim(-0.00001,0.0006) #to take a closer look at the graph
plt.xlim(0, 200) #to take a closer look at the graph
plt.ylabel("Amplitude")
plt.xlabel("Frequency [Hz]")
plt.plot(freq[:n//2],np.abs(C)[:n//2]*1/n)
plt.show()
```

    The peak which has most presence is about 10Hz from graph

    <IPython.core.display.Javascript object>

$PROBLEM$ $3$

$Part$ $A$

```python
sr=data['sr'].reshape((-1,1))
s=data['s'].reshape((-1,1))
# print("1000 samples were taken with Fs of 2000, thus time was 0.5 sec")
t=np.arange(0,0.5,0.0005).reshape((-1,1))
Fs=2000
plt.figure()
plt.plot(t,t*0,t,sr)
plt.show()
```

    <IPython.core.display.Javascript object>

$Part$ $B$

```python
f = np.fft.fft(sr[:,0])
N=1000
plt.figure()
plt.ylabel("Amplitude")
plt.xlabel("Frequency [Hz]")
plt.plot(freq[:N//2],np.abs(f)[:N//2]*1/N)
plt.show()
```

    <IPython.core.display.Javascript object>

$Part$ $C$

```python
print("From graph we can estimate the 4 peaks\n ")
# F1=25Hz
# F2=65Hz
# F3=120Hz
# F4=200Hz
print("F1=25Hz");print("F2=65Hz");print("F3=120Hz");print("F4=200Hz")
```

    From graph we can estimate the 4 peaks

    F1=25Hz
    F2=65Hz
    F3=120Hz
    F4=200Hz

$PROBLEM$ $4$

$Part$ $A$

```python
from scipy.io import wavfile
samplerate, data = wavfile.read('wilhelm.wav')
s=data[:,0]
# Fs=44100 #given sampling frequency
print("Samplerate =",samplerate,", N =",len(data))
N=len(data)
T=len(data)/samplerate
t=np.linspace(0,T,N)
plt.figure()
plt.ylabel("Data")
plt.xlabel("Time [s]")
plt.plot(t,s)
```

    Samplerate = 44100 , N = 49253

    <IPython.core.display.Javascript object>

    [<matplotlib.lines.Line2D at 0x219c1141760>]

$Part$ $B$

```python
freq = np.linspace(0, samplerate, N)
f = np.fft.fft(s)
plt.figure()
plt.ylabel("Amplitude")
plt.xlabel("Frequency [Hz]")
print("From graph the prominent frewuencies are about F1=1400Hz and F2=3000Hz")
plt.plot(freq[:N//2],np.abs(f)[:N//2]*1/N)
plt.show()
```

    <IPython.core.display.Javascript object>

    From graph the prominent frewuencies are about F1=1400Hz and F2=3000Hz


# Understanding $\chi^2$ 2 degrees of freedom noise

In asteroseismology, we tend to work with power-spectra, as opposed to using a timeseries. To get to a power-spectrum, we have to transform into the frequency domain (often done with a Fourier transform, or a Lomb-Scargle transform, such as in [lightkurve](https://docs.lightkurve.org/)).

However because nothing can ever be easy, noise that is *Normal distributed in flux* becomes *chi-squared two degrees of freedom distributed in power*. In this notebook I'm going to break down:

- what chi-squared two degrees of freedom ($\chi^2_2$ for short) noise looks like
- how to fit a model to a power spectrum
- what happens to the noise properites when binned
- how to fit a model to the binned data

```python
import numpy as np
import pymc3 as pm
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_palette('colorblind')
import theano.tensor as tt
import mystyle as ms
```

The distribution of $\chi^2_k$ noise for a degrees of freedom $k$ is given by:

\begin{equation}
p(x | k) = \frac{1}{2^{k/2} \Gamma(k/2)}x^{k/2-1}e^{-x/2}
\end{equation}

where $\Gamma$ is the [Gamma function](https://en.wikipedia.org/wiki/Gamma_function), and $x$ here is the space over which the noise is distributed. Conveniently, the mean of the $\chi^2_k$ distribution is simply $k$, the degrees of freedom.

For $k=2$, we can simplify this equation to:
\begin{equation}
p(x | 2) =\frac{1}{2^{1} \Gamma(1)}x^{1-1}e^{-x/2} \rightarrow \frac{1}{2}e^{-x/2}
\end{equation}

because conveniently, $\Gamma(1)$ is 1. The mean value of this probability distribution is 2, which is important to remember.

To see whether this equation accurately describes the noise on a Fourier transformed time-series, let's create some fake white noise and Fourier transform it to check out its properties:

We're going to take the simplest possible case: white noise centered at $0$, with a standard deviation of $1$.

```python
npts = 20000
sigma = 1.
y = np.random.randn(npts)*sigma
pf = np.abs(np.fft.fft(y))**2
p = pf[int(npts/2):]/(npts/2)
```

```python
with plt.style.context(ms.ms):
    fig, ax = plt.subplots(1,2,figsize=(12,4))
    ax[0].plot(y)
    ax[1].plot(p)
    ax[0].set_ylabel('Flux')
    ax[1].set_ylabel(r'Power$^2$ / $N$')
    ax[0].set_xlabel('Arbitrary Time')
    ax[1].set_xlabel('Arbitrary Frequency')
    fig.tight_layout()
    plt.show()
```

## What is our signal in the frequency domain?

Before we can get onto the noise properties, we need to understand what signal we expect in our power spectrum.

When computing a power spectrum, we are looking at the **mean squared power** of the signal at each frequency. Fortunately, our timeseries data is easy to understand. For normally distributed noise centered at zero with a standard deviation $\sigma$, the mean squared power is simply $\sigma^2$, the variance. For example, for our data:

```python
print('Mean squared power of the data: {:.2f}'.format(np.mean(y**2)))
print('Standard deviation squared: {:.2f}'.format(sigma**2))
```

    Mean squared power of the data: 1.00
    Standard deviation squared: 1.00

as expected!

The next step is understanding where this power is concentrated in our power spectrum. Since we are dealing with white noise, the power is equally distributed across *all* frequencies. So in the absense of noise, our power spectrum should take the value of $\sigma^2$. Lets see if this holds true for our data:

```python
print('Mean of power spectrum: {:.2f}'.format(np.mean(p)))
```

    Mean of power spectrum: 2.00

It looks like the mean of our spectrum is $2\sigma^2$. This is due to the presence of the $\chi^2_2$ noise.

## What are the noise properties of our data?

$\chi^2_2$ noise is *multiplicative*, meaning that it appears *relative* to our data. As mentioned above, a particular property of chi-Squared distributed noise is that it has a mean equal to it's degrees of freedom. In our case, that means $2$.

Because the noise is multiplicative, we can recover the observed mean in our power spectrum by multiplying our signal, $\sigma^2$, with the degrees of freedom, $k$:

```python
print('Mean of power spectrum: {:.2f}'.format(sigma**2 * 2.))
```

    Mean of power spectrum: 2.00

as expected!

To better understand how this noise is distributed, we can plot our data divided through by our known signal to look at its residuals, compared to a $\chi^2_2$ distribution:

```python
signal = sigma**2
```

```python
with plt.style.context(ms.ms):
    sns.distplot(p/signal, label='Data')
    sns.distplot(np.random.chisquare(2., size=npts), label=r'$\chi^2_2$ noise')
    plt.legend(fontsize=20)
    plt.xlim(0,15)
    plt.xlabel('Power')
    plt.ylabel('Normalised N')
```

As you can see, both line up nicely! The noise on our data is indeed $\chi^2_2$ distributed.

# Fitting a model to the data

As a reminder, the simplified $\chi^2_2$ distribution goes as

\begin{equation}
p(x | 2) = \frac{1}{2}e^{-x/2} \, .
\end{equation}

You might notice that there is no reference to a model here. So how do we fit a model to data?

In practice, in order to describe the $\chi^2_2$ noise properties relative to the signal, we have to rescale the equation so that it becomes a function of the model $M$, so that

\begin{equation}
p(x | k = 2) = \frac{1}{2M}e^{-x/2M} \, ,
\end{equation}

where $x$ is our observed data.

This is the appropriate likelihood function for fitting a model to a property of some power spectrum data, instead of the underlying amplitude signal. For more details, see Anderson et al. (1990), Duvall & Harvey (1986), or more modern peakbagging work such as Lund et al. (2017), Davies et al. (2016), Santos et al. (2019) etc.

**Important Note:** In many of these works the factor of $2$ is excluded from the likelihood function, and in truth it doesn't matter whether or not it is included for most inference, it will just change the power of the model by $2$, but still fit the data, because the noise is multiplicative.

## Fitting a spectrum in practice

When fitting a model to a power spectrum using a probabilistic programming tool such as [Stan](https://mc-stan.org/) or [PyMC3](https://docs.pymc.io/), it isn't straightforward to model power spectrum data, as there is no function for the rescaled $\chi^2_2$ function shown above.

Fortunately, there is a solution. The $\chi^2_k$ distribution is simply a special case of the $\Gamma$ distribution (not to be confused with the $\Gamma$ function!). It goes as follows:

\begin{equation}
p(x | \alpha, \beta) = \frac{\beta^\alpha}{\Gamma(\alpha)}x^{\alpha-1}e^{-\beta x} .
\end{equation}

We can already see some similarity between this and the $\chi^2_k$ distribution. Using specific values for $\alpha$ and $\beta$, we can directly recover the $\chi^2_2$ distribution relative to a model.

The specific substitions we want to use are $\alpha = k/2$ and $\beta = 1/2M$. When substituting these into the above equation we get

\begin{equation}
p(x | \alpha = 1, \beta = 1/2M) = \frac{1}{2M^1\Gamma(1)}x^{1-1}e^{-x/2M} \rightarrow \frac{1}{2M}e^{-x/2M} ,
\end{equation}

which is the likelihood function we found above!

Just to showcase how this would work in practice, I've written a quick PyMC3 model to recover the background signal in our power spectrum.

```python
pm_model = pm.Model()
with pm_model:
    back = pm.HalfNormal('back', 100.)

    model = tt.ones(len(p)) * back

    like = pm.Gamma('like',alpha=1., beta=1./(2*model), observed=p)
    t_reg = pm.sample(tune=2000, chains=4)
```

    Auto-assigning NUTS sampler...
    Initializing NUTS using jitter+adapt_diag...
    Multiprocess sampling (4 chains in 4 jobs)
    NUTS: [back]
    Sampling 4 chains: 100%|██████████| 10000/10000 [00:04<00:00, 2371.32draws/s]

```python
pm.summary(t_reg, var_names=['back']);
pm.traceplot(t_reg, var_names=['back']);
```

And we can check that we have correctly found the background by dividing the data through our model, and checking the residuals have the noise properties we expect:

```python
resb = np.median(t_reg['back'])
mod = np.ones(len(p)) * resb

with plt.style.context(ms.ms):
    sns.distplot(p/mod, label='Data / Model')
    sns.distplot(np.random.chisquare(2., size=npts), label=r'$\chi^2_2$')
    plt.legend(fontsize=20)
    plt.xlim(0,15)
    plt.xlabel('Power')
    plt.ylabel('Normalised N')
```

The noise properties are as we expect them, and we have recovered our known signal to high precision! Well done us.

# Fitting a binned power spectrum

A good asteroseismic power spectrum can be *very* long, and consequently, very computationally heavy to fit a model to. When dealing with fitting artefacts that don't require a high frequency resolution (e.g. the granulation background of a star), binning the data can dramatically reduce the computational cost while having a negligible impact on the inference of a model.

However because we can't have nice things, binning a power spectrum changes its noise properties. Let's investigate how these noise properties change, and how we can still fit a decent model to a binned power spectrum.

We're going to use a binning factor of $b = 5$, meaning that we take 5 bins of power and sum them into a new, single bin, as follows:

```python
b = 5
m = int(len(p)/b)
p_bin = np.sum(p[:m*b].reshape((m, b)),axis=1)
```

We can see what the distribution of the new binned power looks like compared to our unbinned spectrum:

```python
with plt.style.context(ms.ms):
    sns.distplot(p, label='Data')
    sns.distplot(p_bin, label='Binned Data')
    sns.distplot(np.random.chisquare(2., size=npts), label=r'$\chi^2_2$')
    plt.legend(fontsize=20)
    plt.xlim(0,25)
    plt.xlabel('Power')
    plt.ylabel('Normalised N')
```

```python
print('Mean of the binned data: {:.2f}'.format(np.mean(p_bin)))
```

    Mean of the binned data: 9.99

So it looks like our data has increased in power, and the noise properties have changed from something we previously understood to god knows what.

What if we try performing a fit to the background of these data? We know that our background has a value of $1$, from our previous fit.

```python
pm_model = pm.Model()
with pm_model:
    back = pm.HalfNormal('back', 100.)
    model = tt.ones(len(p_bin)) * back

    like = pm.Gamma('like',alpha=1., beta=1./(2*model), observed=p_bin)
    t_bin = pm.sample(tune=2000, chains=4)
```

    Auto-assigning NUTS sampler...
    Initializing NUTS using jitter+adapt_diag...
    Multiprocess sampling (4 chains in 4 jobs)
    NUTS: [back]
    Sampling 4 chains: 100%|██████████| 10000/10000 [00:02<00:00, 3837.24draws/s]

```python
pm.summary(t_bin, var_names=['back']);
pm.traceplot(t_bin, var_names=['back']);
```

```python
resb = np.median(t_bin['back'])
mod = np.ones(len(p_bin)) * resb

with plt.style.context(ms.ms):
    sns.distplot(p_bin/mod, label='Data / Model')
    sns.distplot(np.random.chisquare(2., size=npts)/2., label=r'$\chi^2_2$')
    plt.legend(fontsize=20)
    plt.xlim(0,15)
    plt.xlabel('Power')
    plt.ylabel('Normalised N')
```

It looks like the fit mean value is $5$, not $1$, as expected. The residuals, of course, don't look like a $\chi^2_2$ distribution.

Now if we remember, the mean of a $\chi^2_k$ distribution is equal to $k$. The background of our un-binned power spectrum, with degrees of freedom $k=2$, is $1$. The mean of our binned power spectrum is $5$. Could our binned power spectrum follow a $\chi^2_{10}$ distribution?

```python
with plt.style.context(ms.ms):
    sns.distplot(p_bin, label='Data')
    sns.distplot(np.random.chisquare(2., size=npts), label=r'$\chi^2_2$')
    sns.distplot(np.random.chisquare(10., size=npts), label=r'$\chi^2_{10}$')
    plt.legend(fontsize=20)
    plt.xlim(0,25)
    plt.xlabel('Power')
    plt.ylabel('Normalised N')
```

Indeed it is! It's a known property of $\chi^2_2$ noise that when binning over $b$ bins, the noise statistics become $\chi^2_k$ with $k = 2b$ (Appourchaux 2004).

## So how do we model this?

If we remember from earlier, the $\chi^2_2$ distribution, is a special case of the Gamma distribution, which can be evaluated relative to a model by setting the parameters $\alpha = k/2$ and $\beta=1/2M$.

To fit a model to $\chi^2_{10}$ noise, we set $\alpha = 10/2$ when evaluating the Gamma distribution. We can see this by plotting a Gamma distribution with these properties on the above plot:

```python
M = np.ones(len(p_bin))*sigma**2
with plt.style.context(ms.ms):
    sns.distplot(p_bin/M, label='Data / M')
    sns.distplot(np.random.chisquare(10., size=npts), label=r'$\chi^2_{10}$')
    sns.distplot(np.random.gamma(10./2.,2*M),   #Note that the numpy gamma function syntax is a little different
                 label=r'$\gamma(\frac{10}{2}, \frac{1}{2M})$')
    plt.legend(fontsize=20)
    plt.xlabel('Power')
    plt.ylabel('Normalised N')
```

And consequently, we can set $\alpha$ to this value when we model our background, and see what happens!

```python
pm_model = pm.Model()
with pm_model:
    back = pm.HalfNormal('back', 100.)
    model = tt.ones(len(p_bin)) * back
    like = pm.Gamma('like',alpha=10./2., beta=1./(2*model), observed=p_bin)
    t_proper = pm.sample(tune=2000, chains=4)
```

    Auto-assigning NUTS sampler...
    Initializing NUTS using jitter+adapt_diag...
    Multiprocess sampling (4 chains in 4 jobs)
    NUTS: [back]
    Sampling 4 chains: 100%|██████████| 10000/10000 [00:02<00:00, 3599.66draws/s]

```python
pm.summary(t_proper, var_names=['back']);
pm.traceplot(t_proper, var_names=['back']);
```

```python
resb = np.median(t_proper['back'])
mod = np.ones(len(p_bin)) * resb

with plt.style.context(ms.ms):
    sns.distplot(p_bin/mod, label='Data / Model')
    sns.distplot(np.random.chisquare(10., size=npts), label=r'$\chi^2_{10}$')
    sns.distplot(np.random.gamma(10./2., (2*mod)), label=r'$\gamma(\frac{10}{2}, \frac{1}{M})$')
    plt.legend(fontsize=20)
    plt.xlabel('Power')
    plt.ylabel('Normalised N')
```

So despite the values in our binned spectrum being *nowhere* near a value of $1$, we have succesfully recovered the background of our *unbinned* spectrum by properly treating the noise statistics.

Naturally, this applies to any model you try to fit to a power spectrum. Try going through the notebook yourself after adding some signal to the flux, and fitting it in frequency space!

```python

```


```python
# Sympy Symbolic matrix inverse for the Polar Overturning Circulation model.
# twnh Jan, Feb 20
# SPENT A LONG TIME BUG HUNTING TO FIND PYTHON STARTS ARRAYS AT ELEMENT 0 NOT 1!!!!
```

```python
from numpy import *
from sympy import *
import sympy.matrices.matrices
init_printing()
import sympy
import math
from mpmath import mp
from sympy.abc import rho

rho_0, rho_1, rho_2, rho_3, rho_i, rho_s, rho_a, rho = symbols('rho_0 rho_1 rho_2 rho_3 rho_i rho_s rho_a rho', real=True)
Q, Q_m, Q_p, Q_i, F, U_1, U_2, U_3, U_i, u_s, S_s, T_f, T_1, T_2, T_3, T_a, T, S_1, S_2, S_3, S_a, S_i, S, Sstar = symbols('Q Q_m Q_p Q_i F U_1 U_2 U_3 U_i u_s S_s T_f T_1 T_2 T_3 T_a T S_1 S_2 S_3 S_a S_i S Sstar', real=True)
c_p, L, phi, Phi, gamma, DeltaS = symbols('c_p L phi Phi gamma DeltaS', real=True)
X = symbols('X', real=True)
```

```python
E = Matrix([[rho_2, rho_3, rho_i],[rho_2*S_2, rho_3*S_3, rho_i*S_i],[c_p*rho_2*T_f, c_p*rho_3*T_3, -rho_i*L]])
print("Matrix E:")
display(E)
Einv = simplify(MatrixBase.inverse_LU(E))

print(" ")
print("E inverse:")
display(Einv)

print(" ")
print("E determinant:")
Edet = simplify(MatrixBase.det(E))
display(Edet)

print(" ")
print("T3 formula for determinant=0:")
T3_formula = solve(Eq(0,Edet),T_3)
T3_formula = Eq(T_3,collect(T3_formula[0],S_3))
display(T3_formula)

print(" ")
print("Right hand side:")
rhs = Matrix([ [F - rho_1*U_1],  [- rho_1*U_1*S_1], [Q - c_p*rho_1*U_1*T_1]])
display(rhs)

print(" ")
print("------------------------------------------------------------------------------------------------------------")
print(" ")
print("Formula for U_2:")
soln = Einv*rhs
myeqn = factor(simplify(Eq(U_2,soln[0])))
display(myeqn)

print(" ")
print("Q formula in U_2=0")
myeqn = factor(simplify(Eq(0,soln[0])))
Qm_formula = solve(myeqn,Q)
Qm_formula = simplify(Qm_formula[0])
Qm_formula = Eq(Q_m,Qm_formula)
display(Qm_formula)

print(" ")
print("T3 formula in U_2=0")
myeqn = solve(myeqn,T_3)
T3_formula = Eq(T_3,collect(myeqn[0],S_3))
display(T3_formula)

print(" ")
print("T3 formula in U_2=0 intercepts T_f at S*")
myeqn = T3_formula.subs(T_3,T_f)
Sstar_formula = solve(myeqn,S_3)
Sstar_formula = Eq(Sstar,Sstar_formula[0])
display(Sstar_formula)

print(" ")
print("T3 formula in U_2=-X (e.g., X = U_1)")
myeqn = factor(simplify(Eq(-X,soln[0])))
myeqn = solve(myeqn,T_3)
T3_formula = Eq(T_3,collect(myeqn[0],S_3))
display(T3_formula)

print(" ")
print("------------------------------------------------------------------------------------------------------------")
print(" ")
print("Formula for U_3:")
myeqn = factor(simplify(Eq(U_3,soln[1])))
display(myeqn)

print(" ")
print("Q formula for U_3=0")
myeqn = factor(simplify(Eq(0,soln[1])))
Qp_formula = solve(myeqn,Q)
Qp_formula = simplify(Qp_formula[0])
Qp_formula = Eq(Q_p,Qp_formula)
display(Qp_formula)

print(" ")
print("S_2 formula for U_3=0")
S2_formula = solve(myeqn,S_2)
S2_formula = simplify(S2_formula[0])
S2_formula = Eq(S_2,S2_formula)
display(S2_formula)

print(" ")
print("S_2 - Sstar formula (checks proposition above)")
S2mSstar_formula = simplify(S2_formula.rhs-Sstar_formula.rhs)
S2mSstar_formula = Eq(S_2-Sstar,S2mSstar_formula)
display(S2mSstar_formula)

print(" ")
print("T3 formula in U_3=0: Doesn't exist because numerator doesn't depend on T_3")

print(" ")
print("T3 formula in U_3=-X (e.g., X = U_1)")
myeqn = factor(simplify(Eq(-X,soln[1])))
myeqn = solve(myeqn,T_3)
T3_formula = Eq(T_3,collect(myeqn[0],S_3))
display(T3_formula)

print(" ")
print("------------------------------------------------------------------------------------------------------------")
print(" ")
print("Formula for U_i:")
myeqn = factor(simplify(Eq(U_i,soln[2])))
display(myeqn)

print(" ")
print("Q formula for U_i=0")
myeqn = factor(simplify(Eq(0,soln[2])))
Qi_formula = solve(myeqn,Q)
Qi_formula = simplify(Qi_formula[0])
Qi_formula = Eq(Q_i,Qi_formula)
display(Qi_formula)

print(" ")
print("T3 formula in U_i=0")
myeqn = solve(myeqn,T_3)
T3_formula = collect(myeqn[0],S_3)
T3_formula = Eq(T_3,T3_formula)
display(T3_formula)

print(" ")
print("T3 formula in U_i=-X (e.g., X = U_1)")
myeqn = factor(simplify(Eq(-X,soln[2])))
myeqn = solve(myeqn,T_3)
T3_formula = Eq(T_3,collect(myeqn[0],S_3))
display(T3_formula)
```

```python
print(" ")
print("T3 formula in U_i=0 for S_3 = S_2")
T3_formula = simplify(T3_formula.subs(S_3,S_2))
display(T3_formula)
```

```python
# Approximate solution for U_1 when U_2=0
# Set all densities the same and divide them out.
# Set Tf = S_i = 0
print("Approximate matrix E2:")
E2 = Matrix([[ 1, 1, 1],[S_2, S_3, 0],[0, 0, -L]])
display(E2)

print(" ")
print("Approximate inverse matrix E2:")
E2inv = simplify(MatrixBase.inverse_LU(E2))
display(E2inv)

print(" ")
print("Approximate right hand side:")
rhs2 = Matrix([ [F/rho_0 - U_1],  [- U_1*S_1], [Q/rho_0 - c_p*U_1*T_1]])
display(rhs2)

print(" ")
print("Approximate formula for U_2:")
soln2 = E2inv*rhs2
myeqn2 = simplify(Eq(U_2,soln2[0]))
display(myeqn2)

print(" ")
print("Approximate Q formula in U_2:")
Q_formula2 = solve(myeqn2,Q)
display(Q_formula2)

print(" ")
print("Approximate U_1 formula in U_2:")
U1_formula2 = solve(myeqn2,U_1)
display(U1_formula2)
```

```python
print("Approximate solution should give identical results to before:")
# Tinkering, even more.
# Set all densities the same and divide them out.
# Set Tf = S_i = 0
# This means we can write the problem as a 2x2 matrix:
E3 = Matrix([[ 1, 1],[S_2, S_3]])
print("Approximate matrix E3:")
display(E3)

print(" ")
print("Approximate inverse matrix E3:")
E3inv = MatrixBase.inverse_LU(E3)
display(E3inv)

print(" ")
print("Approximate right hand side:")
rhs3 = Matrix([ [(1/(rho_0*L))*(Q + F*L) - U_1*(1+c_p*T_1/L)],  [- U_1*S_1]])
display(rhs3)

print(" ")
print("Approximate U_2 solution:")
soln3 = E3inv*rhs3
myeqn3 = Eq(U_2,soln3[0])
display(simplify(myeqn3))
```

```python
# Singular vector for the shelf matrix system
E4 = Matrix([[ rho_1, rho_i],[rho_1*S_1, rho_i*S_i],[c_p*rho_1*T_1,-rho_i*L]])
print("Shelf system matrix E4:")
display(E4)

# nullspace of E4 transpose is outside the range of E4:
print(" ")
print("Column null space:")
column_null_space = E4.T.nullspace()[0]
display(column_null_space)

# Form linear relation between F and Q
print(" ")
print("Right hand side:")
rhs4 = Matrix([ [F - rho_s*u_s],  [- rho_s*u_s*S_s], [Q - c_p*rho_s*u_s*T_f]])
display(rhs4)
print(" ")
print("Formula for F (look for Q multiplier for s_3/s_1 formula in supplement section 1):")
tmp = (rhs4.T)*column_null_space
myeqn4 = Eq(tmp[0],0)
tmp2 = solve(myeqn4,F)
display(simplify(Eq(tmp2[0],F)))
```


# Finding constants using fsolve

Problem: find $k$ and $c$ given

$\begin{align}
  1 + \sinh^{-1}1 &= k \sinh^{-1}(1/k) + c \;,  \\
  1 + \sinh^{-1}5 &= k \sinh^{-1}(5/k) + c \;.
\end{align}$

Plan: use `fsolve` from `scipy.optimize`.

```python
import numpy as np
from scipy.optimize import fsolve
```

```python
def func(x):
    """Function of x = (k, c) defined so that when each component is zero we
        have our solution.
       No extra arguments need to be passed, so func is simple."""
    k, c = x
    return (
            1. + np.arcsinh(1.) - (k * np.arcsinh(1./k) + c),
            1. + np.arcsinh(5.) - (k * np.arcsinh(5./k) + c)
           )
```

```python
x0 = (0.1, 0.1)   # guesses for k and c
k, c = fsolve(func, x0)
print(f'k = {k:0.2f}, c = {c:0.2f}')
```

    k = 1.00, c = 1.00

```python

```


```python
import sympy as sp
```

```python
def Newton_1D_step(x_prev, f_str, var_str):
    '''
    Given x_k, string representation of function, and string representation of variable
    Return the numerical value of x_{k+1}
    '''
    f = sp.sympify(f_str)
    var = sp.symbols(var_str)
    df = sp.diff(f, var)
    x_next = x_prev - f.subs(var, x_prev) / df.subs(var, x_prev)
    return float(x_next)
```

```python
# Test Case: Newton_1D_step generic test
err_bound = 10**-7
expected = 0.113966093691
actual = Newton_1D_step(-2, 'sin(x/5)', 'x')
assert abs(expected - actual) < err_bound
```

```python
def Newton_1D_step_aux(x_prev, f, df, var):
    '''
    Given x_k, function, differentiation of the function, and the variable
    Return the numerical value of x_{k+1}
    Operate as a lighter weighted version of Newton_1D_step
    '''
    return float(x_prev - f.subs(var, x_prev) / df.subs(var, x_prev))

def Newton_1D(x_0, f_str, var_str, err_bound):
    '''
    Given initial value x_0, string representation of function, string representation of variable
    as well as the error bound
    Return the number of iterations it takes to get to the error bound
    And the final value
    '''
    f = sp.sympify(f_str)
    var = sp.symbols(var_str)
    df = sp.diff(f, var)
    x_curr = x_0
    iterations = 0
    while abs(float(f.subs(var, x_curr))) > err_bound:
        x_curr = Newton_1D_step_aux(x_curr, f, df, var)
        iterations += 1
    return iterations, x_curr
```

```python
# Test Case: Newton_1D generic iteration test
err_bound = 10**-7
it, res = Newton_1D(4.4, 'x*E**x', 'x', err_bound)
assert it == 10
```

```python
def secant_1D_step(x_prev0, x_prev1, f_str, var_str):
    '''
    Given x_{k-1}, x_{k}, string representation of function, and string representation of variable
    Return the numerical value of x_{k+1}
    '''
    f = sp.sympify(f_str)
    var = sp.symbols(var_str)
    x_next = x_prev1 - f.subs(var, x_prev1) / ((f.subs(var, x_prev0) - f.subs(var, x_prev1)) / (x_prev0 - x_prev1))
    return float(x_next)
```

```python
# Test Case: secant_1D_step generic test
err_bound = 10**-7
expected = -0.0134679226308
actual = secant_1D_step(1, -2, 'sin(x/5)', 'x')
assert abs(expected - actual) < err_bound
```

```python
def bisection_1D_step(left, right, f_str, var_str):
    f = sp.sympify(f_str)
    var = sp.symbols(var_str)
    mid = (left + right) / 2
    if float(f.subs(var, left) * f.subs(var, mid)) < 0:
        right = mid
    else:
        left = mid
    return left, right
```

```python
expected = (-1.0, 0.5)
temp = bisection_1D_step(-4, 2, 'sin(x/5)', 'x')
actual = bisection_1D_step(temp[0], temp[1], 'sin(x/5)', 'x')
assert expected == actual
```

```python
# T14-HW14.5 variation 1
f_str = 'sin(x/5)'
var_str = 'x'
newton_res = Newton_1D_step(2, f_str, var_str)
print(newton_res)
secant_res = secant_1D_step(1, 2, f_str, var_str)
print(secant_res)
temp = bisection_1D_step(-1, 2, f_str, var_str)
bisection_res = bisection_1D_step(temp[0], temp[1], f_str, var_str)
print(bisection_res)
```

    -0.1139660936908088
    -0.0415222035418395
    (-0.25, 0.5)

```python
# T14-HW14.5 variation 2
f_str = 'sin(x/3)'
var_str = 'x'
newton_res = Newton_1D_step(1, f_str, var_str)
print(newton_res)
secant_res = secant_1D_step(2, 1, f_str, var_str)
print(secant_res)
temp = bisection_1D_step(-1, 4, f_str, var_str)
bisection_res = bisection_1D_step(temp[0], temp[1], f_str, var_str)
print(bisection_res)
```

    -0.03876064853172647
    -0.12370422383815974
    (-1, 0.25)

```python
# T14-HW14.3 variation 1
Newton_1D(4, 'x*E**x', 'x', 10**-5)
```

    (9, 1.5586599258811135e-06)


# Coupled ODE Integration

This notebook demonstrates integration of coupled ODEs and numerical integration of arrays of values in Python using a physically motivated example.

## A Ballistic Trajectory with Drag

The force on an object (a golfball or cannonball) moving in a uniform gravitation field with air resistance is given by

$$
\vec{F} = -mg\hat{z}-\frac{1}{2}\rho C_d Av^2 \hat{v},
$$

where gravity acts in the $\hat{z}$ direction, $\rho$ is the density of air, $C_d$ is the drag coefficient of the object in air, and $A$ is its cross-sectional area. $C_d$ depends heavily on whether or not the air flow in the wake of the object is laminar or turbulent, and thus depends on both the speed and shape of the object.

Writing this expression in terms of the object's position vector $\vec{r}$ we have

$$
\begin{align}
m\ddot{\vec{r}} &= -mg\hat{z} -\frac{1}{2}\rho C_d A|\dot{\vec{r}}|^2 \frac{\dot{\vec{r}}}{|\dot{\vec{r}}|} \\
 \frac{d}{dt}\dot{\vec{r}} &= -g\hat{z} - \alpha|\dot{\vec{r}}|\dot{\vec{r}},
\end{align}
$$

where $\alpha=1/(2m)~\rho C_d A$. Without loss of generality, restrict the motion to the $xz$ plane. Then the expression above can be written as two coupled first-order ODEs in velocity:

$$
\begin{align}
 \frac{d}{dt}\dot{x} &= -\alpha\dot{x}\sqrt{\dot{x}^2 + \dot{z}^2},
 &
 \frac{d}{dt}\dot{z} &= -mg -\alpha\dot{z}\sqrt{\dot{x}^2 + \dot{z}^2}.
\end{align}
$$

## Numerical Solution

In this example, we use the `odeint` function from `scipy.integrate` to solve for the components of the velocity $\dot{x}$ and $\dot{z}$. Then we can use the trapezoidal rule to estimate the trajectory $x(t)$ and $z(t)$.

```python
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from scipy.integrate import odeint, cumtrapz

mpl.rc('font', size=14)
```

```python
g = 9.8

def ballistic(V, t, m, A, rho, Cd):
    """Velocity equations for ballistic motion with drag.
    Assume SI units for all inputs.

    Parameters
    ----------
    V : list
        Velocity in x and z.
    t : float
        Time point used to solve for v.
    m : float
        Object mass [kg].
    A : float
        Object area [m2].
    rho : float
        Air density [kg/m3].
    Cd : float
        Drag coefficient.

    Returns
    -------
    dVdt : list
        Velocity time derivative along x and z axes.
    """
    vx, vz = V
    alpha = rho*Cd*A/(2*m)
    vmag = np.sqrt(vx**2 + vz**2)
    dVdt = [   -alpha*vx*vmag,
            -g -alpha*vz*vmag]
    return dVdt
```

## Initial Conditions

Choose a $45^\circ$ initial trajectory and an initial velocity of 40 m/s or about 90 mph. Then integrate the motion for 6 seconds.

The object properties and drag coefficients are roughly those of a baseball.

```python
v0 = 40.
angle = 45*np.pi/180.
V0 = [v0*np.cos(angle), v0*np.sin(angle)]

t = np.linspace(0, 6., 1001)

# Object properties and drag coefficients.
m = 0.15 # kg
rho = 1.225 # kg/m3
Cd = 0.5
r = 0.0366
A = np.pi * r**2 # m2
```

## Solve for the Motion

Consider several cases:
1. No drag (vacuum).
2. Drag, using sea-level air density.
3. Drag, using air density at 1.4 km.
4. Drag, using air density at 4 km.

```python
# Solve for vx, vz with no drag.
# Then numerically integrate vx, vz to get x(t) and z(t).
v = odeint(ballistic, V0, t, args=(m, A, rho, 0.)).T
x_vac, z_vac = [cumtrapz(v[i], t) for i in range(2)]

# Solve with drag.
# Numerically integrate vx, vz to get x(t) and z(t).
v = odeint(ballistic, V0, t, args=(m, A, rho, Cd)).T
x_air, z_air = [cumtrapz(v[i], t) for i in range(2)]

# Solve with drag, using high-altitude air density (~1.4 km up).
# Numerically integrate vx, vz to get x(t) and z(t).
v = odeint(ballistic, V0, t, args=(m, A, 1.069, Cd)).T
x_alt, z_alt = [cumtrapz(v[i], t) for i in range(2)]

# Solve with drag, using very high-altitude air density (~4 km).
# Numerically integrate vx, vz to get x(t) and z(t).
v = odeint(ballistic, V0, t, args=(m, A, 0.81, Cd)).T
x_ha, z_ha = [cumtrapz(v[i], t) for i in range(2)]
```

```python
fig, ax = plt.subplots(1,1, figsize=(10,4), tight_layout=True)
ax.plot(x_vac, z_vac, 'k:', label='vacuum')
ax.plot(x_air, z_air, 'k', label=r'$\rho=1.225$ kg m$^{-3}$')
ax.plot(x_alt, z_alt, 'k--', label=r'$\rho=1.069$ kg m$^{-3}$')
ax.plot(x_ha,  z_ha,  'k-.', label=r'$\rho=0.819$ kg m$^{-3}$')

ax.set(aspect='equal',
       xlabel='$x$ [m]',
       ylim=(0,1.3*np.max(z_vac)),
       ylabel='$z$ [m]')

leg = ax.legend(fontsize=11)
```


## Motivation
The motivation for writing a short notebook on the auto-regression
concept is that lots of mathetmatical analysis is applied in the financial
world, on stock prices, valuations etc.

Fundamentally what is not understood very well is that stock prices fluctuate, and not in a random way. Generally there tends to be a **drift** over time, and I'd like to show that this drift is something called stationarity.

## Auto-regression

An auto-regressive, **AR(1)**, process is one that is not always stationary and may contain a unit root.

For an AR process to be stationary, $ |\rho| < 1 $:

\begin{align}
    X_{t} &= X_{t-1} + \epsilon_{t}  \\
            &= X_{t-2} + \epsilon_{t-1} + \epsilon_{t} \\
            &= X_{0} + \sum_{i=0}^{t-1} \epsilon_{t-i}
\end{align}
for $ \epsilon_{t} \sim iid(0,1) $ where $ X_{0}$ is a constant.

Further attributes are as follows:

\begin{align}
            E(X_{t}) &=  E(X_{0}) + E \sum_{i=0}^{t-1} \epsilon_{t-i} \\
                    &= E(X_{0})
\end{align}

since $ \epsilon_{t} \sim iid(0,1) $ and $ \therefore  E(X_{t}) = 0 $, $ Var(\epsilon_{t}) = \sigma^2 $,
so the random noise has zero mean.

\begin{align}
    Var(X_{t}) &=  \sum_{i=0}^{t-1} Var ( \epsilon_{t-i}) \\
                 &=  t \sigma^2   \\
                 &= f(t)
\end{align}
since $ Var(\epsilon_{t}) = \sigma^2$. Therefore, the variance is a function of time, so it is non-stationary.
\\
Therefore a random walk is a non-stationary time series, it is a function of time.
There is no covarance ($\epsilon_{t}$ terms) since $\epsilon_{t}$ are
independent of one another.

\begin{align}
    Cov(X_{t}, X_{t+h}) &= Cov(X_{t}, X_{t} + \sum_{i=0}^{h-1} \epsilon_{t+h-i} ) \\
            &= Cov(X_{t}, X_{t})
            &= Var(X_{t})
\end{align}
given $X_{t+h} = X_{t} + \sum_{i=0}^{h-1} \epsilon_{t+h-i}$, and since $\epsilon_{t}$ are independent, then $Cov(X_{t}, \epsilon_{t_{k}}) = 0$.

Therefore, a random walk is non-stationary, since the variance
increases with time, and the covariance also increases with time.


```python
%matplotlib notebook
from sympy import *
import numpy as np
import matplotlib.pyplot as plt
init_printing()
```

```python
var("x y t")
```

```python
f = x**3 - 3*x**2 - 24*x + 32
plot(f)
```

    <IPython.core.display.Javascript object>

    <sympy.plotting.plot.Plot at 0x1efcef01358>

```python
f = x**3 - 3*x**2 - 24*x + 32
df = f.diff()
ddf = df.diff()
df,ddf
```

```python
pc = solve(df)
pc
```

```python
f=x**2 + y**2
dfx = f.diff(x)
dfy = f.diff(y)

pc = solve( [dfx, dfy],[x,y])

dfx,dfy,pc
```

```python
dfxx = dfx.diff(x)
dfxy = dfx.diff(y)
dfyx = dfy.diff(x)
dfyy = dfy.diff(y)
D = dfxx * dfyy - dfxy
D.subs(pc),dfxx.subs(pc)
```

```python
!start .
```

```python

```


```
#interpolación Lineal
!pip install scipy
import numpy as np
import matplotlib.pyplot as plt
```

    Requirement already satisfied: scipy in /usr/local/lib/python3.6/dist-packages (1.4.1)
    Requirement already satisfied: numpy>=1.13.3 in /usr/local/lib/python3.6/dist-packages (from scipy) (1.18.5)

```
from scipy.interpolate import interp1d

x = np.linspace(1, 6, num=4, endpoint=True)
print(x)
#y = np.cos(-x**2/9.0)
y = np.log(x)
f = interp1d(x, y) #interpolación Lineal
f2 = interp1d(x, y, kind='cubic') #interpolación cubica

xnew = np.linspace(1, 6, num=41, endpoint=True)

plt.plot(x, y, 'o', xnew, f(xnew), '-', xnew, f2(xnew), '--')
plt.legend(['data', 'linear', 'cubic'], loc='best')
plt.show()
```

```
from scipy.interpolate import interp1d
x = np.linspace(0, 10, num=11, endpoint=True)
y = np.cos(-x**2/9.0)
f = interp1d(x, y)
f2 = interp1d(x, y, kind='cubic')

xnew = np.linspace(0, 10, num=41, endpoint=True)
import matplotlib.pyplot as plt
plt.plot(x, y, 'o', xnew, f(xnew), '-', xnew, f2(xnew), '--')
plt.legend(['data', 'linear', 'cubic'], loc='best')
plt.show()
```

```
# Interpolacion de Lagrange
# http://blog.espol.edu.ec/analisisnumerico/interpolacion-de-lagrange/
# divisoresL solo para mostrar valores
import numpy as np
import sympy as sym
import matplotlib.pyplot as plt

# INGRESO , Datos de prueba
xi = np.array([0, 0.2, 0.3, 0.4])
fi = np.array([1, 1.6, 1.7, 2.0])

# PROCEDIMIENTO
# Polinomio de Lagrange
n = len(xi)
x = sym.Symbol('x')
polinomio = 0
divisorL = np.zeros(n, dtype = float)
for i in range(0,n):
    # Termino de Lagrange
    numerador = 1
    denominador = 1
    for j  in range(0,n):
        if (j!=i):
            numerador = numerador*(x-xi[j])
            denominador = denominador*(xi[i]-xi[j])
    terminoLi = numerador/denominador
    polinomio = polinomio + terminoLi*fi[i]
    divisorL[i] = denominador
    # simplifica el polinomio
    polisimple = polinomio.expand()
    # para evaluación numérica
    px = sym.lambdify(x,polisimple)

    # Puntos para la gráfica
    muestras = 101
    a = np.min(xi)
    b = np.max(xi)
    pxi = np.linspace(a,b,muestras)
    pfi = px(pxi)

    # Salida por
    print("********************************")
    print("ITERACION ",i+1)
    print("********************************")
    print('Valores de fi: ',fi)
    print('Divisores en L(i): ',divisorL)
    print()
    print('Polinomio de Lagrange, expresiones')
    print(polinomio)
    print()
    print('Polinomio de Lagrange: ')
    print(polisimple)

    # Gráfica

    plt.plot(pxi,pfi, label = 'Polinomio '+str(i+1))
    plt.legend()
    plt.xlabel('xi')
    plt.ylabel('fi')
    plt.title('Interpolación Lagrange')

plt.plot(xi,fi,'o', label = 'Puntos DE')
plt.show()
print("_________________________________________")
print("Siendo polinomio 4 el polinomio final")
print("_________________________________________")
```

```
# Interpolacion de Lagrange
# http://blog.espol.edu.ec/analisisnumerico/interpolacion-de-lagrange/
# divisoresL solo para mostrar valores
import numpy as np
import sympy as sym
import matplotlib.pyplot as plt

# INGRESO , Datos de prueba
xi = np.array([0, 0.2, 0.3, 0.4])
fi = np.array([1, 1.6, 1.7, 2.0])

# PROCEDIMIENTO
# Polinomio de Lagrange
n = len(xi)
x = sym.Symbol('x')
polinomio = 0
divisorL = np.zeros(n, dtype = float)
for i in range(0,n):
    # Termino de Lagrange
    numerador = 1
    denominador = 1
    for j  in range(0,n):
        if (j!=i):
            numerador = numerador*(x-xi[j])
            denominador = denominador*(xi[i]-xi[j])
    terminoLi = numerador/denominador
    polinomio = polinomio + terminoLi*fi[i]
    divisorL[i] = denominador
    # simplifica el polinomio
    polisimple = polinomio.expand()
    # para evaluación numérica
    px = sym.lambdify(x,polisimple)

    # Puntos para la gráfica
    muestras = 101
    a = np.min(xi)
    b = np.max(xi)
    pxi = np.linspace(a,b,muestras)
    pfi = px(pxi)

    # Salida por
    print("********************************")
    print("ITERACION ",i+1)
    print("********************************")
    print('Valores de fi: ',fi)
    print('Divisores en L(i): ',divisorL)
    print()
    print('Polinomio de Lagrange, expresiones')
    print(polinomio)
    print()
    print('Polinomio de Lagrange: ')
    print(polisimple)

    # Gráfica

    plt.plot(pxi,pfi, label = 'Polinomio '+str(i+1))
    plt.legend()
    plt.xlabel('xi')
    plt.ylabel('fi')
    plt.title('Interpolación Lagrange')

    plt.plot(xi,fi,'o', label = 'Puntos DE')
    plt.show()
print("_________________________________________")
print("Siendo polinomio 4 el polinomio final")
print("_________________________________________")
```


# Multiclass classification

The multiclass classification problem is a regression problem from an input $x \in {\cal X}$ to discrete labels $y\in {\cal Y}$, where ${\cal Y}$ is a discrete set of size $C$ bigger than two (for $C=2$ it is the more usual binary classification).

Labels are encoded in a one-hot fashion, that is if $C=4$ and $y=2$, we note $\bar{y} = [0,1,0,0]$.

The generative model for this problem consists of:

* $C$ latent functions $\mathbf{f} = [f_1,...,f_C]$ with an independent Gaussian Process prior
* a deterministic function that builds a discrete distribution $\pi(\mathbf{f}) = [\pi_1(f_1),...,\pi_C(f_C)]$ from the latents such that $\sum_c \pi_c(f_c) = 1$
* a discrete likelihood $p(y|\mathbf{f}) = Discrete(y;\pi(\mathbf{f})) = \prod_c \pi_c(f_c)^{\bar{y}_c}$

A typical example of $\pi$ is the softmax function:

\begin{equation}
\pi_c (f_c) \propto \exp( f_c)
\end{equation}

Another convenient one is the robust max:
\begin{equation}
\pi_c(\mathbf{f}) = \begin{cases} 1 - \epsilon, & \mbox{if } c = \arg \max_c f_c \\
 \epsilon /(C-1), & \mbox{ otherwise} \end{cases}
\end{equation}

```python
import numpy as np
import tensorflow as tf

import warnings

warnings.filterwarnings("ignore")  # ignore DeprecationWarnings from tensorflow

import matplotlib.pyplot as plt

%matplotlib inline

import gpflow

from gpflow.utilities import print_summary, set_trainable
from gpflow.ci_utils import ci_niter

from multiclass_classification import plot_posterior_predictions, colors

# reproducibility:
np.random.seed(0)
tf.random.set_seed(123)
```

## Sampling from the GP multiclass generative model

### Declaring model parameters and input

```python
# Number of functions and number of data points
C = 3
N = 100

# Lengthscale of the SquaredExponential kernel (isotropic -- change to `[0.1] * C` for ARD)
lengthscales = 0.1

# Jitter
jitter_eye = np.eye(N) * 1e-6

# Input
X = np.random.rand(N, 1)
```

### Sampling

```python
# SquaredExponential kernel matrix
kernel_se = gpflow.kernels.SquaredExponential(lengthscales=lengthscales)
K = kernel_se(X) + jitter_eye

# Latents prior sample
f = np.random.multivariate_normal(mean=np.zeros(N), cov=K, size=(C)).T

# Hard max observation
Y = np.argmax(f, 1).reshape(-1,).astype(int)

# One-hot encoding
Y_hot = np.zeros((N, C), dtype=bool)
Y_hot[np.arange(N), Y] = 1

data = (X, Y)
```

### Plotting

```python
plt.figure(figsize=(12, 6))
order = np.argsort(X.reshape(-1,))

for c in range(C):
    plt.plot(X[order], f[order, c], ".", color=colors[c], label=str(c))
    plt.plot(X[order], Y_hot[order, c], "-", color=colors[c])

plt.legend()
plt.xlabel("$X$")
plt.ylabel("Latent (dots) and one-hot labels (lines)")
plt.title("Sample from the joint $p(Y, \mathbf{f})$")
plt.grid()
plt.show()
```

## Inference

Inference here consists of computing the posterior distribution over the latent functions given the data $p(\mathbf{f}|Y, X)$.

You can use different inference methods. Here we perform variational inference.
For a treatment of the multiclass classification problem using MCMC sampling, see [Markov Chain Monte Carlo (MCMC)](../advanced/mcmc.ipynb).

### Approximate inference: Sparse Variational Gaussian Process

#### Declaring the SVGP model (see [GPs for big data](../advanced/gps_for_big_data.ipynb))

```python
# sum kernel: Matern32 + White
kernel = gpflow.kernels.Matern32() + gpflow.kernels.White(variance=0.01)

# Robustmax Multiclass Likelihood
invlink = gpflow.likelihoods.RobustMax(C)  # Robustmax inverse link function
likelihood = gpflow.likelihoods.MultiClass(3, invlink=invlink)  # Multiclass likelihood
Z = X[::5].copy()  # inducing inputs

m = gpflow.models.SVGP(
    kernel=kernel,
    likelihood=likelihood,
    inducing_variable=Z,
    num_latent_gps=C,
    whiten=True,
    q_diag=True,
)

# Only train the variational parameters
set_trainable(m.kernel.kernels[1].variance, False)
set_trainable(m.inducing_variable, False)
print_summary(m, fmt="notebook")
```

name                               	class    	transform  	prior  	trainable  	shape  	dtype  	value

SVGP.kernel.kernels[0].variance    	Parameter	Softplus   	       	True       	()     	float64	1.0

SVGP.kernel.kernels[0].lengthscales	Parameter	Softplus   	       	True       	()     	float64	1.0

SVGP.kernel.kernels[1].variance    	Parameter	Softplus   	       	False      	()     	float64	0.009999999999999893

SVGP.likelihood.invlink.epsilon    	Parameter	Sigmoid    	Beta   	False      	()     	float64	0.0010000000000000005

SVGP.inducing_variable.Z           	Parameter	           	       	False      	(20, 1)	float64	[[0.5488135...

SVGP.q_mu                          	Parameter	           	       	True       	(20, 3)	float64	[[0., 0., 0....

SVGP.q_sqrt                        	Parameter	Softplus   	       	True       	(20, 3)	float64	[[1., 1., 1....

#### Running inference

```python
opt = gpflow.optimizers.Scipy()

opt_logs = opt.minimize(
    m.training_loss_closure(data), m.trainable_variables, options=dict(maxiter=ci_niter(1000))
)
print_summary(m, fmt="notebook")
```

    WARNING:tensorflow:From /usr/local/lib/python3.6/site-packages/tensorflow/python/ops/math_grad.py:297: setdiff1d (from tensorflow.python.ops.array_ops) is deprecated and will be removed after 2018-11-30.
    Instructions for updating:
    This op will be removed after the deprecation date. Please switch to tf.sets.difference().

name                               	class    	transform  	prior  	trainable  	shape  	dtype  	value

SVGP.kernel.kernels[0].variance    	Parameter	Softplus   	       	True       	()     	float64	131462.1947347722

SVGP.kernel.kernels[0].lengthscales	Parameter	Softplus   	       	True       	()     	float64	0.174334443990939

SVGP.kernel.kernels[1].variance    	Parameter	Softplus   	       	False      	()     	float64	0.009999999999999893

SVGP.likelihood.invlink.epsilon    	Parameter	Sigmoid    	Beta   	False      	()     	float64	0.0010000000000000005

SVGP.inducing_variable.Z           	Parameter	           	       	False      	(20, 1)	float64	[[0.5488135...

SVGP.q_mu                          	Parameter	           	       	True       	(20, 3)	float64	[[-0.23232554, 0.61403875, -0.38171321...

SVGP.q_sqrt                        	Parameter	Softplus   	       	True       	(20, 3)	float64	[[0.08257649, 0.07950594, 0.12076009...

```python
plot_posterior_predictions(m, X, Y)
```


Source: https://github.com/alsnhll

## Model

### Equations

\begin{equation}
\begin{split}
\dot{S} &= -\beta_1 I_1 S -\beta_2 I_2 S - \beta_3 I_3 S\\
\dot{E} &=\beta_1 I_1 S +\beta_2 I_2 S + \beta_3 I_3 S - a E \\
\dot{I_1} &= a E - \gamma_1 I_1 - p_1 I_1 \\
\dot{I_2} &= p_1 I_1 -\gamma_2 I_2 - p_2 I_2 \\
\dot{I_3} & = p_2 I_2 -\gamma_3 I_3 - \mu I_3 \\
\dot{R} & = \gamma_1 I_1 + \gamma_2 I_2 + \gamma_3 I_3 \\
\dot{D} & = \mu I_3
\end{split}
\end{equation}

### Variables
* $S$: Susceptible individuals
* $E$: Exposed individuals - infected but not yet infectious or symptomatic
* $I_i$: Infected individuals in severity class $i$. Severity increaes with $i$ and we assume individuals must pass through all previous classes
  * $I_1$: Mild infection (hospitalization not required)
  * $I_2$: Severe infection (hospitalization required)
  * $I_3$: Critical infection (ICU required)
* $R$: individuals who have recovered from disease and are now immune
* $D$: Dead individuals
* $N=S+E+I_1+I_2+I_3+R+D$ Total population size (constant)

### Parameters
* $\beta_i$ rate at which infected individuals in class $I_i$ contact susceptibles and infect them
* $a$ rate of progression from the exposed to infected class
* $\gamma_i$ rate at which infected individuals in class $I_i$ recover from disease and become immune
* $p_i$ rate at which infected individuals in class $I_i$ progress to class $I_{I+1}$
* $\mu$ death rate for individuals in the most severe stage of disease

### Basic reproductive ratio

Idea: $R_0$ is the sum of
1. the average number of secondary infections generated from an individual in stage $I_1$
2. the probability that an infected individual progresses to $I_2$ multiplied by the average number of secondary infections generated from an individual in stage $I_2$
3.  the probability that an infected individual progresses to $I_3$ multiplied by the average number of secondary infections generated from an individual in stage $I_3$

\begin{equation}
\begin{split}
R_0 & = N\frac{\beta_1}{p_1+\gamma_1} + \frac{p_1}{p_1 + \gamma_1} \left( \frac{N \beta_2}{p_2+\gamma_2} + \frac{p_2}{p_2 + \gamma_2} \frac{N \beta_3}{\mu+\gamma_3}\right)\\
&= N\frac{\beta_1}{p_1+\gamma_1} \left(1 + \frac{p_1}{p_2 + \gamma_2}\frac{\beta_2}{\beta_1} \left( 1 + \frac{p_2}{\mu + \gamma_3} \frac{\beta_3}{\beta_2} \right) \right)
\end{split}
\end{equation}

```
import numpy as np, matplotlib.pyplot as plt
from scipy.integrate import odeint
```

```
#Defining the differential equations

#Don't track S because all variables must add up to 1
#include blank first entry in vector for beta, gamma, p so that indices align in equations and code.
#In the future could include recovery or infection from the exposed class (asymptomatics)

def seir(y,t,b,a,g,p,u,N):
    dy=[0,0,0,0,0,0]
    S=N-sum(y);
    dy[0]=np.dot(b[1:3],y[1:3])*S-a*y[0] # E
    dy[1]= a*y[0]-(g[1]+p[1])*y[1] #I1
    dy[2]= p[1]*y[1] -(g[2]+p[2])*y[2] #I2
    dy[3]= p[2]*y[2] -(g[3]+u)*y[3] #I3
    dy[4]= np.dot(g[1:3],y[1:3]) #R
    dy[5]=u*y[3] #D

    return dy
```

```
# Define parameters based on clinical observations

#I will add sources soon
# https://github.com/midas-network/COVID-19/tree/master/parameter_estimates/2019_novel_coronavirus

IncubPeriod=5  #Incubation period, days
DurMildInf=10 #Duration of mild infections, days
FracMild=0.8  #Fraction of infections that are mild
FracSevere=0.15 #Fraction of infections that are severe
FracCritical=0.05 #Fraction of infections that are critical
CFR=0.02 #Case fatality rate (fraction of infections resulting in death)
TimeICUDeath=7 #Time from ICU admission to death, days
DurHosp=11 #Duration of hospitalization, days

```

```
# Define parameters and run ODE

N=1000
b=np.zeros(4) #beta
g=np.zeros(4) #gamma
p=np.zeros(3)

a=1/IncubPeriod

u=(1/TimeICUDeath)*(CFR/FracCritical)
g[3]=(1/TimeICUDeath)-u

p[2]=(1/DurHosp)*(FracCritical/(FracCritical+FracSevere))
g[2]=(1/DurHosp)-p[2]

g[1]=(1/DurMildInf)*FracMild
p[1]=(1/DurMildInf)-g[1]

#b=2e-4*np.ones(4) # all stages transmit equally
b=2.5e-4*np.array([0,1,0,0]) # hospitalized cases don't transmit

#Calculate basic reproductive ratio
R0=N*((b[1]/(p[1]+g[1]))+(p[1]/(p[1]+g[1]))*(b[2]/(p[2]+g[2])+ (p[2]/(p[2]+g[2]))*(b[3]/(u+g[3]))))
print("R0 = {0:4.1f}".format(R0))
```

    R0 =  2.5

```
print(b)
print(a)
print(g)
print(p)
print(u)
```

    [0.      0.00025 0.      0.     ]
    0.2
    [0.         0.08       0.06818182 0.08571429]
    [0.         0.02       0.02272727]
    0.057142857142857134

```
tmax=365
tvec=np.arange(0,tmax,0.1)
ic=np.zeros(6)
ic[0]=1

soln=odeint(seir,ic,tvec,args=(b,a,g,p,u,N))
soln=np.hstack((N-np.sum(soln,axis=1,keepdims=True),soln))

plt.figure(figsize=(13,5))
plt.subplot(1,2,1)
plt.plot(tvec,soln)
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000 People")
plt.legend(("S","E","I1","I2","I3","R","D"))
plt.ylim([0,1000])

#Same plot but on log scale
plt.subplot(1,2,2)
plt.plot(tvec,soln)
plt.semilogy()
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000 People")
plt.legend(("S","E","I1","I2","I3","R","D"))
plt.ylim([1,1000])
#plt.tight_layout()
```

```
# get observed growth rate r (and doubling time) for a particular variable between selected time points
#(all infected classes eventually grow at same rate during early infection)

#Don't have a simple analytic formula for r for this model due to the complexity of the stages

def growth_rate(tvec,soln,t1,t2,i):
  i1=np.where(tvec==t1)[0][0]
  i2=np.where(tvec==t2)[0][0]
  r=(np.log(soln[i2,1])-np.log(soln[i1,1]))/(t2-t1)
  DoublingTime=np.log(2)/r

  return r, DoublingTime
```

```
(r,DoublingTime)=growth_rate(tvec,soln,10,20,1)
print("The epidemic growth rate is = {0:4.2f} per day and the doubling time {1:4.1f} days ".format(r,DoublingTime))
```

    The epidemic growth rate is = 0.08 per day and the doubling time  9.0 days

### Repeat but with a social distancing measure that reduces transmission rate

```
bSlow=0.6*b
R0Slow=N*((bSlow[1]/(p[1]+g[1]))+(p[1]/(p[1]+g[1]))*(bSlow[2]/(p[2]+g[2])+ (p[2]/(p[2]+g[2]))*(bSlow[3]/(u+g[3]))))

solnSlow=odeint(seir,ic,tvec,args=(bSlow,a,g,p,u,N))
solnSlow=np.hstack((N-np.sum(solnSlow,axis=1,keepdims=True),solnSlow))

plt.figure(figsize=(13,5))
plt.subplot(1,2,1)
plt.plot(tvec,solnSlow)
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000 People")
plt.legend(("S","E","I1","I2","I3","R","D"))
plt.ylim([0,1000])

#Same plot but on log scale
plt.subplot(1,2,2)
plt.plot(tvec,solnSlow)
plt.semilogy()
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000 People")
plt.legend(("S","E","I1","I2","I3","R","D"))
plt.ylim([1,1000])

(rSlow,DoublingTimeSlow)=growth_rate(tvec,solnSlow,30,40,1)

plt.show()
print("R0 under intervention = {0:4.1f}".format(R0Slow))
print("The epidemic growth rate is = {0:4.2f} per day and the doubling time {1:4.1f} days ".format(rSlow,DoublingTimeSlow))
```

#### Compare epidemic growth with and without intervention

```
### All infectious cases (not exposed)

plt.figure(figsize=(13,5))
plt.subplot(1,2,1)
plt.plot(tvec,np.sum(soln[:,2:5],axis=1,keepdims=True))
plt.plot(tvec,np.sum(solnSlow[:,2:5],axis=1,keepdims=True))
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000 People")
plt.legend(("No intervention","Intervention"))
plt.ylim([0,1000])
plt.title('All infectious cases')
```

## COVID19 Cases vs Hospital Capacity

Depending on the severity ($I_i$) stage of COVID-19 infection, patients need different level of medical care.

Individuals in $I_1$ have "mild" infection, meaning they have cough/fever/other flu-like symptoms and may also have mild pneumonia. Mild pneumonia does not require hospitalization, although in many outbreak locations like China and South Korea all symptomatic patients are being hospitalized. This is likely to reduce spread and to monitor these patients in case they rapidly progress to worse outcome. However, it is a huge burden on the health care system.

Individuals in $I_2$ have "severe" infection, which is categorized medically as having any of the following: "dyspnea, respiratory frequency 30/min, blood oxygen saturation 93%, partial pressure of arterial oxygen to fraction of inspired oxygen ratio $<$300, lung infiltrates $>$50% within 24 to 48 hours". These individuals require hospitalization but can be treated on regular wards. They may require supplemental oxygen.

Individuals in $I_3$ have "critical" infection, which is categorized as having any of the following: "respiratory failure, septic shock, and/or multiple organ dysfunction or failure".
They require ICU-level care, generally because they need mechanical ventilation.

We consider different scenarios for care requirements. One variation between scenarios is whether we include hospitalization for all individuals or only those with severe or critical infection. Another is the care of critical patients. If ICUs are full, hospitals have protocols developed for pandemic influenza to provide mechanical ventilation outside regular ICU facility and staffing requirements. Compared to "conventional" ventilation protocols, there are "contingency" and "crisis" protocols that can be adopted to increase patient loads. These protocols involve increasing patient:staff ratios, using non-ICU beds, and involving non-critical care specialists in patient care.

```
#Parameter sources: https://docs.google.com/spreadsheets/d/1zZKKnZ47lqfmUGYDQuWNnzKnh-IDMy15LBaRmrBcjqE

# All values are adjusted for increased occupancy due to flu season

AvailHospBeds=2.6*(1-0.66*1.1) #Available hospital beds per 1000 ppl in US based on total beds and occupancy
AvailICUBeds=0.26*(1-0.68*1.07) #Available ICU beds per 1000 ppl in US, based on total beds and occupancy. Only counts adult not neonatal/pediatric beds
ConvVentCap=0.062 #Estimated excess # of patients who could be ventilated in US (per 1000 ppl) using conventional protocols
ContVentCap=0.15 #Estimated excess # of patients who could be ventilated in US (per 1000 ppl) using contingency protocols
CrisisVentCap=0.42 #Estimated excess # of patients who could be ventilated in US (per 1000 ppl) using crisis protocols

```

### Assumptions 1
* Only severe or critical cases go to the hospital
* All critical cases require ICU care and mechanical ventilation

```
NumHosp=soln[:,3]+soln[:,4]
NumICU=soln[:,4]

plt.figure(figsize=(13,4.8))
plt.subplot(1,2,1)
plt.plot(tvec,NumHosp)
plt.plot(np.array((0, tmax)),AvailHospBeds*np.ones(2),color='C0',linestyle=":")
plt.xlabel("Time (days)")
plt.ylabel("Number Per 1000 People")
plt.legend(("Cases Needing Hospitalization","Available Hospital Beds"))
ipeakHosp=np.argmax(NumHosp) #find peak
peakHosp=10*np.ceil(NumHosp[ipeakHosp]/10)#find time at peak
plt.ylim([0,peakHosp])

plt.subplot(1,2,2)
plt.plot(tvec,NumICU,color='C1')
plt.plot(np.array((0, tmax)),AvailICUBeds*np.ones(2),color='C1',linestyle=":")
plt.xlabel("Time (days)")
plt.ylabel("Number Per 1000 People")
plt.legend(("Cases Needing ICU","Available ICU Beds"))
ipeakICU=np.argmax(NumICU) #find peak
peakICU=10*np.ceil(NumICU[ipeakICU]/10)#find time at peak
plt.ylim([0,peakICU])
plt.ylim([0,10])

#Find time when hospitalized cases = capacity
icross=np.argmin(np.abs(NumHosp[0:ipeakHosp]-AvailHospBeds)) #find intersection before peak
TimeFillBeds=tvec[icross]

#Find time when ICU cases = capacity
icross=np.argmin(np.abs(NumICU[0:ipeakICU]-AvailICUBeds)) #find intersection before peak
TimeFillICU=tvec[icross]

plt.show()
print("Hospital and ICU beds are filled by COVID19 patients after {0:4.1f} and {1:4.1f} days".format(TimeFillBeds,TimeFillICU))
```

Note that we have not taken into account the limited capacity in the model itself. If hospitals are at capacity, then the death rate will increase, since individuals with severe and critical infection will often die without medical care. The transmission rate will probably also increase, since any informal home-care for these patients will likely not include the level of isolation/precautions used in a hospital.

#### Allow for mechanical ventilation outside of ICUs using contingency or crisis capacity

```
plt.plot(tvec,NumICU)
plt.plot(np.array((0, tmax)),ConvVentCap*np.ones(2),linestyle=":")
plt.plot(np.array((0, tmax)),ContVentCap*np.ones(2),linestyle=":")
plt.plot(np.array((0, tmax)),CrisisVentCap*np.ones(2),linestyle=":")
plt.xlabel("Time (days)")
plt.ylabel("Number Per 1000 People")
plt.legend(("Cases Needing Mechanical Ventilation","Conventional Capacity","Contingency Capacity","Crisis Capacity"))
plt.ylim([0,peakICU])
plt.ylim([0,10])

#Find time when ICU cases = conventional capacity
icrossConv=np.argmin(np.abs(NumICU[0:ipeakICU]-ConvVentCap)) #find intersection before peak
TimeConvCap=tvec[icrossConv]
icrossCont=np.argmin(np.abs(NumICU[0:ipeakICU]-ContVentCap)) #find intersection before peak
TimeContCap=tvec[icrossCont]
icrossCrisis=np.argmin(np.abs(NumICU[0:ipeakICU]-CrisisVentCap)) #find intersection before peak
TimeCrisisCap=tvec[icrossCrisis]

plt.show()
print("Capacity for mechanical ventilation is filled by COVID19 patients after {0:4.1f} (conventional), {1:4.1f} (contingency) and {2:4.1f} (crisis) days".format(TimeConvCap,TimeContCap,TimeCrisisCap))
```

Compare to the case with intervention

```
NumHospSlow=solnSlow[:,3]+solnSlow[:,4]
NumICUSlow=solnSlow[:,4]

plt.figure(figsize=(13,4.8))
plt.subplot(1,2,1)
plt.plot(tvec,NumHosp)
plt.plot(tvec,NumHospSlow,color='C0',linestyle="--")
plt.plot(np.array((0, tmax)),AvailHospBeds*np.ones(2),color='C0',linestyle=":")
plt.xlabel("Time (days)")
plt.ylabel("Number Per 1000 People")
plt.legend(("Cases Needing Hospitalization","Cases Needing Hospitalization (Intervetion)","Available Hospital Beds"))
plt.ylim([0,peakHosp])

plt.subplot(1,2,2)
plt.plot(tvec,NumICU,color='C1')
plt.plot(tvec,NumICUSlow,color='C1',linestyle="--")
plt.plot(np.array((0, tmax)),AvailICUBeds*np.ones(2),color='C1',linestyle=":")
plt.xlabel("Time (days)")
plt.ylabel("Number Per 1000 People")
plt.legend(("Cases Needing ICU","Cases Needing ICU (Intervetion)","Available ICU Beds"))
plt.ylim([0,peakICU])

#Find time when hospitalized cases = capacity
ipeakHospSlow=np.argmax(NumHospSlow) #find peak
icross=np.argmin(np.abs(NumHospSlow[0:ipeakHospSlow]-AvailHospBeds)) #find intersection before peak
TimeFillBedsSlow=tvec[icross]

#Find time when ICU cases = capacity
ipeakICUSlow=np.argmax(NumICUSlow) #find peak
icross=np.argmin(np.abs(NumICUSlow[0:ipeakICU]-AvailICUBeds)) #find intersection before peak
TimeFillICUSlow=tvec[icross]

plt.show()
print("With intervention, hospital and ICU beds are filled by COVID19 patients after {0:4.1f} and {1:4.1f} days".format(TimeFillBedsSlow,TimeFillICUSlow))
```

And for expanded mechanical ventilation capacity

```
plt.plot(tvec,NumICU)
plt.plot(tvec,NumICUSlow)
plt.plot(np.array((0, tmax)),ConvVentCap*np.ones(2),linestyle=":")
plt.plot(np.array((0, tmax)),ContVentCap*np.ones(2),linestyle=":")
plt.plot(np.array((0, tmax)),CrisisVentCap*np.ones(2),linestyle=":")
plt.xlabel("Time (days)")
plt.ylabel("Number Per 1000 People")
plt.legend(("Cases Needing Mechanical Ventilation","Cases Needing Mechanical Ventilation (Intervention)","Conventional Capacity","Contingency Capacity","Crisis Capacity"))
plt.ylim([0,peakICU])

#Find time when ICU cases = conventional capacity (with intervention)
icrossConvSlow=np.argmin(np.abs(NumICUSlow[0:ipeakICUSlow]-ConvVentCap)) #find intersection before peak
TimeConvCapSlow=tvec[icrossConvSlow]
icrossContSlow=np.argmin(np.abs(NumICUSlow[0:ipeakICUSlow]-ContVentCap)) #find intersection before peak
TimeContCapSlow=tvec[icrossContSlow]
icrossCrisisSlow=np.argmin(np.abs(NumICUSlow[0:ipeakICUSlow]-CrisisVentCap)) #find intersection before peak
TimeCrisisCapSlow=tvec[icrossCrisisSlow]

plt.show()
print("Capacity for mechanical ventilation is filled by COVID19 patients after {0:4.1f} (conventional), {1:4.1f} (contingency) and {2:4.1f} (crisis) days".format(TimeConvCapSlow,TimeContCapSlow,TimeCrisisCapSlow))
```

Interpretation: While interventions that reduce infectiousness do "flatten the curve", cases are still WAY over hospital capacity. There is no way to get anywhere close to staying under hospital bed capacity or mechanical ventilation capacity without reducing $R_0<1$.

### Assumptions 2
* All cases go to the hospital
* All critical cases require ICU care and mechanical ventilation

Note: No point running this scenario because it would be even more extreme than Assumption 1 (mild cases stayed home) and Assumption 1 already lead to rapid overlow of hospital resources

### Assumptions 3
* Only severe or critical cases go to the hospital
* All critical cases require ICU care and mechanical ventilation
* When hospital capacity is exceed, individual dies

Note: Could be used to simulate expected increases in death if capacity exceeded

## Alternative Models

To be continued, including
* Assuming ~30% of cases are asymptomatic (as seen on Diamond Princess) (This would lead to a re-interpretation of the reported rates of severe and critical infection, so the prevalence of these stages would decrease)
* A parallel instead of series model of disease course (because it is unclear if it is realistic that individuals who pass through the mild stage on the way to a severe state spend as long in the mild stage as individuals who never progress)
* Including pre-symptomatic transmission (for about last ~2 days of exposed period, as estimated in some studies)


```python
import math
import numpy as np
from sympy import *
```

```python
 # 2
 A = [(1, 2, 3), (-6, 5, -4), (7, 8, 9)]
 b = [10, -18, 14]
 detA = np.linalg.det(A)
 detA

```

    -120.00000000000009

```python
# 3
p = math.ceil(100*math.pi)
p
```

    315

```python
# 4
A = [(1, 3), (0, 3)]
b = [14, 21]
x = np.matmul(np.linalg.inv(A), np.transpose(b))
x1 = x[0]
x2 = x[1]
x1 - x2
```

    -14.0

```python
# 5
Lista = [(0, 1), (2, 3), (4, 5)]
Lista[1][1]
```

    3

```python
# 6
A = [(1, 2, 3, 4), (-8, 7, -6, 5), (2, -4, 6, 8), (7, 5, 3, 1)]
m12 = [(-8, -6, 5), (2, 6, 8), (7, 3, 1)]
M12 = np.linalg.det(m12)
C12 = (-1)**(1+2) * M12
C12
```

    360.00000000000006

```python
# 7
A = [(1,  2),  (-3,  4)]
b = [14,  18]
detA = np.linalg.det(A)
M11 = 4
Z11 = detA - M11
print(detA)
print(Z11)
math.floor(Z11)
```

    10.000000000000002
    6.000000000000002

    6

```python
# 8
Vr = math.e
Va = 2.72
er = 100 * abs(Va - Vr)/Vr
print(f"{er:7.1e}")

```

    6.3e-02

```python
# 9

```


## Define a nutrient profile that can be systematically manipulated

I want to build a profile that can be used to explore parameter space for the non-dimensional number $\tau_v=-Z\delta_v^2C/\delta_vC$. Arctan could be a good candidate.

```python
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import seaborn as sns
import sympy as sym
sym.init_printing() # enable fancy printing
```

```python
# Set appearance options seaborn

sns.set_style('white')
sns.set_context('notebook')
```

```python
# Constants and scales from canyon bathy

#L = 6400.0 # canyon length
#R = 5000.0 # Upstream radius of curvature
#g = 9.81 # accel. gravity
#Wsb = 13000 # Width at shelf break
#Hs = 147.5 # Shelf break depth
#Hh = 97.5 #
#Hr = 130.0 # rim depth at dn station
# NOTE: The default values of all functions correspond to the base case

```

```python
a,b,c,z,Csb,Hs = sym.symbols('a,b,c,z,Csb,Hs')
func = -1.5*sym.atan(a*(z+Hs)) + Csb
ndfunc = func/Csb
```

```python
func
```

```python
ndfunc
```

```python
func.diff(z), (func.diff(z)).diff(z)
```

```python
ndfunc.diff(z), ((ndfunc.diff(z)).diff(z))
```

```python
func = func.subs({Hs:147.5,Csb:32.6})
hand =sym.plot(func.subs(a,0.1),
               func.subs(a,0.3),
               func.subs(a,0.6),
               func.subs(a,0.8),(z, -300, 0),
               xlabel='Depth (m)',
               ylabel='Concentration',
               title='%s' %func,
               show=False)
hand[1].line_color='r'
hand[2].line_color='g'
hand[3].line_color='purple'

hand.show()
```

```python
func = func.subs({Hs:147.5,Csb:32.6})
hand =sym.plot(func.subs(a,0.1),
               func.subs(a,0.3),
               func.subs(a,0.6),
               func.subs(a,0.8),(z, -1200, 0),
               xlabel='Depth (m)',
               ylabel='Concentration',
               title='%s' %func,
               show=False)
hand[1].line_color='r'
hand[2].line_color='g'
hand[3].line_color='purple'

hand.show()
```

### Non-dim by C(Hs) = 32.6 $\mu$M

```python
ndfunc = ndfunc.subs({Hs:147.5,Csb:32.6})
hand =sym.plot(ndfunc.subs(a,0.1),
               ndfunc.subs(a,0.3),
               ndfunc.subs(a,0.6),
               ndfunc.subs(a,0.8),(z, -300, 0),
               xlabel='Depth (m)',
               ylabel='Concentration',
               title='%s' %ndfunc,
               show=False)
hand[1].line_color='r'
hand[2].line_color='g'
hand[3].line_color='purple'

hand.show()
```

```python
hand =sym.plot((func.subs(a,0.1)).diff(z),
               (func.subs(a,0.3)).diff(z),
               (func.subs(a,0.6)).diff(z),
               (func.subs(a,0.8)).diff(z),(z, -300, 0),
               xlabel='Depth (m)',
               ylabel='$dC/dz$',
               title='%s' %func.diff(z),
               show=False)
hand[1].line_color='r'
hand[2].line_color='g'
hand[3].line_color='purple'

hand.show()
```

```python
hand =sym.plot(((func.subs(a,0.1)).diff(z)).diff(z),
               ((func.subs(a,0.3)).diff(z)).diff(z),
               ((func.subs(a,0.6)).diff(z)).diff(z),
               ((func.subs(a,0.8)).diff(z)).diff(z),(z, -300, 0),
               xlabel='Depth (m)',
               ylabel='$d^2C/dz^2$',
               title='%s' %((func).diff(z)).diff(z),
               show=False)
hand[1].line_color='r'
hand[2].line_color='g'
hand[3].line_color='purple'

hand.show()
```

```python
Z = 50 # m approx value of upwelling depth
hand = sym.plot((-Z*(func.subs(a,0.1).diff(z)).diff(z))/func.subs(a,0.1).diff(z),
                (-Z*(func.subs(a,0.3).diff(z)).diff(z))/func.subs(a,0.1).diff(z),
                (-Z*(func.subs(a,0.6).diff(z)).diff(z))/func.subs(a,0.1).diff(z),
                (-Z*(func.subs(a,0.8).diff(z)).diff(z))/func.subs(a,0.1).diff(z),
                (z, -300, 0),
                xlabel='Depth (m)',
                ylabel='$Z(d^2C/dz^2)/(dC/dz)$',
                title=r'$\tau_v$',
                show = False)

hand[1].line_color='r'
hand[2].line_color='g'
hand[3].line_color='purple'

hand.show()
```

* I don't know if I like that the second derivative is zero at the shelf break because the second derivative and thus, $\tau_v$ are always zero.

* It is fairly easy to change dC/dz at the shelf break by changing the factor multiplying (z+Hs), for example from 0.3 to 0.8, the value of the first derivative goes from -0.045 to -0.09 at the shelf break (max). The function also gets smoother over a larger depth and that can be a problem.

```python

```