## Authors
The original SEIR model and ipython notebook for it were developed by Dr. Alison Hill
and cloned from github at
https://github.com/alsnhll/SEIR_COVID19
referenced on the web from the 50 best modelling apps for covid:
https://towardsdatascience.com/top-5-r-resources-on-covid-19-coronavirus-1d4c8df6d85f

The cautionary feedback version of the model was conceived and written by John S McCaskill, a fellow of the European Centre for Living Technology. It is based on experience in modelling Q$\beta$ quasispecies viral infections of bacterial cells back in the late 80's and early 90's in Manfred Eigen's lab at the Max Planck Institute. In cell free model systems, exponential growth of the virus changes to linear growth because of the limited amount of replicase enzyme available. I was curious to see if the often remarkably linear response following an early exponential growth (or power law in the case of small world connectivity network) phase, could be simply accounted for by a cautionary saturation effect as outlined below.

## SEIR and SCEIR Models

### Equations

\begin{equation}
\begin{split}
\dot{S} &= -(\beta_1 I_1 +\beta_2 I_2 + \beta_3 I_3) S + c_1 S_c - c_2*S*I_3\\
\dot{S_c} &= - c_0(\beta_1 I_1 +\beta_2 I_2 + \beta_3 I_3) S_c - c_1 S_c + c_2*S*I_3\\
\dot{E} &=(\beta_1 I_1 +\beta_2 I_2 + \beta_3 I_3 ) (S + c_0 S_c) - 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}

The use of I_3 as a state variable triggering susceptibles to execute caution is just one choice. In contrast with deaths, it does not accumulate over time and so retains the property of an active threat to society, rather than an historical one.

Actually, the effect of caution may be quadratic, since both the individual doing the infection and individual potentially being infected may be executing caution. The current version assumes that infectives do not change their precautionary measures in response to I_3. To implement this we would need to further distinguish careful and non careful infectives at least up to the I_1 level.

### Variables
* $S$: Susceptible individuals living as normal
* $S_c$: Susceptible individuals exercising pandemy precautions
* $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+S_c+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

* $c_i$ three parameters characterizing cautionary response of population via class $S_c$

    - c_0 : reduction factor for exposure for cautioned susceptibles

    - c_1 : duration of caution (exponential decay time constant in days)

    - c_2 : rate constant for transition from uncautioned to cautioned susceptible

### Basic reproductive ratio

$R_0$ in the simple uncautioned SEI3R model 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}

$R_0$ as such clearly applies to a population where S=N, i.e. at outset of infection.
As such, it remains unchanged in the SCEI3R model, since S_c is then 0.

###  Influence of caution on SEIR model

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

```python
# Jupyter Specifics
# For widgets (sliders used below)

from IPython.display import HTML
from ipywidgets.widgets import interact, IntSlider, FloatSlider, Layout, ToggleButton, ToggleButtons, fixed

style = {'description_width': '100px'}
slider_layout = Layout(width='99%')
```

```python
#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

#include caution correction via y[6]=S_c and three parameters in vector c (see below)

def sceir(y,t,b,a,g,p,u,c,N):
    dy=[0,0,0,0,0,0,0]
    S=N-sum(y);
    dy[0]=np.dot(b[1:3],y[1:3])*(S+c[0]*y[6])-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
    dy[6]= -c[0]*np.dot(b[1:3],y[1:3])*y[6]-c[1]*y[6]+c[2]*S*y[3]  #Sc

    return dy
```

```python
# Define parameters based on clinical observations

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

Exposure=0.25 # Rate coefficient for exposure per individual in contact per day
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

# Model extension by John McCaskill to include caution
CautionFactor= 0.3 # Fractional reduction of exposure rate for cautioned individuals
CautionRetention= 14. # Duration of cautionary state of susceptibles (2 weeks)
CautionICUFrac= 0.25 # Fraction of ICUs occupied leading to transition to caution @ 1/day
ICUFrac= 0.001 # Fraction of ICUs relative to population size N
```

```python
# Define parameters and run ODE

N=1000000
b=np.zeros(4) #beta
g=np.zeros(4) #gamma
p=np.zeros(3)
c=np.zeros(3) #caution

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]

c[0]=CautionFactor
c[1]=1/CautionRetention
c[2]=1/(N*ICUFrac*CautionICUFrac)

#b=Exposure*np.ones(4)/N # all stages transmit equally
b=Exposure*np.array([0,1,0,0])/N # 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

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

    [0.0e+00 2.5e-07 0.0e+00 0.0e+00]
    0.2
    [0.         0.08       0.06818182 0.08571429]
    [0.         0.02       0.02272727]
    0.057142857142857134
    [0.3        0.07142857 0.004     ]

```python
tmax=365
tvec=np.arange(0,tmax,0.1)

#SEIR
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))

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

#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 1000000 People")
plt.legend(("S","E","I1","I2","I3","R","D"))
plt.ylim([1,1000000])
#plt.tight_layout()

#SCEIR
icc=np.zeros(7)
icc[0]=1
solnc=odeint(sceir,icc,tvec,args=(b,a,g,p,u,c,N))
solnc=np.hstack((N-np.sum(solnc,axis=1,keepdims=True),solnc))

#Plot on linear scale
plt.figure(figsize=(13,5))
plt.subplot(1,2,1)
plt.plot(tvec,solnc)
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000000 People")
plt.legend(("S","E","I1","I2","I3","R","D","Sc"))
plt.ylim([0,1000000])

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

#### Cumulative corona cases

```python
plt.figure(figsize=(13,5))
plt.subplot(1,2,1)
plt.plot(tvec,np.sum(soln[:,2:6],axis=1,keepdims=True))
plt.plot(tvec,np.sum(solnc[:,2:6],axis=1,keepdims=True))
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000000 People")
plt.legend(("No caution","Caution"))
plt.ylim([0,1000000])
plt.title('Cumulative cases')
print("")
```

## SCCEIR model with caution distinguished $E$ and  𝐼1

### Equations

\begin{equation}
\begin{split}
\dot{S} &= -(\beta_1 (I_1 + c_0 I_{1c}) +\beta_2 I_2 + \beta_3 I_3) S + c_1 S_c - c_2*S*I_3\\
\dot{S_c} &= - c_0(\beta_1 (I_1 + c_0 I_{1c}) +\beta_2 I_2 + \beta_3 I_3) S_c - c_1 S_c + c_2*S*I_3\\
\dot{E} &=(\beta_1 (I_1 + c_0 I_{1c}) +\beta_2 I_2 + \beta_3 I_3 ) S - a E \\
\dot{E_c} &=(\beta_1 (I_1 + c_0 I_{1c}) +\beta_2 I_2 + \beta_3 I_3 ) c_0 S_c - a E \\
\dot{I_1} &= a E - \gamma_1 I_1 - p_1 I_1 \\
\dot{I_{1c}} &= a E_c - \gamma_1 I_{1c} - p_1 I_{1c} \\
\dot{I_2} &= p_1 (I_1 + I_{1c}) -\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 + I_{1c}) + \gamma_2 I_2 + \gamma_3 I_3 \\
\dot{D} & = \mu I_3
\end{split}
\end{equation}

The use of I_3 as a state variable triggering susceptibles to execute caution is just one choice. In contrast with deaths, it does not accumulate over time and so retains the property of an active threat to society, rather than an historical one.

Here, the effect of caution is quadratic, since both the individual doing the infection and individual potentially being infected may be executing caution. To implement this we distinguish careful and non careful exposed and infectives up to the I_1 level. Once in hospital there is no difference, since all caution is executed wrt infected patients.

### Variables
* $S$: Susceptible individuals living as normal
* $S_c$: Susceptible individuals exercising pandemy precautions
* $E$: Exposed individuals living as normal - infected but not yet infectious or symptomatic
* $E_c$: Exposed individuals exercising pandemy precautions
* $I_i$: Infected individuals in severity class $i$. Severity increaes with $i$ and we assume individuals must pass through all previous classes. Split non hospital cases by caution.
  * $I_1$: Mild infection (hospitalization not required), living as normal
  * $I_{1c}$: Mild infection (hospitalization not required), exercising caution
  * $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+S_c+E+E_c+I_{1c}+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

* $c_i$ three parameters characterizing cautionary response of population via class $S_c$

    - c_0 : reduction factor for exposure for cautioned susceptibles

    - c_1 : duration of caution (exponential decay time constant in days)

    - c_2 : rate constant for transition from uncautioned to cautioned susceptible

###  Influence of caution on SCCEIR model

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

```python
#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.
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
#include caution correction via y[6]=S_c and three parameters in vector c (see below)
def sceir(y,t,b,a,g,p,u,c,N):
    dy=[0,0,0,0,0,0,0]
    S=N-sum(y);
    dy[0]=np.dot(b[1:3],y[1:3])*(S+c[0]*y[6])-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
    dy[6]= -c[0]*np.dot(b[1:3],y[1:3])*y[6]-c[1]*y[6]+c[2]*S*y[3]  #Sc
    return dy

#include caution correction to E and I_1 via y[7]=E_c and y[8]=I_1c
def scceir(y,t,b,a,g,p,u,c,N):
    dy=[0,0,0,0,0,0,0,0,0]
    S=N-sum(y);
    dy[0]=np.dot(b[1:3],y[1:3])*S+c[0]*b[1]*y[8]*S-a*y[0] # E
    dy[1]= a*y[0]-(g[1]+p[1])*y[1] #I1
    dy[2]= p[1]*(y[1]+y[8])-(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])+g[1]*y[8] #R
    dy[5]=u*y[3] #D
    dy[6]= -c[0]*np.dot(b[1:3],y[1:3])*y[6]-c[0]*c[0]*b[1]*y[8]*y[6]-c[1]*y[6]+c[2]*S*y[3]  #Sc
    dy[7]=np.dot(b[1:3],y[1:3])*c[0]*y[6]+c[0]*c[0]*b[1]*y[8]*y[6]-a*y[7] # Ec
    dy[8]= a*y[7]-(g[1]+p[1])*y[8] #I1
    return dy
```

```python
# Define parameters based on clinical observations

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

Exposure=0.25 # Rate coefficient for exposure per individual in contact per day
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

# Model extension by John McCaskill to include caution
CautionFactor= 0.3 # Fractional reduction of exposure rate for cautioned individuals
CautionRetention= 14. # Duration of cautionary state of susceptibles (4 weeks)
CautionICUFrac= 0.25 # Fraction of ICUs occupied leading to 90% of susceptibles in caution
ICUFrac= 0.001 # Fraction of ICUs relative to population size N
```

```python
# Define parameters and run ODE
tmax = 365
N=1000000
b=np.zeros(4) #beta
g=np.zeros(4) #gamma
p=np.zeros(3) # progression
c=np.zeros(3) #caution

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]

c[0]=CautionFactor
c[1]=1/CautionRetention
c[2]=1/(N*ICUFrac*CautionICUFrac)   # this is the rate coefficient giving 1/day at IE = denom

#b=Exposure*np.ones(4)/N # all stages transmit equally
b=Exposure*np.array([0,1,0,0])/N # 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

```python
def solveplot(b,a,g,p,u,c,N,tmax,scale='linear',species='no_susc',plottitle= '',
              label='',newplot = True,models=['SEIR','SCEIR','SCCEIR']):
    """
    b,a,g,p,u,c,N,tmax  simulation parameters
    scale: alternaticve 'linear' or 'log'
    species alternatives 'all', 'confirmed', 'deaths', 'daily confirmed'
    title
    newplot : whether to open new plot True/False
    """
    tvec=np.arange(0,tmax,0.1)
    nmodels = len(models)
    nm = 0

    if newplot == True:
        plt.figure(figsize=(nmodels*8,6))

    solns = []
    if 'SEIR' in models:   #SEIR
        nm = nm + 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)) # adds S as first elt

        #Plot
        plt.subplot(1,nmodels,nm)
        if scale == 'log': #Plot on log scale
            plt.semilogy()
            plt.ylim([1,10000])
        elif species != 'daily confirmed':           # Plot on normal linear scale
            #plt.ylim([0,10000])
            pass
        if species == 'no_susc':
            plt.plot(tvec,soln[:,1:5],label=label)
            plt.legend(("E","I1","I2","I3"))
        elif species == 'confirmed' or species == 'daily confirmed':
            suma = np.sum(soln[:,2:7],axis=1)
            # print('length=',len(suma))
            if species == 'daily confirmed':
                sumd = np.zeros(len(suma))
                for i in range(1,len(suma)):
                    sumd[i] = suma[i]-suma[i-1]
                #plt.ylim([0,1000])
                plt.plot(tvec,sumd,label=label)
            else:
                #plt.ylim([0,200000])
                plt.plot(tvec,suma,label=label)
        else:
            plt.plot(tvec,soln,label=label)
            plt.legend(("S","E","I1","I2","I3","R","D"))
        plt.xlabel("Time (days)")
        plt.ylabel("Number per 1000000 People")
        plt.title('SEIR %s' % plottitle)
        solns.append(soln)

    if 'SCEIR' in models:   #SCEIR
        nm = nm + 1
        ic=np.zeros(7)
        ic[0]=1
        solnc=odeint(sceir,ic,tvec,args=(b,a,g,p,u,c,N))
        solnc=np.hstack((N-np.sum(solnc,axis=1,keepdims=True),solnc)) # adds S as first elt

        #Plot
        plt.subplot(1,nmodels,nm)
        if scale == 'log': #Plot on log scale
            plt.semilogy()
            plt.ylim([1,10000])
        elif species != 'daily confirmed':           # Plot on normal linear scale
            #plt.ylim([0,10000])
            pass
        if species == 'no_susc':
            plt.plot(tvec,solnc[:,1:5],label=label)
            plt.legend(("E","I1","I2","I3"))
        elif species == 'confirmed' or species == 'daily confirmed':
            suma = np.sum(solnc[:,2:7],axis=1)
            # print('length=',len(suma))
            if species == 'daily confirmed':
                sumd = np.zeros(len(suma))
                for i in range(1,len(suma)):
                    sumd[i] = suma[i]-suma[i-1]
                #plt.ylim([0,1000])
                plt.plot(tvec,sumd,label=label)
            else:
                #plt.ylim([0,200000])
                plt.plot(tvec,suma,label=label)
        else:
            plt.plot(tvec,solnc,label=label)
            plt.legend(("S","E","I1","I2","I3","R","D","Sc"))
        plt.xlabel("Time (days)")
        plt.ylabel("Number per 1000000 People")
        plt.title('SCEIR %s' % plottitle)
        solns.append(solnc)

    if 'SCCEIR' in models:   #SCCEIR
        nm = nm + 1
        ic=np.zeros(9)
        ic[0]=1
        solncc=odeint(scceir,ic,tvec,args=(b,a,g,p,u,c,N))
        solncc=np.hstack((N-np.sum(solncc,axis=1,keepdims=True),solncc)) # prepend S

        #Plot
        plt.subplot(1,nmodels,nm)
        if scale == 'log': #Plot on log scale
            plt.semilogy()
            plt.ylim([1,10000])
        elif species != 'daily confirmed':           # Plot on normal linear scale
            #plt.ylim([0,10000])
            pass
        if species == 'no_susc':
            plt.plot(tvec,solncc[:,1:5])
            plt.legend(("E","I1","I2","I3"))
        elif species == 'confirmed' or species == 'daily confirmed':
            suma = np.sum(solncc[:,2:7],axis=1) + solncc[:,9]
            if species == 'daily confirmed':
                sumd = np.zeros(len(suma))
                for i in range(1,len(suma)):
                    sumd[i] = suma[i]-suma[i-1]
                # plt.ylim([0,1000])
                plt.plot(tvec,sumd,label=label)
            else:
                # plt.ylim([0,200000])
                plt.plot(tvec,suma,label=label)
        else:
            plt.plot(tvec,solncc,label=label)
            plt.legend(("S","E","I1","I2","I3","R","D","Sc","Ec","I1c"))
        plt.xlabel("Time (days)")
        plt.ylabel("Number per 1000000 People")
        plt.title('SCCEIR %s' % plottitle)
        solns.append(solncc)
    return solns

```

```python
(b,a,g,p,u,c,N,tmax)
```

    (array([0.0e+00, 2.5e-07, 0.0e+00, 0.0e+00]),
     0.2,
     array([0.        , 0.08      , 0.06818182, 0.08571429]),
     array([0.        , 0.02      , 0.02272727]),
     0.057142857142857134,
     array([0.3       , 0.07142857, 0.004     ]),
     1000000,
     365)

```python
solveplot(b=b,a=a,g=g,p=p,u=u,c=c,N=N,tmax=tmax,scale='linear');
```

```python
solns=solveplot(b,a,g,p,u,c,N,tmax,scale='linear',species='confirmed')
```

```python
solns=solveplot(b,a,g,p,u,c,N,tmax,scale='linear',species='daily confirmed')
```

```python
# caution standard parameters
CautionFactor= 0.3    # Fractional reduction of exposure rate for cautioned individuals
CautionRetention= 14. # Duration of cautionary state of susceptibles (4 weeks)
CautionICUFrac= 0.25  # Fraction of ICUs occupied leading to transition to caution @ 1/day
ICUFrac= 0.001        # Fraction of ICUs relative to population size N

# Sensitivity scans
CautionFactors= [0.1,0.25,0.5,0.75,1.0]  # Fractional reduction of exposure rate for cautioned individuals
CautionRetentions= [7.,14.,28.,56.,112.] # Duration of cautionary state of susceptibles (2 weeks)
CautionICUFracs= [0.0625,0.125,0.25,0.5,0.75] # Fraction of ICUs occupied leading to 90% of susceptibles in caution

newplot = True
for i in range(5):
    c[0]=CautionFactors[i]
    c[1]=1./CautionRetention
    c[2]=1./(N*ICUFrac*CautionICUFrac)
    label_c = 'CautionFactor %s' % CautionFactors[i]
    plottitle = 'Caution Factor Sensitivity'
    solns=solveplot(b,a,g,p,u,c,N,tmax,scale='linear',plottitle= plottitle,label=label_c,
              newplot = newplot,species='daily confirmed',models=['SCEIR','SCCEIR'])
    plt.legend()
    newplot = False

newplot = True
#for i in reversed(range(5)):
for i in range(5):
    c[0]=CautionFactor
    c[1]=1/CautionRetentions[i]
    c[2]=1./(N*ICUFrac*CautionICUFrac)
    label_c = 'CautionRetention %s'% CautionRetentions[i]
    plottitle = 'Caution Retention Sensitivity'
    solns=solveplot(b,a,g,p,u,c,N,tmax,scale='linear',plottitle= plottitle,label=label_c,
              newplot = newplot,species='daily confirmed',models=['SCEIR','SCCEIR'])
    plt.legend()
    newplot = False

newplot = True
for i in range(5):
    c[0]=CautionFactor
    c[1]=1/CautionRetention
    c[2]=1./(N*ICUFrac*CautionICUFracs[i])
    label_c = 'CautionICUFrac %s'% CautionICUFracs[i]
    plottitle = 'Caution ICUFrac Sensitivity'
    solns=solveplot(b,a,g,p,u,c,N,tmax,scale='linear',plottitle= plottitle,label=label_c,
              newplot = newplot,species='daily confirmed',models=['SCEIR','SCCEIR'])
    plt.legend()
    newplot = False
```

### Interactive sliders for caution variables

Initialized to whatever is in the `c` array:

```python
print('Currenly c = ',c)
```

    Currenly c =  [0.3        0.07142857 0.004     ]

```python
def isolveplot(c0,c1,c2):
    cc = [c0,c1,c2]
    solveplot(b,a,g,p,u,cc,N,tmax,scale='linear')
```

```python
interact(isolveplot,c0=FloatSlider(min=0,max=1,step=0.01,value=c[0],description='c0',
                            style=style,layout=slider_layout,continuous_update=False),
                       c1=FloatSlider(min=0,max=.1,step=0.001,value=c[1],description='c1',
                            style=style,layout=slider_layout,continuous_update=False,readout_format='.3f'),
                       c2=FloatSlider(min=0,max=.01,step=0.0001,value=c[2],description='c2',
                            style=style,layout=slider_layout,continuous_update=False,readout_format='.4f'))
```

    interactive(children=(FloatSlider(value=0.3, continuous_update=False, description='c0', layout=Layout(width='9…

    <function __main__.isolveplot(c0, c1, c2)>

```python

# caution standard parameters
def Csolveplot(CautionFactor= 0.3,    # Fractional reduction of exposure rate for cautioned individuals
               CautionRetention= 14., # Duration of cautionary state of susceptibles (4 weeks)
               CautionICUFrac= 0.25):  # Fraction of ICUs occupied leading to transition to caution @ 1/day
    ICUFrac= 0.001        # Fraction of ICUs relative to population size N
    c = [None]*3
    c[0]=CautionFactor
    c[1]=1./CautionRetention
    c[2]=1./(N*ICUFrac*CautionICUFrac)
    solveplot(b,a,g,p,u,c,N,tmax,scale='linear')

```

```python
interact(Csolveplot,CautionFactor=FloatSlider(min=0,max=1,step=0.01,value=0.3,description='CautionFactor',
                            style=style,layout=slider_layout,continuous_update=False),
                       CautionRetention=FloatSlider(min=0,max=50,step=0.2,value=14.,description='CautionRetention',
                            style=style,layout=slider_layout,continuous_update=False,readout_format='.3f'),
                       CautionICUFrac=FloatSlider(min=0,max=1,step=0.01,value=0.25,description='CautionICUFrac',
                            style=style,layout=slider_layout,continuous_update=False,readout_format='.4f'))
```

    interactive(children=(FloatSlider(value=0.3, continuous_update=False, description='CautionFactor', layout=Layo…

    <function __main__.Csolveplot(CautionFactor=0.3, CautionRetention=14.0, CautionICUFrac=0.25)>

### Cumulative corona cases

```python
solns=solveplot(b,a,g,p,u,c,N,tmax,scale='linear',species='confirmed')
soln = solns[0]
solnc = solns[1]
solncc = solns[2]
plt.figure(figsize=(40,25))
plt.subplot(1,2,1)
sum0 = np.zeros(len(soln[:0]))
suma = np.zeros(len(soln[:0]))
sumb = np.zeros(len(soln[:0]))
sum0 = np.sum(soln[:,2:6],axis=1,keepdims=True)   # shape  (3650,1)
suma = np.sum(solnc[:,2:6],axis=1,keepdims=True)   # shape  (3650,1)
sumb = np.sum(solncc[:,2:6],axis=1) + solncc[:,9]  # shape  (3650,)
plt.plot(tvec,sum0)
plt.plot(tvec,suma)
plt.plot(tvec,sumb)
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000000 People")
plt.legend(("SEIR No caution","SCEIR Linear caution","SCCEIR Quadratic caution"))
plt.ylim([0,100000])
plt.title('Cumulative cases')
print("")
```

## Dr. Alison's SEIR analysis

```python
# 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
```

```python
(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  8.9 days

### Repeat of SEIR but with a social distancing measure that reduces transmission rate
This and subsequent sections currently unaltered from Dr. Alison Hill's notebook.

```python
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

```python
### 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.semilogy()
plt.xlabel("Time (days)")
plt.ylabel("Number per 1000 People")
plt.legend(("No intervention","Intervention"))
plt.ylim([0,1000000])
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.

```python
#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

```python
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

```python
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

```python
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

```python
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

```

```python

```

```python

```


## A simple Susceptible-Infected-Recovered (SIR) model

The SIR model is explained in many places (e.g. [here](https://en.wikipedia.org/wiki/Compartmental_models_in_epidemiology#The_SIR_model)). Economists sometimes embed this in a larger economics model, for example this [NBER paper](https://www.nber.org/papers/w26902) working paper and references cited within.

Python code below by Jonathan Conning, guided by this ['Coronavirus Curve - Numberphile](https://www.youtube.com/watch?v=k6nLfCbAzgo) video.  If you're not doing so already you can run this as an interactive [web app](https://ricardian.herokuapp.com/).  The notebook with code is [here](https://github.com/jhconning/Econ-Teach/blob/master/notebooks/epidemic/SIRmodel.ipynb)

### Model
The proportion of the population are infected $I_t$, susceptible $S$, and recovered $R$ evolve over time according to these equation swhich depend on the transmission rate $\beta$ and the recovery rate $\gamma$:

$$
\begin{align}
\frac{dI}{dt} &= \beta \cdot S \cdot I - \gamma \cdot I  \\
\frac{dS}{dt} &=-\beta \cdot S \cdot I  \\
\frac{dR}{dt} &= \gamma \cdot I
\end{align}
$$

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from ipywidgets import interact
```

```python
N = 1          # Size of the population (so everything in proportions)
I0 = 0.01      # Initial proportion of the population infected
S0 = N - I0    # Initial proportion of the population susceptible
R0 = 0.0       # Initial proportion of the population recovered
maxT = 25      # max number of periods in simulation

beta = 0.5      # transmission rate
gamma = 0.1     # recovery rate
```

```python
def SIR(y, t, beta, gamma):
    '''the SIR model'''
    S, I, R = y
    dSdt = -beta*S*I
    dIdt = beta*S*I - gamma*I
    dRdt = gamma*I
    return([dSdt, dIdt, dRdt])
```

```python
def plotSIR(beta = beta, gamma = gamma, maxT = maxT):
    '''Solve differential equations in SIR and plot'''
    t = np.linspace(0, maxT, 1000)
    soln = odeint(SIR,[S0,I0,R0], t, args=(beta, gamma))
    soln = np.array(soln)
    plt.figure(figsize=[8,6])

    plt.plot(t, soln[:,0], linewidth=3, label = 'S(t)')
    plt.plot(t, soln[:,1], linewidth=3, label = 'I(t)')
    plt.plot(t, soln[:,2], linewidth=3, label = 'R(t)')
    plt.grid()
    plt.legend()
    plt.xlabel("Time")
    plt.ylabel("proportions")
    plt.title("SIR model")
    plt.show()
```

Change the parameters with the sliders below (you'll only have interactivity with a running jupyter notebook server).

```python
interact(plotSIR, beta=(0,1,0.05), gamma=(0,1,0.05), maxT=(5,100,5));
```

    interactive(children=(FloatSlider(value=0.5, description='beta', max=1.0, step=0.05), FloatSlider(value=0.1, d…

Below is a plot with the default parameters ($\beta=0.5$, $\gamma=0.1$) to have a graphic in case widget above does not display.

```python
plotSIR(beta, gamma, maxT)
```

# A More complex model

## In this model, social distancing for all can be counterproductive!

One feature of the Coronavirus is that some cases are almost Asymptomatic. In general, these cases recover faster. Also, one might expect that these cases move faster to begin with. How would we do this? Let's make a model in which

$$
\begin{align}
\frac{dI}{dt} &= \beta \cdot S \cdot I - \gamma \cdot I  \\
\frac{dS}{dt} &=-\beta \cdot S \cdot I  \\
\frac{dR}{dt} &= \gamma \cdot I
\end{align}
$$

```python

```


# The part where we follow a trajectory is based on the vehicle we controlled in HW4, so let's go over HW4 first!

Three propellers generate torques and forces in the body frame to translate and rotate the multirotor system. We can obtain the *local* wrench $\mathbf{w} = [\mathbf{f}^\top, \boldsymbol\tau^\top]^\top$ by using the mapping $\mathbf{A}$ from the input vector $\mathbf{u}$: $\mathbf{w=Au}$. Then accounting the effects of gravity, the rotation of the drone in the reference frame, and the inertia, we can obatin the derivative of the states by applying
\begin{equation}
\dot{\mathbf{X}} = \begin{bmatrix}
        \frac{1}{m}\mathbf{R}^\top & \mathbf{O}\\
        \mathbf{O} & \mathbf{I}^{-1}_S
    \end{bmatrix}(\mathbf{A}\mathbf{u}
    -
    \begin{bmatrix}
        nmg\mathbf{\hat e}_3\\
        \mathbf{I}_S\boldsymbol\omega\times\boldsymbol\omega
    \end{bmatrix}).
\end{equation}

We can find the way to calculate the `A` matrix in the code...

```
import numpy as np
import cvxpy as cp
import matplotlib.pyplot as plt
plt.style.use('dark_background')

# initial states: [x0, y0, theta0, dx0, dy0, dtheta0]
x0 = [0, 0, 0, 0, 0.5, 0]
# inertia
I = 1.0
# mass
m = 1.5
# Do we want cvx?
en_cvx = False
# Do we want constains?
en_constrain = False
# simulation time
time_out = 60

# number of motors
n = 3
# position of the thrusters
p = np.array([[-1, 0], [0, 1.5], [1, 0]]).T
# tilting angles of the propellers
alpha = np.array([-45, 10, 30])*np.pi/180.0
# A-matrix of the quadrotor
Af = np.zeros([2, n]);
Atau = np.zeros([1, n]);
R = np.zeros([2, 2*n]);
for i in range(n):
    R[:, 2*i:2*i+2] = [[np.cos(alpha[i]), -np.sin(alpha[i])],
                       [np.sin(alpha[i]), np.cos(alpha[i])]]
    Af[:,i] = R[:, 2*i:2*i+2].dot(np.array([0, 1]))
    Atau[:,i] = np.cross(p[:,i], R[:, 2*i:2*i+2].dot(np.array([0, 1])))

A = np.concatenate([Af, Atau], axis=0)
assert np.linalg.matrix_rank(A) == 3
# gravity acceleration
g = 9.8
# time step
dt = 0.05
# max force of a propeller
fM = 20.0
# minimum force of a propeller
fm = 0
# # desired state
# des_state = np.array([5, 5, 30.0/180*np.pi, 0, 0, 0])
```

```
# step function for the motion of the drone
def step(state, f):
    x, y, theta, dx, dy, dtheta = state
    # calculate wrench
    w = np.array([[np.cos(theta), -np.sin(theta), 0],
                 [np.sin(theta), np.cos(theta), 0],
                  [0, 0, 1]]).dot(A.dot(f))
    fx, fy, tau = w
    dstate = np.array([dx, dy, dtheta, fx/m, fy/m-g, tau/I])
    new_state = state + dstate*dt
    # make sure the range of theta is in (-pi, pi]
    if new_state[2] <= -np.pi:
        new_state[2] += 2*np.pi
    elif new_state[2] > np.pi:
        new_state[2] -= 2*np.pi
    return new_state
```

Once we have the curernt states $\mathbf{X}, \dot{\mathbf{X}}$ and the desired state $\mathbf{X}^{des}, \dot{\mathbf{X}}^{des}$, based on linear feedback, we can calculate the desired wrench by using a PID.

I made a mistake when designing the system. We have 3 DoF and 3 actuators. Thus, this is a non-redundent fully-actuated system. Therefore, by using $\mathbf{u} = \mathbf{A}^{-1}\mathbf{w}^{des}$, we are obtaining the only answer. Therefore, the convex optimization does not give any extra benefit here.

```
Kp = 1.5
Kd = 2.5
Ktheta = 1.5
Komega = 1.8
def control(state, des_state, en_cvx, en_constrain):
    theta = state[2]
    err = des_state - state
    if err[2] <= -np.pi:
        err[2] += 2*np.pi
    elif err[2] > np.pi:
        err[2] -= 2*np.pi
    # linear feedback
    des_w = np.array([Kp*err[0]+Kd*err[3],
                      Kp*err[1]+Kd*err[4]+m*g,
                      Ktheta*err[2]+Komega*err[5]])
    R = np.array([[np.cos(theta), -np.sin(theta), 0],
                  [np.sin(theta), np.cos(theta), 0],
                  [0, 0, 1]])
    if en_cvx:
        # cvx to constrain the forces
        # Construct the problem.
        u = cp.Variable(n)
        objective = cp.Minimize(cp.norm_inf(u))
        constraints = [R*A*u == des_w, fm <= u, fM >= u]
        prob = cp.Problem(objective, constraints)
        # The optimal objective value is returned by `prob.solve()`.
        result = prob.solve()
        if prob.status == cp.OPTIMAL:
            f = u.value
        else:
            # Maybe we have no valid solution
            f = np.linalg.inv(A).dot(des_w)
    else:
        f = np.linalg.inv(A).dot(R.T.dot(des_w))

    if en_constrain:
        return clamp(f, fm, fM)
    else:
        return f

def clamp(vec, minimum, maximum):
    for i in range(len(vec)):
        if vec[i] < minimum:
            vec[i] = minimum
        elif vec[i] > maximum:
            vec[i] = maximum
    return vec
```

# Trajectory functions

Given the initial and final states (including the position, velocity, acceleration), and the time to finish the trajectory, calculate the coefficients of a quintic trajectory

### Procedure
* polynomial(p) = `a0 + a1 * t + a2 * t**2 + a3 * t**3 + a4 * t**4 + a5 * t**5`
* derivative(v) = `a1 + 2*a2 * t + 3*a3 * t**2 + 4*a4 * t**3 + 5*a5 * t**4`
* double derivative(a) = `2*a2 + 6*a3 * t + 12*a4 * t**2 + 20*a5 * t**3`
* Making t = 0, we will get a0, a1, a2 by making p=x1, v=v1, and a=acc1
* Making t = delta_t, then insert the calculated a0, a1, a2 into the equations, we will get a3, a4, a5 by solving 3 first-order equations of p=x2, v=v2, and a=acc2

```
def state_to_state_traj(x1, x2, v1, v2, acc1, acc2, delta_t):
    # generate the time-stamps with an equal step
    t = np.arange(0, delta_t, dt)
    a0 = x1
    a1 = v1
    a2 = acc1/2

    # The coefficients are generated with the Mathematica script:
    # https://drive.google.com/file/d/1AJ7zPijAW1jco-QZTF0dMUIYxVBLG8-F/view?usp=sharing
    a3 = -(3*acc1*delta_t**2 - acc2*delta_t**2 + 12*delta_t*v1 + 8*delta_t*v2 + 20*x1 - 20*x2) / (2*delta_t**3)
    a4 = -(-3*acc1*delta_t**2 + 2*acc2*delta_t**2 - 16*delta_t*v1 - 14*delta_t*v2 - 30*x1 + 30*x2) / (2*delta_t**4)
    a5 = -((acc1*delta_t**2 - acc2*delta_t**2 + 6*delta_t*v1 + 6*delta_t*v2 + 12*x1 - 12*x2)/(2*delta_t**5))

    polynomial = a0 + a1 * t + a2 * t**2 + a3 * t**3 + a4 * t**4 + a5 * t**5
    derivative = a1 + 2*a2 * t + 3*a3 * t**2 + 4*a4 * t**3 + 5*a5 * t**4
    accel = 2*a2 + 6*a3 * t + 12*a4 * t**2 + 20*a5 * t**3
    return polynomial, derivative, accel

def piecewise2D (X, Y, Vx, Vy, Accx, Accy, T, num_points):
    x, y, dx, dy, ddx, ddy, t = [], [], [], [], [], [], []

    for i in range(num_points-1):
        xi, dxi, ddxi = state_to_state_traj(X[i], X[i+1], Vx[i], Vx[i+1], Accx[i], Accx[i+1], T[i+1] - T[i])
        yi, dyi, ddyi = state_to_state_traj(Y[i], Y[i+1], Vy[i], Vy[i+1], Accy[i], Accy[i+1], T[i+1] - T[i])
        t_starti = T[i]

        x += xi.tolist()
        y += yi.tolist()
        dx += dxi.tolist()
        dy += dyi.tolist()
        ddx += ddxi.tolist()
        ddy += ddyi.tolist()
        t += (np.arange(0, T[i+1] - T[i], 0.05) + t_starti).tolist()

    plt.plot(x, y)
    return x, y, dx, dy, ddx, ddy, t
```

## Plotting functions

```
# Plotting
def plot_points():
    plt.plot(X,Y, '--')
    plt.plot(X,Y, 'o')
    plt.quiver(X,Y, Vx, Vy, color='r')
    plt.quiver(X,Y, Accx, Accy, color='b')

# Speed
def plot_speed():
    speed = np.sqrt(np.array(dx)**2 + np.array(dy)**2)
    plt.plot(time_stamps, speed)

# Acceleration
def plot_accel():
    accel = np.sqrt(np.array(ddx)**2 + np.array(ddy)**2)
    plt.plot(time_stamps, accel)
```

## Example on min-snap trajectory generation

```
##### Requirements for the trajectory

# Waypoints
p1 = [0.,0]
p2 = [2,3]
p3 = [4,5]
p4 = [5,-1]
p5 = [1,-1]

# Velocities
v1 = [0,0.5]
v2 = [1,1]
v3 = [1,0]
v4 = [-1,1]
v5 = [0,-0.001]

# Accelerations
acc1 = [0.05,0]
acc2 = [0.1,-0.2]
acc3 = [-0.1,0.2]
acc4 = [-0.1, -0.05]
acc5 = [0.05,0]

# Time
t1 = 0
t2 = 12
t3 = 22
t4 = 32
t5 = 42
```

```
# Convert the initial conditions to a vector form
P = np.vstack((p1, p2, p3, p4, p5))
V = np.vstack((v1, v2, v3, v4, v5))
Acc = np.vstack((acc1, acc2, acc3, acc4, acc5))
T = [t1, t2, t3, t4, t5]

X, Y = P[:,0], P[:,1]
Vx, Vy = V[:,0], V[:,1]
Accx, Accy = Acc[:,0], Acc[:,1]

plot_points()
```

```
# Plot the trajectory that passes trhough the desired waypoints
x, y, dx, dy, ddx, ddy, time_stamps = piecewise2D(X,Y, Vx, Vy, Accx, Accy, T, len(P))
plot_points()
```

```
# Plot speed
plot_speed()
```

```
# plot acceleration
plot_accel()
```

# Trajectory Following simulation

```
def simulate(init_state, des_states, time_stamps, time_out=60):
    length = int(len(time_stamps))
    state_log = np.zeros((length, 6))
    f_log = np.zeros((length, n))
    state = init_state
    count = 0

    while count < length:
        state_log[count, :] = state
        des_state = des_states[:, count]
        f = control(state, des_state, en_cvx, en_constrain)
        f_log[count, :] = f
        state = step(state, f)
        count += 1
    return state_log, f_log
```

```
des_x, des_y, des_dx, des_dy = np.array(x), np.array(y), np.array(dx), np.array(dy)
des_theta = 0.2*np.ones(len(des_x))
des_dtheta = np.zeros(len(des_x))
des_states = np.vstack([des_x, des_y, des_theta, des_dx, des_dy, des_dtheta])

state_log, f_log = simulate(x0, des_states, time_stamps)
print(len(state_log))
```

    840

```
import matplotlib.pyplot as plt

fig1 = plt.figure()
ax11 = fig1.add_subplot(111)
ax11.grid()
ax11.plot(x, y, '--')
ax11.plot(X, Y, 'o')
ax11.set_xlabel('x position (m)')
ax11.set_xlabel('y position (m)')
ax11.set_title('Des v.s. real position')
ax11.plot(state_log[:, 0], state_log[:, 1])

fig2 = plt.figure()
ax21 = fig2.add_subplot(311)
ax21.grid()
ax21.set_ylabel('p1 force (N)')
ax21.set_title('forces v.s. time')
ax21.plot(time_stamps, f_log[:, 0])

ax22 = fig2.add_subplot(312)
ax22.grid()
ax22.set_ylabel('p2 force (N)')
ax22.set_xlabel('time (s)')
ax22.plot(time_stamps, f_log[:, 1])

ax23 = fig2.add_subplot(313)
ax23.grid()
ax23.set_ylabel('p3 force (N)')
ax23.set_xlabel('time (s)')
ax23.plot(time_stamps, f_log[:, 2])
```

```
!pip install JSAnimation
```

    Requirement already satisfied: JSAnimation in /usr/local/lib/python3.7/dist-packages (0.1)

```
import matplotlib.pyplot as plt
from matplotlib import animation
from JSAnimation import IPython_display
from IPython.display import HTML

fig, ax = plt.subplots()
plt.axis('equal')
sampling = int(2)

def animate(t):
    ax.clear()
    plt.xlim([-4, 8])
    plt.ylim([-4, 8])
    ax.grid()
    # Path
    plt.plot(state_log[:, 0], state_log[:, 1], 'b--')
    plt.plot(x, y, '--')
    plt.plot(X, Y, 'o')

    # current position and orientation
    ax.plot(state_log[sampling*t, 0], state_log[sampling*t, 1], 'ro')  # Center of mass
    ax.arrow(state_log[sampling*t, 0], state_log[sampling*t, 1], np.cos(state_log[sampling*t,2]), np.sin(state_log[sampling*t,2]), width=0.03, color=[0.5,0,0])
    # actuators
    # homogeneous vectors and rotations
    pos_p = np.zeros([2, n])
    rot_p = np.zeros([2, n])
    for i in range(n):
        # homogenenous transformation
        T = np.eye(3)
        T[0:2, 0:2] = np.array([[np.cos(state_log[sampling*t, 2]), -np.sin(state_log[sampling*t, 2])],
                                [np.sin(state_log[sampling*t, 2]), np.cos(state_log[sampling*t, 2])]])
        T[0:2, 2] = state_log[sampling*t, 0:2]
        pos_p[:, i] = T.dot(np.array([p[0, i], p[1, i], 1]))[0:2]
        rot_p[:, i] = T[0:2, 0:2].dot(R[:, 2*i:2*i+2].dot(f_log[sampling*t, i]*0.1*np.array([0, 1])))
        # print(rot_p)

    ax.plot(pos_p[0,:], pos_p[1, :], 'r.--')
    ax.plot([pos_p[0,-1], pos_p[0,0]], [pos_p[1, -1], pos_p[1, 0]], 'r.--')

    for i in range(n):
        ax.arrow(pos_p[0, i], pos_p[1, i], rot_p[0, i], rot_p[1, i], width=0.03, color=[0.5,0,0.5])

time = int(len(state_log)/sampling)
anim = animation.FuncAnimation(fig, animate, frames=time, interval=dt*1000*sampling)

HTML(anim.to_jshtml())
```


```python
%matplotlib nbagg
from __future__ import division
from IPython.display import HTML
from IPython.display import display
from scipy.optimize import *
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
```

## Question 1: Linear Regression, Curvature Matrix

Consider the data listed below,
\begin{equation}
\begin{array}{lcccccc}
\hline
{\rm frequency~(Hz)} &10&20&30&40&50&60\\
{\rm voltage~(mV)} &16&45&64&75&70&115\\
{\rm error~(mV)}   &5&5&5&5&30&5\\
\hline
{\rm frequency~(Hz)} &70&80&90&100&110&\\
{\rm voltage~(mV)} &142&167&183&160&221&\\
{\rm error~(mV)}   &5&5&5&30&5&\\
\hline
\end{array}
\end{equation}

This data is also contained in the file 'linear_regression.csv'.

Required:
<bf>
> (i) Calculate the 4 elements of the curvature matrix.
<bf>
> (ii) Invert this to give the error matrix.
<bf>
> (iii) What are the uncertainties in the slope and intercept?
<bf>
> (iv) Comment on your answer.

### (i) Calculate the 4 elements of the curvature matrix.

```python
data = pd.read_csv('linear_regression.csv')
frequencies = data.iloc[:,0]
voltages = data.iloc[:,1]
voltage_errors = data.iloc[:,2]

def one_i():
    '''Your function should return something of the form np.matrix([[a_cc,a_cm],[a_mc,a_mm]])'''

    a_cc = 0
    a_cm = 0
    a_mm = 0
    for j,i in enumerate(frequencies):

        # Note given we are assuming linear regression we can use the formulae
        # 7.23 - 7.25 in the Huges and Hayes book
        alpha_i = voltage_errors[j]
        a_cc += (1/(alpha_i**2))
        a_cm += (i/(alpha_i**2))
        a_mm += (i**2/(alpha_i**2))
    a_mc = a_cm
    curvature_matrix = np.matrix([[a_cc,a_cm],[a_mc,a_mm]])

    return curvature_matrix
one_i()
```

### (ii) Invert this to give the error matrix.

```python
data = pd.read_csv('linear_regression.csv')
frequencies = data.iloc[:,0]
voltages = data.iloc[:,1]
voltage_errors = data.iloc[:,2]

def one_ii():
    '''Your function should return something of the form np.matrix([[a_cc,a_cm],[a_mc,a_mm]])'''

    curvature_matrix = one_i()
    inverted_matrix = curvature_matrix.I

    return inverted_matrix

one_ii()
```

### (iii) What are the uncertainties in the slope and intercept?

```python
data = pd.read_csv('linear_regression.csv')
frequencies = data.iloc[:,0]
voltages = data.iloc[:,1]
voltage_errors = data.iloc[:,2]

def one_iii():
    slope_uncertainty = 0
    intercept_uncertainty = 0

    error_matrix = one_ii()
    slope_uncertainty = np.sqrt(error_matrix[1,1])
    intercept_uncertainty = np.sqrt(error_matrix[0,0])

    return slope_uncertainty,intercept_uncertainty

one_iii()
```

### (iv) Comment on your answer

We did the same analysis last week and got the same answer with a different method.

The diagonal elements of the error matrix are also not equal to the inverse of the diagonal elements of the curvature matrix and therefore suggest a correlation between the errors in the gradient and intercept. More specifically, since the off-diagonal elemements of the error matrix are negative (and therefore so are the values of the off-diagonal correlation matrix), the two variables are negatively correlated.

The error on the intercept is much larger than the error on the gradient. Given that the errors in the y-data (the voltage) are large relative to the negligible error in the x-data (frequency) this is not surprising.

## Question 2: Using a calibration curve

A frequently encountered case where the correlation of the uncertainties must be taken into account is that of a calibration curve.  Consider the following set of measurements from an optical-activity experiment, where the angle of rotation of a plane-polarized light beam, $\theta$, is measured as a function of the independent variable, the concentration, $C$, of a sucrose solution.

\begin{equation}
\begin{array}{lcccc}
\hline
C \mbox{ (g cm$^{-3}$)} &0.025&0.05&0.075&0.100\\
\theta \mbox{ (degrees)}&10.7&21.6&32.4&43.1\\
\hline
C \mbox{ (g cm$^{-3}$)}&0.125&0.150&0.175\\
\theta \mbox{ (degrees)}&53.9&64.9&75.4\\
\hline
\end{array}
\end{equation}

The errors in the angle measurement are all $0.1^{\circ}$, the errors in the concentration are negligible.  A straight line  fit to the data yields  a gradient of $431.7\,^{\circ}\mbox{ g$^{-1}$ cm$^{3}$}$, and intercept $-0.03^{\circ}$. This data is contained in 'optical_activity.csv'.

<bf>
Required:
<bf>
>(i) Show that the curvature matrix, $\mathsf{A}$, is given by

\begin{equation}
\mathsf{A}=\left[\begin{array}{cc}
700\left((^{\circ})^{-2}\right)&70\left((^{\circ})^{-2}\mbox{g cm$^{-3}$}\right)\\
70\left((^{\circ})^{-2}\mbox{g cm$^{-3}$}\right)&8.75\left((\mbox{g/$^\circ$ cm$^{3})^2$}\right)\\
\end{array}\right] ,
\end{equation}

>and that the error matrix  is

\begin{equation}
\mathsf{C}=\left[\begin{array}{cc}
0.00714\left((^{\circ})^2\right)&-0.0571\left((^{\circ})^2\mbox{g$^{-1}$cm$^{3}$}\right)\\
-0.0571\left((^{\circ})^2\mbox{g$^{-1}$cm$^{3}$}\right)&0.571\left((^{\circ})^2\mbox{g$^{-2}$ cm$^{6}$}\right)\\
\end{array}\right] .
\end{equation}

The entry for the intercept is in the top left-hand corner, that for the gradient in the bottom right-hand corner.
<bf>
>(ii) Calculate the associated correlation matrix.

Use the  entries of the error matrix to answer the following  questions:
<bf>
>(iii) What are the uncertainties in the best-fit intercept and gradient?
<bf>
>(iv) What optical rotation is expected for a known concentration of $C=0.080g cm^{-3}$, and what is the uncertainty?
<bf>
>(v) What is the concentration given a measured rotation of $\theta=70.3^{\circ}$ and what is the uncertainty?

### (i) Verify the curvature matrix and the error matrix above.

```python
data = pd.read_csv('optical_activity.csv')
concentrations = data.iloc[:,0]
angles = data.iloc[:,1]
angle_errors = data.iloc[:,2]

def two_i():
    '''Your function should return something of the form np.matrix([[a_cc,a_cm],[a_mc,a_mm]]). Must return the curvature and error matricies)'''
    curvature_matrix = 0
    error_matrix = 0

    a_cc = 0
    a_cm = 0
    a_mm = 0
    for j,i in enumerate(concentrations):
        alpha_i = angle_errors[j]
        a_cc += (1/(alpha_i**2))
        a_cm += (i/(alpha_i**2))
        a_mm += (i**2/(alpha_i**2))
    a_mc = a_cm

    curvature_matrix = np.matrix([[a_cc,a_cm],[a_mc,a_mm]])
    error_matrix = curvature_matrix.I

    return curvature_matrix,error_matrix

two_i()
```

### (ii) Calculate the associated correlation matrix.

```python
data = pd.read_csv('optical_activity.csv')
concentrations = data.iloc[:,0]
angles = data.iloc[:,1]
angle_errors = data.iloc[:,2]

def two_ii():
    '''Your function should return something of the form np.matrix([[a_cc,a_cm],[a_mc,a_mm]])'''

    matricies = two_i()
    error_matrix = matricies[1]
    diagonal_element = 1
    off_diagonal = error_matrix[0,1]/np.sqrt(error_matrix[0,0]*error_matrix[1,1])
    correlation_matrix = np.matrix([[1,off_diagonal],[off_diagonal,1]])

    return correlation_matrix
```

### (iii) What are the uncertainties in the best-fit intercept and gradient?

```python
data = pd.read_csv('optical_activity.csv')
concentrations = data.iloc[:,0]
angles = data.iloc[:,1]
angle_errors = data.iloc[:,2]

def two_iii():
    '''Your function should return the uncertainty in the gradient and intercept'''
    gradient_uncertainty = 0
    intercept_uncertainty = 0

    matricies = two_i()
    error_matrix = matricies[1]
    intercept_uncertainty = np.sqrt(error_matrix[0,0])
    gradient_uncertainty = np.sqrt(error_matrix[1,1])

    return gradient_uncertainty,intercept_uncertainty

two_iii()
```

### (iv) What optical rotation is expected for a known concentration of $C=0.080g cm^{-3}$, and what is the uncertainty?

```python
data = pd.read_csv('optical_activity.csv')
concentrations = data.iloc[:,0]
angles = data.iloc[:,1]
angle_errors = data.iloc[:,2]

def two_iv():
    '''Your function should return the angle and the uncertainty'''
    angle = 0
    uncertainty = 0

    matricies = two_i()
    error_matrix = matricies[1]
    angle = (431.7*0.08)-0.03

    # combine errors together
    uncertainty = np.sqrt(((0.08**2)*error_matrix[1,1])+error_matrix[0,0]+(2*0.08*error_matrix[0,1]))

    return angle,uncertainty

two_iv()
```

### (v) What is the concentration given a measured rotation of $\theta=70.3^{\circ}$ and what is the uncertainty? You must return your answer in $gcm^{-3}$

```python
data = pd.read_csv('optical_activity.csv')
concentrations = data.iloc[:,0]
angles = data.iloc[:,1]
angle_errors = data.iloc[:,2]

def two_v():
    '''Your function should return the concentration and uncertainty'''

    matricies = two_i()
    error_matrix = matricies[1]
    concentration = 70.33/431.7 #Should be 0.16291 gcm^-3
    uncertainty = (1/431.7)*np.sqrt(((0.16291**2)*0.571)+0.00714+(2*0.16291*(-0.0571)))

    return concentration,uncertainty

two_v()
```

## Question 3: Error bars from a $\chi^2$ minimisation to a non-linear function

In this question we will analyse the data shown in the figure below, which is an X-ray spectrum as a function of angle.

The data is contained in the file 'LorentzianData.csv'. There are three columns: the angle, the signal (in counts per second), and the error.  The number of X-rays counted in 20 seconds was recorded.

The model to describe the data has four parameters:  the height of the Lorentzian lineshape, $S_0$; the angle at which the peak is centered, $\theta_{0}$;
 the angular width of the peak, $\Delta\theta$; and a constant background offset, $S_{\rm bgd}$. Mathematically, the signal, $S$, is of the form:
\begin{equation}
S=S_{\rm bgd}+\frac{S_{0}}{1+4\left(\frac{\theta-\theta_{0}}{\Delta\theta}\right)^2}.
\end{equation}

and the function is defined by `lorentzian(theta, s_0, s_bgd,delta_theta,theta_0)`.

Required:
<bf>
>(i) Explain how the error in the count rate was calculated.
<bf>
>(ii) Perform a $\chi^2$ minimisation.  What are the best-fit parameters?
<bf>
>(iii) Evaluate the error matrix.
<bf>
>(iv) Calculate the correlation matrix.
<bf>
>(v) What are the uncertainties in the best-fit parameters?
<bf>
>(vi) If you can plot contour plots, show the $\chi^2$ contours for
<bf>
>>(a) background--peak centre.
<bf>
>>(b) background--peak width.
<bf>
>These figures are shown in figure 6.11 of Hughes and Hase. Comment on the shape of the contours.
<bf>

### (i) Explain how the error in the count rate was calculated.

The errors were calculated using Poisson statistics. As each count rate was calculated by measuring the number of counts in $20$ s, the error in the rate was given by taking the square root of the number of counts calculated in this interval, and then dividing it by this length of time. Synbolically, if $\tau$ is the amount of time the measurement was taken over and $\dot{N}$ the calculated count rate, then

$$\alpha_{\dot{N}} = \frac{\sqrt{\dot{N}\tau}}{\tau} = \sqrt{\frac{\dot{N}}{\tau}}~.$$

### (ii) Perform a $\chi^2$ minimisation.  What are the best-fit parameters?

```python
data = pd.read_csv('LorentzianData.csv')

def lorentzian(theta, s_0, s_bgd,delta_theta,theta_0):
    return s_bgd+(s_0/(1+4*(((theta-theta_0)/delta_theta)**2)))

def three_ii():
    s_0 = 0
    s_bgd = 0
    delta_theta = 0
    theta_0 = 0
    covariance_matrix = 0
    angles = data.iloc[:,0]
    intensity = data.iloc[:,1]
    intensity_errors = data.iloc[:,2]

    guess = np.array([5.,1.,0.5,44.])
    #guess = np.array([5.42,1.40,0.94,44.39])
    popt,pcov= curve_fit(lorentzian, angles, intensity, guess, sigma=intensity_errors)
    covariance_matrix = pcov
    s_0= popt[0]
    s_bgd = popt[1]
    theta_0 = popt[3]
    delta_theta = popt[2]
    angles_1 = np.arange(39,50,0.01)
    intensity_1 = []
    for i in angles_1:
        element = s_bgd + (s_0/(1+4*((i-theta_0)/delta_theta)**2))
        intensity_1.append(element)

    return(s_0,s_bgd,delta_theta,theta_0,covariance_matrix)

three_ii()
```

### (iii) Evaluate the error matrix.

```python
data = pd.read_csv('LorentzianData.csv')

def three_iii():
    '''Your function should return something of the form np.matrix([[a_cc,a_cm],[a_mc,a_mm]])'''

    cov_matrix = np.matrix(three_ii()[4])
    error_matrix = cov_matrix

    return error_matrix

three_iii()
```

### (iv) Calculate the correlation matrix.

```python
data = pd.read_csv('LorentzianData.csv')

def three_iv():
    '''Your function should return something of the form np.matrix([[a_cc,a_cm],[a_mc,a_mm]])'''

    cov_matrix = np.matrix(three_ii()[4])
    correlation_matrix = np.corrcoef(cov_matrix)

    return correlation_matrix

three_iv()
```

### (v) What are the uncertainties in the best-fit parameters?

```python
data = pd.read_csv('LorentzianData.csv')

def three_v():
    uncertainty_s_0 = 0
    uncertainty_s_bgd = 0
    uncertainty_delta_theta = 0
    uncertainty_theta_0 = 0

    error_mat = three_iii()
    uncertainty_s_0 = np.sqrt(error_mat[0,0])
    uncertainty_s_bgd = np.sqrt(error_mat[1,1])
    uncertainty_delta_theta = np.sqrt(error_mat[2,2])
    uncertainty_theta_0 = np.sqrt(error_mat[3,3])

    return uncertainty_s_0,uncertainty_s_bgd,uncertainty_delta_theta,uncertainty_theta_0

three_v()
```

### (vi) These contours are shown in figure 6.11 of Hughes and Hase. Comment on the shape of the contours.

The contours are not entirely elliptical; this is due to the non-linear fit. The contours for the background-width plot are flatter near the minimum than those of the background-centre plot.

## Question 4: Prove the following properties:

Assume in this question that the uncertainties in $A$ and $B$ are correlated.
>(i) If $Z=A\pm B$, show that
${\displaystyle\alpha_{Z}^2=\alpha_{A}^2+\alpha_{B}^2\pm2\alpha_{AB}}$.
<bf>
>(ii) If $Z=A\times B$, show that
 ${\displaystyle\left(\frac{\alpha_Z}{Z}\right)^2=\left(\frac{\alpha_A}{A}\right)^2+\left(\frac{\alpha_B}{B}\right)^2+2\left(\frac{\alpha_{AB}}{AB}\right)}$.
<bf>
>(iii) If ${\displaystyle Z=\frac{A}{B}}$, show that
${\displaystyle\left(\frac{\alpha_Z}{Z}\right)^2=\left(\frac{\alpha_A}{A}\right)^2+\left(\frac{\alpha_B}{B}\right)^2-2\left(\frac{\alpha_{AB}}{AB}\right)}$.

YOUR ANSWER HERE

Core 1/data analysis/ipython notebook/Homework 3 Model Solution/Assignment 3 Model Solutions.ipynb


# Compute root of Rosencrantz function

**Randall Romero Aguilar, PhD**

This demo is based on the original Matlab demo accompanying the  Computational Economics and Finance 2001 textbook by Mario Miranda and Paul Fackler.

Original (Matlab) CompEcon file: **demslv02.m**

Running this file requires the Python version of CompEcon. This can be installed with pip by running

    !pip install compecon --upgrade

Last updated: 2021-Oct-01

## About

Compute root of

\begin{equation}
f(x_1,x_2)= \begin{bmatrix}200x_1(x_2-x_1^2) + 1-x_1 \\ 100(x_1^2-x_2)\end{bmatrix}
\end{equation}

using Newton and Broyden methods. Initial values generated randomly.  True root is $x_1=1, \quad x_2=1$.

```python
import numpy as np
import pandas as pd
from compecon import NLP, tic, toc
```

### Set up the problem

```python
def f(x):
    x1, x2 = x
    fval = [200 * x1 * (x2 - x1 ** 2) + 1 - x1, 100 * (x1 ** 2 - x2)]
    fjac = [[200 * (x2 - x1 ** 2) - 400 * x1 ** 2 - 1, 200 * x1],
            [200 * x1, -100]]
    return np.array(fval), np.array(fjac)

problem = NLP(f)
```

### Randomly generate starting point

```python
problem.x0 = np.random.randn(2)
```

### Compute root using Newton method

```python
t0 = tic()
x1 = problem.newton()
t1 = 100 * toc(t0)
n1 = problem.fnorm
```

### Compute root using Broyden method

```python
t0 = tic()
x2 = problem.broyden()
t2 = 100 * toc(t0)
n2 = problem.fnorm
```

### Print results

```python
print('Hundreds of seconds required to compute root of Rosencrantz function')
print('f(x1,x2)= [200*x1*(x2-x1^2)+1-x1;100*(x1^2-x2)] via Newton and Broyden')
print('methods, starting at x1 = {:4.2f} x2 = {:4.2f}'.format(*problem.x0))

pd.DataFrame({
    'Time': [t1, t2],
    'Norm of f': [n1, n2],
    'x1': [x1[0], x2[0]],
    'x2': [x1[1], x2[1]]},
    index=['Newton', 'Broyden']
)
```

    Hundreds of seconds required to compute root of Rosencrantz function
    f(x1,x2)= [200*x1*(x2-x1^2)+1-x1;100*(x1^2-x2)] via Newton and Broyden
    methods, starting at x1 = 0.35 x2 = 0.87

      Time
      Norm of f
      x1
      x2

      Newton
      1.100898
      1.527217e-10
      1.0
      1.0

      Broyden
      3.888774
      1.074780e-09
      1.0
      1.0

notebooks/slv/02 Compute root of Rosencrantz function.ipynb


# Data analysis in the Amazon audit
Most of the materials in this notebook were kindly provided by Piotr Sapiezynski (www.sapiezynski.com)

### The motivation behind the analysis
Read it here
https://themarkup.org/amazons-advantage/2021/10/14/amazon-puts-its-own-brands-first-above-better-rated-products

```python
import pandas as pd
from sklearn.model_selection import train_test_split
import statsmodels.api as sm
from statsmodels.tools.tools import add_constant
```

```python
# this is the data from the markup audit, downloaded from their github:
# https://github.com/the-markup/investigation-amazon-brands
data = pd.read_csv('https://www.sapiezynski.com/cs4910/markup/data.csv')
data.head()
```

      search_term
      placed_higher
      stars_delta
      reviews_delta
      is_shipped_by_amazon
      is_sold_by_amazon
      is_amazon
      is_top_clicked
      random_noise
      asin_1
      asin_2

      0
      #10 envelope
      True
      0.0
      1654.0
      0
      2
      2
      0
      -2.569736
      B06VVLD2GL
      B01D0OANU4

      1
      #6 envelope
      True
      0.1
      7844.0
      0
      2
      2
      2
      0.951532
      B06X15WSLL
      B07JNXMBSX

      2
      1 inch binder
      False
      0.0
      -9383.0
      0
      0
      -2
      0
      0.618294
      B00A45VF2S
      B01BRGTWOA

      3
      1% milk
      False
      0.0
      -183.0
      0
      0
      0
      -2
      0.645435
      B07W5Z8SJ8
      B07WC9MMPD

      4
      10 dollar gifts
      True
      0.8
      75410.0
      0
      2
      0
      0
      -1.075435
      B00F4CEHNK
      B07FCNYND8

```python
columns = ['stars_delta', 'reviews_delta', 'is_amazon',
        'is_shipped_by_amazon', 'is_sold_by_amazon',
        'is_top_clicked', 'placed_higher']
```

Exercise 1

Use a random forest classifier to predict `placed_higher` using the features `is_amazon`,`is_shipped_by_amazon`, `is_sold_by_amazon`,`is_top_clicked`, `stars_delta`, `reviews_delta`. What is the most important feature?

You should get that `is_amazon` is the most important feature. How does it translate into the probability of being the top result? RandomForests won't tell us... But logistic regression can.

Quick reminder on regression and explainability - here applied to the amazon dataset.

**Linear** regression has the following formula:

$$ y = \beta_0 + \beta_1x_1 + \beta_2x_2 + ... $$

This means that:
* when all variables are equal to 0, the outcome variable $y$ is equal to $\beta_0$, the intercept.
* a **unit** change in $x_1$ corresponds to $y$ changing by $\beta_1$

It might not be a powerful model, but it offers a clear interpretation/explanation in regression problems.

**Logistic** regression is mostly used for binary classification and it looks similar to the linear regression, but now the outcome variable is the logarithm of odds of success.

$$ ln(\frac{\pi}{1-\pi}) = \beta_0 + \beta_1x_1 + \beta_2x_2 + ... $$

where $pi$ is the probability of success, so $1-\pi$ is the probability of failure.

Then, a unit change in $x_1$ corresponds to $\beta_1$ change in log odd ratios... but that is not intuitive at all. Let's rewrite it to calculate the probability directly:

$$\begin{align}
ln(\frac{\pi}{1-\pi}) &= \beta_0 + \beta_1x_1 + \beta_2x_2 + ... \\
\frac{\pi}{1-\pi} &= e^{\beta_0 + \beta_1x_1 + \beta_2x_2 + ...} \\
{\pi} &= ({1-\pi})e^{\beta_0 + \beta_1x_1 + \beta_2x_2 + ...} \\
{\pi} &= e^{\beta_0 + \beta_1x_1 + \beta_2x_2 + ...}-\pi e^{\beta_0 + \beta_1x_1 + \beta_2x_2 + ...}\\
{\pi}(1+e^{\beta_0 + \beta_1x_1 + \beta_2x_2 + ...}) &= e^{\beta_0 + \beta_1x_1 + \beta_2x_2 + ...} \\
{\pi} &= \frac{e^{\beta_0 + \beta_1x_1 + \beta_2x_2 + ...}}{1+e^{\beta_0 + \beta_1x_1 + \beta_2x_2 + ...}}
\end{align}$$

So, while we cannot directly say from that a unit change of $x_1$ corresponds to a certain change in the probability of the outcome, we can just plug zeros and ones into the equation above and compute the changes.

Let's load the data from scratch and do some cleaning (I'm dividing by two, because The Markup left the values of -2, 0, 2 so they never have a "unit" change. If we divide them by two, the unit change actually is meaningful).

```python
data = pd.read_csv('https://www.sapiezynski.com/cs4910/markup/data.csv')
data['is_shipped_by_amazon'] /= 2
data['is_sold_by_amazon'] /= 2
data['is_amazon'] /= 2
data['is_top_clicked'] /= 2
dataset = data[columns]
train, test = train_test_split(dataset, test_size=0.2, stratify=dataset['placed_higher'])
```

Let's start with a model that only has the intercept (constant) and `is_amazon` as features.

```python
# adding the intercept as a variable (add column of ones to the dataframe)
train = add_constant(train)
# selecting which features to train on
features = ['const', 'is_amazon']
# training a logistic regression model. First the dependent (outcome variable), then the features
log_reg = sm.Logit(train['placed_higher'], train[features]).fit()
```

    Optimization terminated successfully.
             Current function value: 0.490868
             Iterations 7

```python
log_reg.summary()
```

Logit Regression Results

  Dep. Variable:	     placed_higher	    No. Observations:  	     1132

  Model:	                 Logit	        Df Residuals:      	     1130

  Method:	                 MLE	         Df Model:          	        1

  Date:	            Thu, 07 Apr 2022	   Pseudo R-squ.:     	   0.2918

  Time:	                23:32:11	       Log-Likelihood:    	   -555.66

  converged:	             True	         LL-Null:           	   -784.58

  Covariance Type:	     nonrobust	      LLR p-value:       	 1.424e-101

      	         coef	     std err	      z	      P>|z|	  [0.025	    0.975]

  const	         0.0645	     0.073	     0.882	  0.378	    -0.079	     0.208

  is_amazon	     2.6426	     0.178	    14.876	  0.000	     2.294	     2.991

We will mostly be looking at the bottom table.

`const` (the intercept) translates to the probability if all other variables are equal to 0

$$ p = \frac{e^{\beta_{const}}}{1+e^{\beta_{const}}} $$

```python
import numpy as np
# the beta coefficients are stored in the .params list, with the constant being first
# get the constant out of the fit model:
const = log_reg.params[0]

print("P (placed higher) = %.3f" %(np.e**const/(1+np.e**const)))
```

    P (placed higher) = 0.516

This should be very close to 50%. In our case if all variables are zero, it means that either:
1. The products are both from amazon
2. The products are both not from amazon

We don't have a way to differentiate them, so it's a coin toss (50%)

The other coefficients describe the change in odds that corresponds to a unit change in the variable.
* if the coefficient is positive, it means that an **increase** of variable corresponds to an **increase** in the odds of the positive outcome
* if the coefficient is negative, it means that an **increase** of variable corresponds to an **decrease** in the odds of the positive outcome

So what happens when `is_amazon` is equal to 1, i.e. one product is an amazon product and the other isn't?
By looking at the `coef` we already know it's going to go up (because the coefficient is positive). But by How much? Let's calculate:

$$ p = \frac{e^{\beta_{const} + \beta_{is\_amazon}}}{1+e^{\beta_{const} + \beta_{is\_amazon}}} $$

```python
numerator = np.e**(log_reg.params[0] + log_reg.params[1])
print("P (placed higher) = %.3f" %(numerator/(1+numerator)))
```

    P (placed higher) = 0.937

That means that in this dataset 93\% of the time we have two products where one is from amazon and the other isn't, it's the amazon product that will be on the first place.

Solved? Well no, because maybe this is just because amazon products are better. Let's include the star difference in the analysis to **control** for this:

```python
features = ['const', 'is_amazon','stars_delta', ]
log_reg = sm.Logit(train['placed_higher'], train[features]).fit()
log_reg.summary()
```

    Optimization terminated successfully.
             Current function value: 0.494329
             Iterations 6

Logit Regression Results

  Dep. Variable:	     placed_higher	    No. Observations:  	    1132

  Model:	                 Logit	        Df Residuals:      	    1129

  Method:	                 MLE	         Df Model:          	       2

  Date:	            Mon, 07 Feb 2022	   Pseudo R-squ.:     	  0.2868

  Time:	                11:21:33	       Log-Likelihood:    	  -559.58

  converged:	             True	         LL-Null:           	  -784.58

  Covariance Type:	     nonrobust	      LLR p-value:       	 1.925e-98

       	          coef	     std err	      z	      P>|z|	  [0.025	    0.975]

  const	           0.0867	     0.073	     1.185	  0.236	    -0.057	     0.230

  is_amazon	       2.4786	     0.163	    15.202	  0.000	     2.159	     2.798

  stars_delta	    -0.2737	     0.241	    -1.138	  0.255	    -0.745	     0.198

Now notice the last two columns - that's the 95% confidence interval. If one value is positive, and the other negative, the interval includes 0, which means we can't really tell if the effect is positive, or negative - it's not significant. Turns out, controling for the star rating doesn't change anything.

Let's add the review delta:

* **Write code to include `reviews_delta`**
* Is the `reviews_delta` *important*?

Let's include the rest of our variables:
* **Write code to include the other variables**
* Which variable is *important* among those included?
* How do you interpret its value? (*Hint*: look at the scenarios when this variable is equal to 0, and to 1)

What about the predictive performance? The Markup uses RandomForests with hyperparameter tuning to get the best prediction and ends up with 73.2% accuracy.
* **Calculate accuracy for your model**
* Which model would you give preference and why?

The problem that we looked at so far is defined as follows: given the top two items, which one is going to be placed in the first position?

Now, let's solve a slightly different problem formulation (not covered in the Markup write up): given characteristics of the product, how likely is it the top product on its search page?

Excercise 2: Interpreting the meaning of logistic regression coefficients

In this exercise you will interpret the values of the coefficients of a logistic regression model that tries to predict whether a product was the top product on its search page.

In particular, please answer these questions:
1. What is the probability that a hypothetical product with the lowest rating, no reviews, not sold/shipped/produced by amazon, and not among the top clicked products will be placed at the top?
1. Increase in which variables corresponds to a higher probability of being placed at the top?
1. How do you interpret the `stars` coefficient and its statistical significance?
1. If a product receives one more review without changing any other of its characteristics, how does it affect its chances to placed at the top?
    * How about 1000000 more reviews?
1. What is the probability that a five-star product with 1000 reviews, produced, sold, and shipped by amazon that was among the top clicked results will be placed on the top of the result list?
    * Is this number higher, or lower than you expected? Does it change how you interpret the results of the model for only the top two positions? Why?

```python
# let's get the data first:
dataset = pd.read_csv('https://www.sapiezynski.com/cs4910/markup/dataset_full.csv')
# the lowest star rating is 1, not 0, let's adjust the rating such that it starts at 0:
dataset['stars'] -= 1
dataset.head()
```

      stars
      reviews
      is_amazon
      is_sold_by_amazon
      is_shipped_by_amazon
      top_clicked
      is_top

      0
      3.8
      2120.0
      0.0
      1.0
      1.0
      0.0
      1.0

      1
      3.7
      3747.0
      0.0
      1.0
      1.0
      1.0
      0.0

      2
      3.8
      6031.0
      0.0
      1.0
      1.0
      1.0
      0.0

      3
      3.8
      7017.0
      0.0
      1.0
      1.0
      1.0
      0.0

      4
      3.8
      455.0
      0.0
      1.0
      1.0
      1.0
      0.0

This dataset is an aggregation of all first search result pages from the Markup audit that had any amazon products on them. The sponsored results are removed.

In this dataset each row is a product with its star rating, the number of reviews, the binary indicators of whether it's an amazon product, it's sold or shipped by amazon, whether it was among the most clicked products, and the outcome variable - whether it was the top product in its search results page.

We're looking at at most 24 first results, so on average only one in 24 rows is the top placed product:

```python
print("Fraction of top placed products (is_top = 1): %.2f %%" %(dataset['is_top'].mean()* 100))
```

    Fraction of top placed products (is_top = 1): 4.55 %

**You code here**


# Understanding the VGAE and GAE Models

The code from pygcn is designed for a simple GCN network so the model architecture is simpler than that in vgae-pytorch, which is designed for a (V)AE. We'll compare the two code bases and understand the VAE and AE architecture in the context of the math.

## pygcn Implementation (GCN only)

```python
import torch.nn as nn
import torch.nn.functional as F
from pygcn.layers import GraphConvolution

class GCN(nn.Module):
    def __init__(self, nfeat, nhid, nclass, dropout):
        super(GCN, self).__init__()

        self.gc1 = GraphConvolution(nfeat, nhid)
        self.gc2 = GraphConvolution(nhid, nclass)
        self.dropout = dropout

    def forward(self, x, adj):
        x = F.relu(self.gc1(x, adj)) # notice that the ReLU is applied outside of the GCN layer definition
        x = F.dropout(x, self.dropout, training=self.training) # does vgae-pytorch use dropout?
        x = self.gc2(x, adj)
        return F.log_softmax(x, dim=1) # note that the final output is using log-softmax
```

## vgae-pytorch Implementation

### VGAE

```python
import torch
import torch.nn as nn
import torch.nn.functional as F
import os
import numpy as np

import args

class VGAE(nn.Module):
	def __init__(self, adj):
		super(VGAE,self).__init__()
		self.base_gcn = GraphConvSparse(args.input_dim, args.hidden1_dim, adj) # first layer
        # the second layer is generated twice, once for the mean and once for the log-std dev
        # this correlates with the original VGAE paper which says that the mean and std dev GCN layers share W0 parameters
		self.gcn_mean = GraphConvSparse(args.hidden1_dim, args.hidden2_dim, adj, activation=lambda x:x)
		self.gcn_logstddev = GraphConvSparse(args.hidden1_dim, args.hidden2_dim, adj, activation=lambda x:x)

	def encode(self, X):
		hidden = self.base_gcn(X)
		self.mean = self.gcn_mean(hidden)
		self.logstd = self.gcn_logstddev(hidden)
        # the encoder adds N x Nlatent size Gaussian noise to Z during sampling of the latent space
		gaussian_noise = torch.randn(X.size(0), args.hidden2_dim)
		sampled_z = gaussian_noise*torch.exp(self.logstd) + self.mean
		return sampled_z

	def forward(self, X):
        # the full run of the VGAE is to encode a sample,
        # pull a random sample that is not exactly equal to the encoding of the input
        # and return the dot product of Z (the random sample) as the prediction Ahat
		Z = self.encode(X)
		A_pred = dot_product_decode(Z)
		return A_pred
```

### Explanation of Decoder

According to Kipf and Welling, the generative part of the model (i.e. the decoder) can be written as follows:

$$p(\mathbf{\hat{A}} | \mathbf{Z}) = \Pi_{i=1}^N \Pi_{j=1}^N p(\hat{A}_{ij} | \mathbf{z}_i, \mathbf{z}_j)$$

Where the probability distribution can be written as:

$$p(\hat{A}_{ij} | \mathbf{z}_i, \mathbf{z}_j) = \sigma(\mathbf{z}_i^T \mathbf{z}_j)$$

Where $\sigma(\cdot)$ is the logistic sigmoid function:

$$\sigma(x) = \frac{1}{1 + e^{-x}}$$

We use the logistic sigmoid function because we're using binary cross entropy on the loss function and we want to keep the range of output values of $\hat{\mathbf{A}}$ to between 0 and 1. We also use sigmoid activation when we are doing KL divergence...what exactly is the reason, then?

NOTE: I do not really understand why this works? Is it because the GCN layers will really learn to generate a latent space that can produce representations which can be multiplied in this way to return a good prediction?

```python
def dot_product_decode(Z):
    # the prediction itself is the sigmoid of the product of Z*Z.T
	A_pred = torch.sigmoid(torch.matmul(Z,Z.t()))
	return A_pred
```

### GAE

This model just removes any stochasticity and represents everything deterministically.

```python
class GAE(nn.Module):
	def __init__(self,adj):
		super(GAE,self).__init__()
		self.base_gcn = GraphConvSparse(args.input_dim, args.hidden1_dim, adj)
        # the second layer just represents the mean of the latent space
		self.gcn_mean = GraphConvSparse(args.hidden1_dim, args.hidden2_dim, adj, activation=lambda x:x)

	def encode(self, X):
		hidden = self.base_gcn(X)
		z = self.mean = self.gcn_mean(hidden)
		return z

	def forward(self, X):
		Z = self.encode(X)
		A_pred = dot_product_decode(Z) # decoding is the same as for the VAE
		return A_pred
```

## Loss Functions

Now let's talk about the loss function used to train the (V)AE. According to Kipf and Welling again, the VAE loss function is the ELBO:

$$\mathcal{L} = \mathbb{E}_{q(\mathbf{Z}|\mathbf{X,A})} [\log p(\mathbf{A}|\mathbf{Z})] - \text{KL}[q(\mathbf{Z}|\mathbf{X,A} || p(\mathbf{Z})]$$

Here $\text{KL}[q(\cdot) || p(\cdot)]$ is the Kullback-Leibler divergence between $q(\cdot)$ and $p(\cdot)$.

The paper says it also uses a Gaussian prior for $p(\mathbf{Z})$:
$$\begin{align} p(\mathbf{Z}) &= \Pi_i p(\mathbf{z}_i) \\
&= \Pi_i \mathcal{N}(\mathbf{z}_i | 0, \mathbf{I})\end{align}$$

After reviewing the code below, I believe we are using a very specific method of computing the KL divergence that formulates it as the "relative entropy between a diagonal multivariate normal (i.e. $q(\mathbf{Z}|\mathbf{X,A}$)( and a standard normal distribution (i.e. $p(\mathbf{Z})$)" (source: https://en.wikipedia.org/wiki/Kullback%E2%80%93Leibler_divergence):

$$\begin{align}D_{KL}\bigg( \mathcal{N}\big((\mu_1, ... , \mu_k)^T, \text{diag}(\sigma_1^2, ..., \sigma_k^2) \big) || \mathcal{N}(0, \mathbf{I}) \bigg) &= \frac{1}{2} \sum_{i=1}^k (- 1 - \ln(\sigma_i^2) + \sigma_i^2 + \mu_i^2 ) \\
&= \frac{1}{2N} \text{mean}(\text{sum}(\big(1 + 2 ln(\sigma_i) - (e^{ln(\sigma_i)})^2 - \mu_i^2 \big))) \end{align}$$

I think the mean might be redundant here. Note that the middle two terms are equivalent in the two expressions, we just write the second row a specific way because the log of the standard deviation has been computed as one monolithic term by the second layer of the GCN.

It also says that if $\tilde{\mathbf{A}}$ is very sparse, then it can help to reweight terms with $\tilde{\mathbf{A}}_{ij}=1$ in the loss function, $\mathcal{L}$. (Although I cannot see where we do that in the code?)

Finally, training is done with full-batch gradient descent, and the reparameterization trick (see here: https://sassafras13.github.io/ReparamTrick/) is used to handle the stochasticity of the VAE (not necessary for the AE).

### What about the AE?

The AE is deterministic so instead of using the KL divergence for the loss function, we use binary cross entropy, aka the log-loss function (see here: https://sassafras13.github.io/BiCE/). This is essentially doing the same thing as KL divergence - it is measuring the difference between two distributions:

$$H_p(q) = - \frac{1}{N} \sum_{i=1}^N y_i \log(p(y_i)) + (1 - y_i)\log(1 - p(y_i))$$

```python
# Create Model
pos_weight = float(adj.shape[0] * adj.shape[0] - adj.sum()) / adj.sum()
norm = adj.shape[0] * adj.shape[0] / float((adj.shape[0] * adj.shape[0] - adj.sum()) * 2)

adj_label = adj_train + sp.eye(adj_train.shape[0])
adj_label = sparse_to_tuple(adj_label)

adj_norm = torch.sparse.FloatTensor(torch.LongTensor(adj_norm[0].T),
                            torch.FloatTensor(adj_norm[1]),
                            torch.Size(adj_norm[2]))
adj_label = torch.sparse.FloatTensor(torch.LongTensor(adj_label[0].T),
                            torch.FloatTensor(adj_label[1]),
                            torch.Size(adj_label[2]))
features = torch.sparse.FloatTensor(torch.LongTensor(features[0].T),
                            torch.FloatTensor(features[1]),
                            torch.Size(features[2]))

weight_mask = adj_label.to_dense().view(-1) == 1
weight_tensor = torch.ones(weight_mask.size(0))
weight_tensor[weight_mask] = pos_weight

# init model and optimizer
model = getattr(model,args.model)(adj_norm)
optimizer = Adam(model.parameters(), lr=args.learning_rate)

# train model
for epoch in range(args.num_epoch):
    t = time.time()

    A_pred = model(features)
    optimizer.zero_grad()
    # the AE uses binary cross entropy
    loss = log_lik = norm*F.binary_cross_entropy(A_pred.view(-1), adj_label.to_dense().view(-1), weight = weight_tensor)
    if args.model == 'VGAE':
        # I think this is the expression for KL divergence for multivariate normal distributions that have a diagonal multivariate normal
        kl_divergence = 0.5/ A_pred.size(0) * (1 + 2*model.logstd - model.mean**2 - torch.exp(model.logstd)**2).sum(1).mean() # is this mean redundant?
        loss -= kl_divergence

    loss.backward()
    optimizer.step()

    train_acc = get_acc(A_pred,adj_label)

    val_roc, val_ap = get_scores(val_edges, val_edges_false, A_pred)
    print("Epoch:", '%04d' % (epoch + 1), "train_loss=", "{:.5f}".format(loss.item()),
          "train_acc=", "{:.5f}".format(train_acc), "val_roc=", "{:.5f}".format(val_roc),
          "val_ap=", "{:.5f}".format(val_ap),
          "time=", "{:.5f}".format(time.time() - t))

test_roc, test_ap = get_scores(test_edges, test_edges_false, A_pred)
print("End of training!", "test_roc=", "{:.5f}".format(test_roc),
      "test_ap=", "{:.5f}".format(test_ap))
```


# 1. Imports

```python
import dornbusch as db
```

# 2. Model

#### Consider the Dornbusch model from the lecture slides.

The money market equilibrium:
\begin{align}
    m - p_t &= \eta y - \sigma r_t
\end{align}

The international bond market equilibrium where expectations are regressive:
\begin{align}
    \mathbb{E}_t(s_{t+1}) - s_t &= r_t - r^* \\
    \mathbb{E}_t(s_{t+1}) - s_t &= \theta(\bar{s} - s_t)
\end{align}

The domestic goods market and the Phillips curve:
\begin{align}
    d_t = \beta + \alpha(s_t \ + \ &p^* - p_t) + \phi y - \lambda r_t \\
    p_{t+1} - p_t &= \pi (d_t - y)
\end{align}

Finally, the long-run equilibrium condition for the goods market is
\begin{align}
    d_{LR} = y
\end{align}

# 3. Simulation

The simulation is a one-time increase in the money supply in the $5^{th}$ period. I used the following parameter values:

\begin{align}
\eta = 0.8 \quad &\text{income elasticity of demand for money} \\
\sigma = 0.8 \quad &\text{interest rate semielasticity of demand for money} \\
\beta = 0.0 \quad &\text{shift parameter} \\
\alpha = 1.0 \quad &\text{responsiveness of domestic goods demand to the real exchange rate} \\
\phi = 0.5 \quad &\text{income elasticity of demand for domestic goods} \\
\lambda = 0.0 \quad &\text{interest rate semielasticity of demand for domestic goods} \\
p^* = 0.0 \quad &\text{foreign price level} \\
y = 1 \quad &\text{real income/output} \\
r^* = 0.05 \quad &\text{foreign interest rate} \\
\end{align}

The time horizon is set to 100. Furthermore, it is possible to manually change the degree of price flexibility, $\pi$. Recall from the lecture slides that if $\pi \rightarrow \infty$, the graph would jump from the initial to the final equilibrium. Below I use the following transformation to cover the whole range from 0 until $\infty$:
\begin{align}
    \pi = \frac{\tilde{\pi}}{1 - \tilde{\pi}}
\end{align}
That is, when $\tilde{\pi} \rightarrow 1$, then $\pi \rightarrow \infty$.

```python
db.simulate()
```

    interactive(children=(FloatSlider(value=0.2, description='$\\pi$', max=5.0, min=0.01), Output()), _dom_classes…