Electron Spectrometer 2D Simulation

Uses given magnetic fields to determine electron trajectories for various energies. Compares the results to an analytical expression that assumes a uniform magnetic field. The basic setup is given in the image below. The coordinate system used throughout this program is also defined here.

Imports and Fundamental Constants

Import python packages and define commonly used physical constants

```python
# Electron spectrometer 2D simulation code

#Imports
from math import *
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import sympy as sym
from pylab import *
from mpl_toolkits.mplot3d import Axes3D
import seaborn as sns
import scipy.interpolate
from scipy.stats import maxwell
import matplotlib.cbook as cbook
from scipy.optimize import curve_fit
import os

#Import custom modules
from physics import *

%matplotlib notebook
```

Program specific constants

Define and calculate constants used throughout this particular program.

```python
#Program specific constants
length = 3.0 #magnet and interaction length in inches
width = 0.6 #interaction width in inches
magwidth = 2.0 #magnet width in inches
magsep = 0.125 #magnet separation in inches
pinholediameter = 500 #pinhole diameter in um
numbins = 300 #number of bins
KEmin = 60 #minimum desired electron KE to be detected in keV
KEmax = 4000 #maximum desired electron KE to be detected in keV

#Conversion to base units
length = length*0.0254
width = width*0.0254
magwidth = magwidth*0.0254
magsep = magsep*0.0254
pinholediameter = pinholediameter*10**-6
KEmin = KEmin*1000*q
KEmax = KEmax*1000*q

#Calculated constants
binwidth = length/numbins #screen bin width
yf = width #final x-position when at screen
B0 = 2*me*c/(q*yf)*sqrt((KEmin/(me*c**2)+1)**2-1) #maximum magnetic field (in T) required to detect electrons with KE=KEmin
##B0 = 2*me*c*yf/(q*(length**2+yf**2))*sqrt((KEmax/(me*c**2)+1)**2-1) # miniumum magnetic field (in T) required to detect electrons with KE=KEmax
B0 = 50./1000
print("magnetic field =",B0*1000,"mT")
```

    magnetic field = 50.0 mT

Define Functions

Here we define the analytical expressions. These have been derived assuming a uniform magnetic field and relativistic electrons. The radius of the electron trajectory in this case is given by

$$ r = \frac{m_e c}{e B_0} \sqrt{ \left( \frac{E}{m_e c^2} + 1 \right)^2 - 1 }$$

where $B_0$ is the magnetic field strength and $E$ is the electron energy. In the simplest case, the detector is placed at the edge of the magnet. The final electron position along the detector as a function of energy is then given by

$$ z = \sqrt{ r^2 - (y-r)^2)} $$

where y is the distance from the edge of the detector that the electron is injected.

```python
#Defined analytic functions
def KEcalc(z,y):
    """Returns KE in J given z-position in m"""
    return me*c**2*(sqrt((q*B0/(me*c))**2*((z**2+y**2)/(2*y))**2+1)-1)

def Radius(KE):
    """Returns radius of electron orbit in m given KE in keV"""
    return me*c/(q*B0)*sqrt((KE*1000*q/(me*c**2)+1)**2-1)

def zfcalc(KE,y):
    """Returns the z-position at the screen in inches given the KE in keV and the distance between the
    electron injection position and the screen (y) in m"""
    R = Radius(KE)
    return sqrt(R**2 - (y-R)**2)

#Above functions were generalized for a screen standing off of the magnets by some distance
def zfcalcGeneral(KE,yM,y):
    """Returns the z-position at the screen in inches given the KE in keV, and the distance between the
    electron injection position and the screen (y) in m, and the magnet edge y-position yM in m"""
    R = Radius(KE)
    zM = zfcalc(KE,yM) #Final z-position of the electrons once they reach the magnet edge
    return zM + (y - yM)*(R - yM)/zM

def KEcalcGeneral(zf,yM,yf):
    """Returns KE in J given z-position of electrons, y-position of magnet edge, and y-position of screen, all in m"""
    a = (yM+yf)**2
    b = -2*yM*(yf*(yM+yf)+zf**2)
    d = yM**2*(zf**2+yf**2)
    f = (me*c)/(q*B0)
    g = (-b+sqrt(b**2-4*a*d))/(2*a)
    return me*c**2*(sqrt(g**2+f**2)/f - 1)

def AngleIncidence(KE,yM):
    """Returns the angle at which the electrons are incident on the screen"""
    R = Radius(KE)
    return asin((R-yM)/R)
```

## Numerical Simulation with uniform magnetic field in 2D (relativistic)

```python
#Create Figure
fig = plt.figure(figsize=(16,12))
ax = fig.add_subplot(121)

#Input constants
B0 = 2136.0 #Magnetic field strength in Gauss
B0 = B0/10**4
yM = 0.5 #edge of magnet relative to slit in inches
yf = yM+0.12 #screen position relative to slit in inches
length = 3.0 #magnet and interaction length in inches
width = 0.5 #interaction width in inches
magwidth = 2.0 #magnet width in inches
magsep = 0.125 #magnet separation in inches

#Conversion to base units
yf = yf*.0254
yM = yM*.0254
length = length*0.0254
width = width*0.0254
magwidth = magwidth*0.0254
magsep = magsep*0.0254

#Electron kinetic energy in keV
KE = [round(KEcalcGeneral(9.5*10**-3,yM,yf)/(1000*q),1),round(KEcalcGeneral(length/4,yM,yf)/(1000*q),1),\
      round(KEcalcGeneral(length/3,yM,yf)/(1000*q),1),round(KEcalcGeneral(length/2,yM,yf)/(1000*q),1),\
      round(KEcalcGeneral(length/1.5,yM,yf)/(1000*q),1),round(KEcalcGeneral(length/1.25,yM,yf)/(1000*q),1),\
      round(KEcalcGeneral(length/1.1,yM,yf)/(1000*q),1),round(KEcalcGeneral(length,yM,yf)/(1000*q),1)]

for i in range(len(KE)):

    #Initial conditions
    KE[i] = KE[i]*1000*q #Convert to J
    v = [0,0,c*sqrt(1-(1/(KE[i]/(me*c**2)+1))**2)] #initial velocity
    gamma = 1/sqrt(1-v[2]**2/c**2)
    p = [0,0,gamma*me*v[2]] #initial momentum
    r = [0,0,0] #initial position
    dt = 10**-14 #time step in s

    ypos = []
    zpos = []
    while r[1]<yf:
        #update magnetic field
        if r[1]<=yM:
            B = B0
        else:
            B = 0

        #update velocity (since the magnetic field does no work, |v| and gamma are constant)
        v[1] += q*v[2]*B/(me*gamma)*dt
        v[2] += -q*v[1]*B/(me*gamma)*dt

        #update position:
        r[1] += v[1]*dt
        r[2] += v[2]*dt

        #record position:
        ypos.append(r[1]/.0254)
        zpos.append(r[2]/.0254)

        #prevent infinite loop if electrons never reach screen:
        if r[1]<0:
            break

    if i==0:
        print('KE (keV)','\t','Simulation z-position (in)','\t','Theory z-position (in)','\t','Incident Angle (degrees)')

    if r[1]<0:
        print(KE[i]/(1000*q),'keV electrons never reach the detector')
    else:
        plt.plot(ypos,zpos,linewidth=2,color=[0,1,0])
        plt.xlim(yf/0.0254-magwidth/0.0254,yf/.0254)
        plt.ylim(0,length/.0254)
        annotation = '{KE} keV e$^-$'.format(KE=str(KE[i]/(1000*q)))
        ax.annotate(annotation, xy=(yf/.0254, r[2]/.0254), xytext=(yf/.0254+0.25, r[2]/.0254-0.01),\
                    arrowprops=dict(facecolor='black', shrink=0.05, width=1.0, headwidth=5.0))
        print(KE[i]/(1000*q),'\t \t',round(r[2]/.0254,6),'\t \t \t',round(zfcalcGeneral(KE[i]/(1000*q),yM,yf)/0.0254,6),\
        '\t \t',round(atan(v[2]/v[1])*360/(2*pi),6), '\t \t', round(AngleIncidence(KE[i]/(1000*q),yM)*360/(2*pi),6))
```

    <IPython.core.display.Javascript object>

    KE (keV) 	 Simulation z-position (in) 	 Theory z-position (in) 	 Incident Angle (degrees)
    329.9 	 	 0.37382 	 	 	 0.373957 	 	 -12.584898 	 	 -12.578099
    812.4 	 	 0.74989 	 	 	 0.74998 	 	 19.496766 	 	 19.500121
    1349.6 	 	 0.999948 	 	 	 0.999999 	 	 33.054324 	 	 33.053473
    2937.7 	 	 1.499943 	 	 	 1.499999 	 	 49.60056 	 	 49.600949
    5192.0 	 	 1.999966 	 	 	 1.999995 	 	 58.961872 	 	 58.961402
    7467.7 	 	 2.399952 	 	 	 2.399997 	 	 63.878606 	 	 63.878187
    9640.1 	 	 2.727261 	 	 	 2.727272 	 	 66.894101 	 	 66.894189
    11663.4 	 	 3.000025 	 	 	 3.000003 	 	 68.92981 	 	 68.929581

## Load and plot magnetic field

```python
#Set tick padding
mpl.rcParams['xtick.major.pad'] = 16
mpl.rcParams['ytick.major.pad'] = 6

especFolderPath = os.curdir + '/Magnetic_Fields'

RadiaFieldPath = especFolderPath + '/3Dmagnetostaticespecdata.csv'
fname = cbook.get_sample_data(RadiaFieldPath, asfileobj=False)

# FakeFieldPath = especFolderPath + '/measuredfakefull_Bx.csv'
# FakeFieldycoordPath = especFolderPath + '/measuredfakefull_ycoord.csv'
# FakeFieldzcoordPath = especFolderPath + '/measuredfakefull_zcoord.csv'

RealFieldPath = especFolderPath + '/march7fields_Bx.csv'
RealFieldycoordPath = especFolderPath + '/march7fields_ycoord.csv'
RealFieldzcoordPath = especFolderPath + '/march7fields_zcoord.csv'

# Bx = np.matrix.transpose(np.loadtxt(open(FakeFieldPath, "rb"), delimiter=","))
# Bx = np.multiply(Bx, 0.1)
# Y = np.matrix.transpose(np.loadtxt(open(FakeFieldycoordPath, "rb"), delimiter=","))
# Z = np.matrix.transpose(np.loadtxt(open(FakeFieldzcoordPath, "rb"), delimiter=","))

Bx = np.loadtxt(open(RealFieldPath, "rb"), delimiter=",")
Bx = np.multiply(Bx, 10**-3)
Y = np.loadtxt(open(RealFieldycoordPath, "rb"), delimiter=",")
Z = np.loadtxt(open(RealFieldzcoordPath, "rb"), delimiter=",")

Ny = len(Bx)
Nz = len(Bx[0])

#Convert 1D radia array to 3D array
# Bx1DArray = np.genfromtxt(fname,delimiter=",",usecols=(5))
# By1DArray = np.genfromtxt(fname,delimiter=",",usecols=(3))
# Bz1DArray = np.genfromtxt(fname,delimiter=",",usecols=(4))
# Nx = 3 #Number of divisions along specified dimension
# Ny = 61
# Nz = 87
# Bx = np.zeros((Nx,Ny,Nz))
# By = np.zeros((Nx,Ny,Nz))
# Bz = np.zeros((Nx,Ny,Nz))
# X = range(Nx)
# Y = range(Ny)
# Z = range(Nz)
# Y, Z = np.meshgrid(Z, Y)
# for i in range(Nx):
#     for j in range(Ny):
#         for k in range(Nz):
#             Bx[i][j][k] = Bx1DArray[k+j*Nz+i*Ny*Nz]
#             By[i][j][k] = By1DArray[k+j*Nz+i*Ny*Nz]
#             Bz[i][j][k] = Bz1DArray[k+j*Nz+i*Ny*Nz]

#Plot
fig = plt.figure(figsize=(5,8))
#plt.imshow(Bx,cmap='viridis')
plt.pcolormesh(Y,Z,Bx,cmap='viridis',shading='gouraud')
plt.xlim(np.min(Y),np.max(Y))
plt.ylim(np.min(Z),np.max(Z))
plt.colorbar()
plt.tight_layout()
# ax = fig.add_subplot(111, projection='3d')
# field = ax.plot_surface(Y,Z,Bx,rstride=1, cstride=1, cmap='viridis')
# fig.colorbar(field, shrink=0.7, aspect=10)
```

    <IPython.core.display.Javascript object>

## Magnetic field interpolation

```python
#2D interpolation
y00 = Y[0,0]*10**-3
z00 = Z[0,0]*10**-3
deltay = 1.0*10**-3
deltaz = 1.0*10**-3
SimSizey = (Ny-1)*deltay
SimSizez = (Nz-1)*deltaz
def Binterp2D(y,z):
    """Uses bilinear interpolation to determine magnetic field at any given point in space (see wikipedia)"""
    #Lattice points below (x,y,z). Here n refers to the index and (x0,y0,z0) to the physical position:
    ny0 = int(floor((y-y00)/deltay))
    nz0 = int(floor((z-z00)/deltaz))
    y0 = y00 + ny0*deltay
    z0 = z00 + nz0*deltaz
    #Lattice points above (x,y,z). Here n refers to the index and (x1,y1,z1) to the physical position:
    ny1 = int(ceil((y-y00)/deltay))
    nz1 = int(ceil((z-z00)/deltaz))
    y1 = y00 + ny1*deltay
    z1 = z00 + nz1*deltaz
    return 1/((y1-y0)*(z1-z0))*(Bx[ny0][nz0]*(y1-y)*(z1-z) + Bx[ny1][nz0]*(y-y0)*(z1-z)\
                                + Bx[ny0][nz1]*(y1-y)*(z-z0) + Bx[ny1][nz1]*(y-y0)*(z-z0))
```

## Numerical simulation with measured magnetic field in 2D (relativistic)

```python
#Input constants
B0 = 2136.0 #Magnetic field strength in Gauss
r0 = [0,-12.7,-12.7] #initial position in mm
yM = 0.5 #magnet edge position in inches
CCDpos = 3.02 #CCD y-position relative to magnet edge in mm
length = 3.0 #magnet and interaction length in inches
width = 0.5 #interaction width in inches
magwidth = 2.0 #magnet width in inches
magsep = 0.125 #magnet separation in inches

#Conversion to base units
B0 = B0/10**4
yM = yM*.0254
length = length*0.0254
width = width*0.0254
magwidth = magwidth*0.0254
magsep = magsep*0.0254
r0 = np.multiply(r0,10**-3)
CCDpos = CCDpos*10**-3

#Create Figures
# fig1 = plt.figure(figsize=(16,16.4))
# ax1 = fig1.add_subplot(121)
# fig2 = plt.figure(figsize=(8,8))
# ax2 = fig2.add_subplot(111)

#Main Program (older code, should be vectorized)
yf = yM + CCDpos #Screen position
# KE = [225.0, 500.0, 1000.0, 1570.0, round(KEcalcGeneral((1500*8+9500)*10**-6,yM,yf)/(1000*q),1), \
#       round(KEcalcGeneral((2200*8+9500)*10**-6,yM,yf)/(1000*q),1)] #Electron kinetic energy in keV
# KE = [200.0, 500.0, 1000.0, 1700.0]
KE = np.arange(200.0,1710.0,10)
zfinal = []
zfinalTheory = []
Angle = []
AngleSim = []
arclengthArray = [] #arclength of trajectory
for i in range(len(KE)):

    #Initial conditions
    KE[i] = KE[i]*1000*q #Convert to J
    v = [0,0,c*sqrt(1-(1/(KE[i]/(me*c**2)+1))**2)] #initial velocity
    gamma = 1/sqrt(1-v[2]**2/c**2)
    p = [0,0,gamma*me*v[2]] #initial momentum
    r = np.copy(r0) #initial position
    dt = 10**-13 #time step in s

    ypos = []
    zpos = []
    Barray = []
    tarray = []
    t = 0 #initial time
    arclength = 0 #arclength of trajectory
    while r[1]<CCDpos:

        #if the electrons take more than a nanosecond, something wicked occurred
        if t>10*10**-9:
            break

        if r[1]==r0[1]:
            B = Bx[r0[1]*10**3,r0[2]*10**3]
        else:
            B = Binterp2D(r[1],r[2])

        #update velocity (since the magnetic field does no work, |v| and gamma are constant)
        v[1] += -q*v[2]*B/(me*gamma)*dt
        v[2] += q*v[1]*B/(me*gamma)*dt

        #update position:
        yold = r[1]
        zold = r[2]
        r[1] += v[1]*dt
        r[2] += v[2]*dt
        arclength += sqrt((r[1]-yold)**2+(r[2]-zold)**2)

        #record position:
        ypos.append(r[1]*10**3)
        zpos.append(r[2]*10**3)

        #record magnetic field:
        Barray.append(B*10**3)

        #record time:
        tarray.append(t*10**12)

        #prevent infinite loop if electrons never reach screen:
        if r[2]>33*10**-3:
            break

        #increment time
        t+=dt

    arclengthArray.append(arclength)
#     if i==0:
#         print('KE (keV)','\t','Simulation z-position (mm)','\t','Theory z-position (mm)','\t','Incident Angle (degrees)')

#     if r[2]>40*10**-3:
#         print(KE[i]/(1000*q),'keV electrons never reach the detector')
#     else:
#         print(KE[i]/(1000*q),'\t \t',round(r[2]*10**3,6),'\t \t \t',\
#         round(zfcalcGeneral(KE[i]/(1000*q),-r0[1], -r0[1]+CCDpos)*10**3,6), '\t \t',\
#         round(atan(v[2]/v[1])*360/(2*pi),6), '\t \t', round(AngleIncidence(KE[i]/(1000*q),yM)*360/(2*pi),6),\
#         '\t \t', round(arclength,4)*10**2)

    zfinal.append(r[2]*10**3)
    zfinalTheory.append(zfcalcGeneral(KE[i]/(1000*q),-r0[1], -r0[1]+CCDpos)*10**3)
    Angle.append(AngleIncidence(KE[i]/(1000*q),yM))
    AngleSim.append(atan(v[2]/v[1]))
#     ax1.plot(ypos,zpos,linewidth=2,color=[0,1,0])
#     ax1.set_xlim(r0[1]*10**3-10,CCDpos*10**3)
#     ax1.set_ylim(r0[2]*10**3,40)
#     annotation = '{KE} keV e$^-$'.format(KE=str(KE[i]/(1000*q)))
#     ax1.annotate(annotation, xy=(CCDpos*10**3, r[2]*10**3), xytext=(CCDpos*10**3+5, r[2]*10**3+2),\
#                 arrowprops=dict(facecolor='black', shrink=0.05, width=1.0, headwidth=5.0))

# ax1.set_xlabel('Position (mm)')
# ax1.set_ylabel('Position (mm)')
# ax2.plot(tarray,Barray,linewidth=2,color=[0,0,1])
# ax2.set_xlabel('Time (ps)')
# ax2.set_ylabel('Magnetic field (mT)')
# fig1.savefig('Trajectories.svg',bbox_inches='tight')

KESim = np.multiply(KE, 1/(1000*q))
```

    /home/drake/.anaconda3/lib/python3.4/site-packages/ipykernel/__main__.py:63: VisibleDeprecationWarning: using a non-integer number instead of an integer will result in an error in the future

```python
KE = np.arange(200.0,2000.0,10)
zfcalcGeneralVectorized = np.vectorize(zfcalcGeneral)
zfinalTheory = zfcalcGeneralVectorized(KE,-r0[1], -r0[1]+CCDpos)*10**3
```

```python
i = -1
print(zfinalTheory[i])
zfinalTheoryPixels = np.multiply(np.subtract(zfinalTheory, 9.5), 1000/8.0)
print(zfinalTheoryPixels[i],KEcalcGeneral(np.multiply(zfinalTheory[i],10**-3),yM,yf)/(10**6*q))
```

    31.1665092108
    2708.31365135 1.99

```python
# Arclength_vs_Energy = np.vstack((np.divide(KE,1000*q),np.multiply(arclengthArray,10**2)))
# np.savetxt('Arclength_vs_Energy.csv',np.transpose(Arclength_vs_Energy),delimiter=',')
```

## Plot kinetic energy vs. position

```python
sns.set(context='poster',font_scale=1.5)
sns.set_style("darkgrid")
sns.set_palette(palette='deep')
sns.set_color_codes(palette='deep')

#Set tick padding
mpl.rcParams['xtick.major.pad'] = 16
mpl.rcParams['ytick.major.pad'] = 6

B0 = 1710*10**-4
zfinalPixels = np.multiply(np.subtract(zfinal, 9.5), 1000/8.0)
zfinalTheoryPixels = np.multiply(np.subtract(zfinalTheory, 9.5), 1000/8.0)
fig1, ax1 = plt.subplots(figsize=(10,6))
ax1.plot(zfinalPixels,np.divide(KESim,1000),linewidth=2, color='b', label='Particle Tracking Code')
ax1.plot(zfinalTheoryPixels,KEcalcGeneral(np.multiply(zfinalTheory,10**-3),yM,yf)/(10**6*q)\
         ,linewidth=2, color='r', label='Analytical Expression',linestyle='--')
ax1.set_ylabel('Electron Energy (MeV)')
ax1.set_xlabel('Pixel Number')
ax1.set_xlim(0,3648)
plt.ticklabel_format(style='sci', axis='x', scilimits=(0,0))
plt.legend(loc=2)
plt.title('Energy Calibration')
plt.subplots_adjust(left=0.14,bottom=0.16) #Adjust spacing to prevent clipping of x and y labels
#plt.savefig('Energy_Calibration.svg')
print()
```

    /home/drake/.anaconda3/lib/python3.4/site-packages/matplotlib/__init__.py:872: UserWarning: axes.color_cycle is deprecated and replaced with axes.prop_cycle; please use the latter.
      warnings.warn(self.msg_depr % (key, alt_key))

    <IPython.core.display.Javascript object>

```python
KE = []
zposition = []
for i in linspace(0,66.7,10000):
    zposition.append(i+9.5)
    KE.append(KEcalcGeneral(np.multiply(i+9.5,10**-3),yM,yf)/(1000*q))

# np.savetxt("EvsZ.csv", KE, delimiter=",")
# np.savetxt("Z.csv", zposition, delimiter=",")
# np.savetxt("KE.csv", np.arange(250.0,1580.0,10), delimiter=",")
# np.savetxt("Angle.csv", Angle, delimiter=",")
```

Electron Spectrometer Simulation 2D.ipynb


## Serological Shields

Here, we use an age-structured epidemiological model to investigate how age-structured decision making may influence disease dynamics. The model incorporates 10 age classes (0-9 years old, 10-19 years old, ... 80-90 years old, 90+ years old); and 9 epidemiological states. The population who have not encountered the disease can be in a susceptible ($S$) class where there is the potential of being infected. Upon being exposed ($E$), the disease progresses either asymptomatically ($I_{asym}$) or symptomatically ($I_{sym}$). Of those who display symptoms, some will recover alone, but others will require sub-critical ($I_{h,sub}$) or critical (ICU beds required) ($I_{h,crit}$) hospital care. Of those in critical condition some will be killed by the disease ($D$). Those who do not die from the disease recover ($R$) with immunity to the disease. If recovered individuals can be identified they can take up key positions requiring face-to-face interactions, without further spreading the disease. They will not be able to catch the disease from infected individuals, and will not be able to transmit the disease to the susceptible population. Therefore, it is beneficial for people to interact with those who are serologically shown to have recovered from covid-19 and who are no longer shedding viruses. In this way identified recovered individuals can act as "serological shields" through becoming preferential contacts for those who have not been tested.

```julia
#load package dependencies
using Plots, DifferentialEquations, LaTeXStrings, StatsPlots, Pkg
#show package versions
Pkg.status()
```

    ┌ Info: Precompiling DifferentialEquations [0c46a032-eb83-5123-abaf-570d42b7fbaa]
    └ @ Base loading.jl:1273
    ┌ Info: Precompiling StatsPlots [f3b207a7-027a-5e70-b257-86293d7955fd]
    └ @ Base loading.jl:1273

        Status `C:\Users\Steve\.julia\environments\v1.3\Project.toml`
      [336ed68f] CSV v0.5.26
      [a93c6f00] DataFrames v0.20.2
      [aae7a2af] DiffEqFlux v1.3.2
      [0c46a032] DifferentialEquations v6.10.1
      [587475ba] Flux v0.10.1
      [7073ff75] IJulia v1.21.1
      [c601a237] Interact v0.10.3
      [b964fa9f] LaTeXStrings v1.1.0
      [47be7bcc] ORCA v0.3.1
      [429524aa] Optim v0.20.1
      [1dea7af3] OrdinaryDiffEq v5.34.1
      [f0f68f2c] PlotlyJS v0.13.1
      [91a5bcdd] Plots v0.28.4
      [d330b81b] PyPlot v2.8.2
      [6f49c342] RCall v0.13.4
      [90137ffa] StaticArrays v0.12.1
      [f3b207a7] StatsPlots v0.14.2
      [fd094767] Suppressor v0.1.1
      [fce5fe82] Turing v0.8.3
      [30578b45] URIParser v0.4.0
      [0ae4a718] VegaDatasets v2.0.0
      [112f6efa] VegaLite v2.0.1
      [0f1e0344] WebIO v0.8.13
      [fdbf4ff8] XLSX v0.6.0

```julia
#show Julia version
VERSION
```

    v"1.3.1"

First, we define a population age structure. Here, we define a population of 10,000,000 people with an age structure similar to that of the UK and the US. Let us contain the population demography in an object.

```julia
#Define population structure
struct POPDEMOG
    AgeStructure #fraction of population in each age class
    PopulationSize #number of people in population
end

ExamplePOP = POPDEMOG([0.12,0.14,0.14,0.13,0.13,0.13,0.10,0.06,0.04,0.01],10*10^6)

MinAge = [0,10,20,30,40,50,60,70,80,90]
MiddleAge = [5,15,25,35,45,55,65,75,85,95]

#Plot the population structure
plot(MiddleAge,ExamplePOP.AgeStructure*100.0,xlabel="Age",ylims=(0,20),ylabel = "Pop.Demography (%)",legend = false,yguidefontsize=8,markershape = :auto,markersize=2)
savefig("ExampleDemography.pdf")
current()
```

We define the number of states in the model; and the indices that will refer to these states.

```julia
#Define state indexes
NumAgeClasses = 10; #0-9,10-19,20-29,30-39,40-49,50-59,60-69,70-79,80-89,90+
NumEpiStates = 8; # S,E,Ia,Is,Ihsub,Ihcrit,R,D

#Indexing
S_ind = 1:NumAgeClasses
E_ind = (1*NumAgeClasses+1):2*NumAgeClasses
Ia_ind = (2*NumAgeClasses+1):3*NumAgeClasses
Is_ind = (3*NumAgeClasses+1):4*NumAgeClasses
Ihsub_ind = (4*NumAgeClasses+1):5*NumAgeClasses
Ihcrit_ind = (5*NumAgeClasses+1):6*NumAgeClasses
R_ind = (6*NumAgeClasses+1):7*NumAgeClasses
D_ind = (7*NumAgeClasses+1):8*NumAgeClasses
```

    71:80

The system of nonlinear differential equations governing this age-structured epidemiological are as follows:

\begin{align}
\frac{dS(a)}{dt} & =  -\overset{new\ cases\ from\ symptomatic\ contact}{\overbrace{\frac{\beta_S S(a) I_{sym,Tot}}{N_{Tot} + \alpha R_{Shields}} }}  - \overset{new\ cases\ from\ asymptomatic\ contact}{\overbrace{\frac{\beta_A S(a) I_{asym,Tot}}{N_{Tot} + \alpha R_{Shields}} }}  \\
\frac{dE(a)}{dt} & =  \overset{new\ cases\ from\ symptomatic\ contact}{\overbrace{ \frac{\beta_S S(a) I_{sym,Tot}}{N_{Tot} + \alpha R_{Shields}} }} + \overset{new\ cases\ from\ asymptomatic\ contact}{\overbrace{\frac{\beta_A S(a) I_{asym,Tot}}{N_{Tot} + \alpha R_{Shields}} }}  - \overset{onset\ of\ disease}{\overbrace{\gamma_E E(a)}} \\
\frac{dI_{asym}(a)}{dt} & =  \overset{asymptomatic\ onset}{\overbrace{p(a)\gamma_E E(a)}} - \overset{recovery}{\overbrace{\gamma_A I_{asym}(a)}} \\
\frac{dI_{sym}(a)}{dt} & =  \overset{symptomatic\ onset}{\overbrace{(1-p(a))\gamma_E E(a)}} - \overset{transfer\ from\ I_{sym}}{\overbrace{\gamma_S I_{sym}(a)}} \\
\frac{dI_{h,sub}(a)}{dt} & =  \overset{subcritical\ cases}{\overbrace{h(a)(1-\xi(a)) \gamma_S I_{sym}(a)}} - \overset{transfer\ from\ I_{h,sub}}{\overbrace{\gamma_h I_{h,sub}(a)}} \\
\frac{dI_{h,crit}(a)}{dt} & =  \overset{critical\ (ICU)\ cases}{\overbrace{h(a)\xi(a) \gamma_S I_{sym}(a)}} - \overset{transfer\ from\ I_{h,crit}}{\overbrace{\gamma_h I_{h,crit}(a)}} \\
\frac{dR(a)}{dt} & =  \overset{recovery\ from\ I_{asym}}{\overbrace{\gamma_A I_{asym}(a)}} + \overset{recovery\ from\ I_{sym}}{\overbrace{(1-h(a))\gamma_S I_{sym}(a)}} + \overset{recovery\ from\ I_{h,sub}}{\overbrace{\gamma_h I_{h,sub}(a)}} + \overset{recovery\ from\ I_{h,crit}}{\overbrace{(1-\mu)\gamma_h I_{h,crit}(a)}} \\
\frac{dD(a)}{dt} & = \overset{mortality}{\overbrace{\mu \gamma_h I_{h,crit}(a)}}
\end{align}

where $I_{sym,Tot}$ are the total number (across all age classes) of sympomatic infectious individuals, $I_{asym,Tot}$ are the total number of asymptomatic infectious individuals, $N_{Tot}$ is the total number of alive individuals (not in the $D$ state), and $R_{Shields}$ are the number of recovered individuals who could serve as serological shields - which we define as those of ages between 20 and 59.

```julia
#code for the system of differential equations
function SerologicalShields!(du,u,par,t)
    β_a,β_s,p,γ_e,γ_a,γ_s,γ_h,h,ξ,μ,α,S_ind,E_ind,Ia_ind,Is_ind,Ihsub_ind,Ihcrit_ind,R_ind,D_ind = par

    TOTI_a = sum(u[Ia_ind]) #Total asymptomatic infectious individuals
    TOTI_s = sum(u[Is_ind]) #Total symptomatic infectious individuals

    ShieldRTOT = sum(u[R_ind[3:6]]) #Total serological shields - those who are recovered and 20-60 yo.
    NTOT = sum(u[1:40])+sum(u[61:70]) #All individuals not in the hospitalised (Ihsub,Ihcrit) or dead (D) states
    SHIELD = ShieldRTOT

    for aa = 1:length(S_ind)#for each age class
        INDS = [S_ind[aa],E_ind[aa],Ia_ind[aa],Is_ind[aa],Ihsub_ind[aa],Ihcrit_ind[aa],R_ind[aa],D_ind[aa]]
        S,E,Ia,Is,Ihsub,Ihcrit,R,D = u[INDS]
        du[INDS[1]] = -β_a*S*TOTI_a/(NTOT+α*SHIELD) - β_s*S*TOTI_s/(NTOT+α*SHIELD) #dS/dt
        du[INDS[2]] = +β_a*S*TOTI_a/(NTOT+α*SHIELD) + β_s*S*TOTI_s/(NTOT+α*SHIELD) -  γ_e*E   #dE/dt
        du[INDS[3]] = p[aa]*γ_e*E - γ_a*Ia  #dI_a /dt
        du[INDS[4]] = (1-p[aa])*γ_e*E - γ_s*Is  #dI_s/dt
        du[INDS[5]] = h[aa]*(1-ξ[aa])*γ_s*Is - γ_h*Ihsub #dIhsub/dt
        du[INDS[6]] = h[aa]*ξ[aa]*γ_s*Is -  γ_h*Ihcrit #dIhcrit/dt
        du[INDS[7]] = γ_a*Ia + (1-h[aa])*γ_s*Is + γ_h*Ihsub + (1-μ[aa])*γ_h*Ihcrit  #dR/dt
        du[INDS[8]] = μ[aa]*γ_h*Ihcrit #dD/dt
    end

end
```

    SerologicalShields! (generic function with 1 method)

The serological states model has 11 parameters: the tramission rate from symptomatic infectious individuals $\beta_S$, the tramission rate from asymptomatic infectious individuals $\beta_A$, the mean exposed period $\frac{1}{\gamma_E}$, the mean asymptomatic infected period $\frac{1}{\gamma_A}$, the mean symptomatic infected period $\frac{1}{\gamma_S}$, $p$ the proportion of cases that result in asymptomatic infection, $h$ the proportion of cases with symptoms needing a hospital visit, $\xi$ the proportion of hospitilised cases that require critical ICU care, the mean period of hospitalisation $\frac{1}{\gamma_h}$, $\mu$ the proportion of critical cases that die from the disease, and $\alpha$ the level of serological shielding.  $h$ and $\xi$ are age-structured parameters.

We model dynamics looking at a high and low level of transmission, and with three levels of serological shielding - either none, 2:1 or 20:1.

```julia
#Define model parameters

β_A = 0.4 #asymptomatic transmission (days-1)
β_S = 0.8 #symptomatic transmission (days-1)
γ_e = (1.0/4.0) #inverse mean exposed period (days-1)
#low R0 scenario
γ_al = (1.0/4.0) #inverse mean asymptomatic period (days-1)
γ_sl = (1.0/4.0) #inverse mean symptomatic period (days-1)
#High R0 scenario
γ_ah = (1.0/6.0) #inverse mean asymptomatic period (days-1)
γ_sh = (1.0/6.0) #inverse mean symptomatic period (days-1)

γ_h = (1.0/10.0) #inverse mean hospitalised period (days-1)

p = [0.95, 0.95,0.9,0.8,0.7,0.6,0.4,0.2,0.2,0.2]  #fraction of cases that are asymptomatic
h = [0.1, 0.3, 1.2, 3.2, 4.9, 10.2, 16.6, 24.3, 27.3, 27.3]./100; #fraction of cases that require a hospital visit (by age class)
ξ = [5, 5, 5, 5, 6.3, 12.2, 27.4, 43.2, 70.9, 70.9]./100; #fraction of hospitalised cases that become critical and require critical care (by age class)
μ = 0.5.*ones(NumAgeClasses) #mortality fraction

#levels of serological shielding
α0 = 0.0
α1 = 2.0
α2 = 20.0

#parameter sets
params0 = [β_A,β_S,p,γ_e,γ_al,γ_sl,γ_h,h,ξ,μ,α0,S_ind,E_ind,Ia_ind,Is_ind,Ihsub_ind,Ihcrit_ind,R_ind,D_ind]
params2 = [β_A,β_S,p,γ_e,γ_al,γ_sl,γ_h,h,ξ,μ,α1,S_ind,E_ind,Ia_ind,Is_ind,Ihsub_ind,Ihcrit_ind,R_ind,D_ind]
params20 = [β_A,β_S,p,γ_e,γ_al,γ_sl,γ_h,h,ξ,μ,α2,S_ind,E_ind,Ia_ind,Is_ind,Ihsub_ind,Ihcrit_ind,R_ind,D_ind]

params0h = [β_A,β_S,p,γ_e,γ_ah,γ_sh,γ_h,h,ξ,μ,α0,S_ind,E_ind,Ia_ind,Is_ind,Ihsub_ind,Ihcrit_ind,R_ind,D_ind]
params2h = [β_A,β_S,p,γ_e,γ_ah,γ_sh,γ_h,h,ξ,μ,α1,S_ind,E_ind,Ia_ind,Is_ind,Ihsub_ind,Ihcrit_ind,R_ind,D_ind]
params20h = [β_A,β_S,p,γ_e,γ_ah,γ_sh,γ_h,h,ξ,μ,α2,S_ind,E_ind,Ia_ind,Is_ind,Ihsub_ind,Ihcrit_ind,R_ind,D_ind]
```

    19-element Array{Any,1}:
      0.4
      0.8
       [0.95, 0.95, 0.9, 0.8, 0.7, 0.6, 0.4, 0.2, 0.2, 0.2]
      0.25
      0.16666666666666666
      0.16666666666666666
      0.1
       [0.001, 0.003, 0.012, 0.032, 0.049, 0.102, 0.166, 0.243, 0.273, 0.273]
       [0.05, 0.05, 0.05, 0.05, 0.063, 0.122, 0.27399999999999997, 0.43200000000000005, 0.7090000000000001, 0.7090000000000001]
       [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]
     20.0
       1:10
       11:20
       21:30
       31:40
       41:50
       51:60
       61:70
       71:80

We now define the initial conditions for the model. We assume an initial individual aged 20-29 is exposed. We then run the model forward until 10,000 people have been exposed to the virus (10,000 people are no longer in the susceptible classes). We use this as the initial condition for when to begin intervention policies or not.

```julia
POP = ExamplePOP #assign population statistics
#seed population with 500 symptomatic and 9500 asymptomatic cases
uss0 = zeros(NumAgeClasses*NumEpiStates) #vector of initial states
uss0[S_ind] = POP.AgeStructure.*POP.PopulationSize #put everything in S
uss0[3] = uss0[3]-1 #remove infected from S ages 20-29
uss0[13] = 1  #assign E cases ages 20-29
uss0 = uss0./POP.PopulationSize;  #renormalise
```

First lets simulate the low R0 outbreak. Now we define the 3 scenarios  (no, some and more serological shielding) as differential equation problems and solve them using numerical integration.

```julia
tspan = (0.0,400.0) #run the simulations between 0 days and 400 days

#initial spin-up
prob_init = ODEProblem(SerologicalShields!,uss0,tspan,params0)

#set up callback to stop integration when number of cases reaches a threshold
CaseThreshold = 10_000/POP.PopulationSize

condition(u,t,integrator)  = sum(u[1:NumAgeClasses]) - (1 - CaseThreshold)  #this must be equal to zero at stopping condition
affect!(integrator) = terminate!(integrator)
cb = ContinuousCallback(condition,affect!)
#solve the initial spin up, checking for positivity and using callback definition
sol_init = solve(prob_init,Tsit5(), abstol=1e-8,reltol=1e-8,saveat=1,isoutofdomain=(y,p,t)->any(x->x<0,y), callback = cb)

#assign end spinup states as new initial condition.
uInit = sol_init.u[end];

#run scenarios
prob_alph0 = ODEProblem(SerologicalShields!,uInit,tspan,params0)
prob_alph2 = ODEProblem(SerologicalShields!,uInit,tspan,params2)
prob_alph20 = ODEProblem(SerologicalShields!,uInit,tspan,params20)

sol_alph0 = solve(prob_alph0,Tsit5(), abstol=1e-8,reltol=1e-8,saveat=1,isoutofdomain=(y,p,t)->any(x->x<0,y))
sol_alph2 = solve(prob_alph2,Tsit5(), abstol=1e-8,reltol=1e-8,saveat=1,isoutofdomain=(y,p,t)->any(x->x<0,y))
sol_alph20 = solve(prob_alph20,Tsit5(), abstol=1e-8,reltol=1e-8,saveat=1,isoutofdomain=(y,p,t)->any(x->x<0,y));
```

and for the high R0 transmission outbreak scenarios

```julia
tspan = (0.0,400.0) #run the simulations between 0 days and 400 days

#initial spin-up
prob_init = ODEProblem(SerologicalShields!,uss0,tspan,params0h)

#set up callback to stop integration when number of cases reaches a threshold
CaseThreshold = 10_000/POP.PopulationSize

condition(u,t,integrator)  = sum(u[1:NumAgeClasses]) - (1 - CaseThreshold)  #this must be equal to zero at stopping condition
affect!(integrator) = terminate!(integrator)
cb = ContinuousCallback(condition,affect!)
#solve the initial spin up, checking for positivity and using callback definition
sol_init = solve(prob_init,Tsit5(), abstol=1e-8,reltol=1e-8,saveat=1,isoutofdomain=(y,p,t)->any(x->x<0,y), callback = cb)

#assign end spinup states as new initial condition.
uInit = sol_init.u[end];

#run scenarios
prob_alph0h = ODEProblem(SerologicalShields!,uInit,tspan,params0h)
prob_alph2h = ODEProblem(SerologicalShields!,uInit,tspan,params2h)
prob_alph20h = ODEProblem(SerologicalShields!,uInit,tspan,params20h)

sol_alph0h = solve(prob_alph0h,Tsit5(), abstol=1e-8,reltol=1e-8,saveat=1,isoutofdomain=(y,p,t)->any(x->x<0,y))
sol_alph2h = solve(prob_alph2h,Tsit5(), abstol=1e-8,reltol=1e-8,saveat=1,isoutofdomain=(y,p,t)->any(x->x<0,y))
sol_alph20h = solve(prob_alph20h,Tsit5(), abstol=1e-8,reltol=1e-8,saveat=1,isoutofdomain=(y,p,t)->any(x->x<0,y));
```

Now we define some functions to help plot the output.

```julia

#Define some additional functions to help with analysis

#Find Population-wide states from the solution to ODE
function PopWideStates_ss!(sol,par)
    β_a,β_s,p,γ_e,γ_a,γ_s,γ_h,h,ξ,μ,α,S_ind,E_ind,Ia_ind,Is_ind,Ihsub_ind,Ihcrit_ind,R_ind,D_ind = par
Sols =  hcat(sol.u...)
SS = zeros(length(sol.t))
EE = zeros(length(sol.t))
IA = zeros(length(sol.t))
IS = zeros(length(sol.t))
IHs = zeros(length(sol.t))
IHc = zeros(length(sol.t))
RR = zeros(length(sol.t))
DD =zeros(length(sol.t))
D_per_day = zeros(length(sol.t))
    NumAgeClasses=length(S_ind)
for aa=1:NumAgeClasses
    SS= SS+ Sols[S_ind[aa],:]
    EE= EE+ Sols[E_ind[aa],:]
    IA= IA+ Sols[Ia_ind[aa],:]
    IS= IS+ Sols[Is_ind[aa],:]
    IHs = IHs+ Sols[Ihsub_ind[aa],:]
    IHc = IHc+ Sols[Ihcrit_ind[aa],:]
    RR= RR+ Sols[R_ind[aa],:]
    DD= DD+ Sols[D_ind[aa],:]
    #D_per_day = D_per_day+ μ[aa].*γ_h.*Sols[Ihcrit_ind[aa],:].*1.0 #this is a rate of dead/day multiplied by time period of 1 day
    D_per_day = D_per_day + [(Sols[D_ind[aa],2:end] .- Sols[D_ind[aa],1:(end-1)]);  0]
end
    D_by_age = Sols[D_ind,end] #at end timepoint

    return SS,EE,IA,IS,IHs,IHc,RR,DD,D_per_day,D_by_age
end

#plotting function - plot deaths per day, ICU beds occupied and cumulative deaths by age group
function PlotResults!(sol,par,POP,MeanAge,title_plot,txtsize,SCALING)
    SS,EE,IA,IS,IHs,IHc,RR,DD,D_per_day,D_by_age = PopWideStates_ss!(sol,par);

    p1 = plot(sol.t,SCALING.*D_per_day,ylabel="Deaths per day",xlabel="Time (days)",legend = false,yguidefontsize=txtsize,title=title_plot)
    p2 = plot(sol.t,SCALING.*(IHc),ylabel="ICU bed req.",xlabel="Time (days)",legend=false,yguidefontsize=txtsize)
    #plot!(sol.t,SCALING.*(IHs+IHc),legend=false) #all hospitalised
    p3 = plot(MeanAge,SCALING.*D_by_age,xlabel="Age",ylabel="Cumulative deaths",legend=false,yguidefontsize=txtsize)
    #plt = twinx() #this helps do two axes on one plot, hard to make this look good right now.
    # plot!(plt,x,y,...) #same as previous line
    #p4 = plot(MeanAge,POP.frac_pop_by_age,xlabel="Age",ylabel = "Pop.Demography",legend = false,yguidefontsize=txtsize)
    return p1,p2,p3,DD
end

#plotting function - plot deaths per day, ICU beds occupied and cumulative deaths by age group
function PlotResults2!(sol1,sol2,sol3,par1,par2,par3,MiddleAge,title_plot,txtsize,SCALING)
    SS1,EE1,IA1,IS1,IHs1,IHc1,RR1,DD1,D_per_day1,D_by_age1 = PopWideStates_ss!(sol1,par1);
    SS2,EE2,IA2,IS2,IHs2,IHc2,RR2,DD2,D_per_day2,D_by_age2 = PopWideStates_ss!(sol2,par2);
    SS3,EE3,IA3,IS3,IHs3,IHc3,RR3,DD3,D_per_day3,D_by_age3 = PopWideStates_ss!(sol3,par3);

    MKS=2

    p1 = plot(sol1.t,SCALING.*D_per_day1,ylabel="Deaths per day \n per 100,000",xlabel="Time (days)",label = L"\alpha=0",
        foreground_color_legend = nothing,background_color_legend=nothing, yguidefontsize=txtsize,title=title_plot,legend=:topright)
    plot!(sol2.t,SCALING.*D_per_day2,label=L"\alpha=2")
    plot!(sol3.t,SCALING.*D_per_day3,label=L"\alpha=20")
    p2 = plot(sol1.t,SCALING.*(IHc1),ylabel="ICU bed demand \n per 100,000",xlabel="Time (days)",legend=false,yguidefontsize=txtsize)
    plot!(sol2.t,SCALING.*IHc2,legend=false)
    plot!(sol3.t,SCALING.*IHc3,legend=false)
    #plot!(sol.t,SCALING.*(IHs+IHc),legend=false) #all hospitalised
    p3 = plot(MiddleAge,SCALING.*D_by_age1,markershape = :auto,markersize=MKS,xlabel="Age",ylabel="Cumulative deaths \n per 100,000",legend=false,yguidefontsize=txtsize)
    plot!(MiddleAge,SCALING.*D_by_age2,legend=false,markershape = :auto,markersize=MKS)
    plot!(MiddleAge,SCALING.*D_by_age3,legend=false,markershape = :auto,markersize=MKS)

    #plt = twinx() #this helps do two axes on one plot, hard to make this look good right now.
    # plot!(plt,x,y,...) #same as previous line
    #p4 = plot(MeanAge,POP.frac_pop_by_age,xlabel="Age",ylabel = "Pop.Demography",legend = false,yguidefontsize=txtsize)
    return p1,p2,p3,DD1,DD2,DD3
end

```

    PlotResults2! (generic function with 1 method)

```julia
function PlotHosp!(sol)
Sols =  hcat(sol.u...)
S_Ihs = zeros(NumAgeClasses,length(sol.t))
S_Ihc = zeros(NumAgeClasses,length(sol.t))
sumIhc = zeros(length(sol.t))
    SS=sumIhc
for aa=1:NumAgeClasses
    S_Ihs[aa,:] = Sols[Ihsub_ind[aa],:]
    S_Ihc[aa,:] = Sols[Ihcrit_ind[aa],:]
    sumIhc = sumIhc + Sols[Ihcrit_ind[aa],:]
        SS=SS+Sols[S_ind[aa],:]
end

    JJ = findmax(sumIhc)[2]

titlestr = string(sol.t[JJ], " days")
p1 = plot(MiddleAge,100_000*S_Ihs[:,JJ],ylabel="Hospitalised \n per 100,000",xlabel="Age",legend=:topleft,
        markershape = :auto,markersize=2,label="subacute by age",xlims=(0.0,100.0),title=titlestr,yguidefontsize=8,
        foreground_color_legend = nothing,background_color_legend=nothing,color=:pink)
    plot!(MiddleAge,100_000*S_Ihc[:,JJ],markershape = :auto,markersize=2,label="ICU by age",color=:purple)
    plot!(MiddleAge,ones(NumAgeClasses)*sum(100_000*S_Ihc[:,JJ]),label="Total ICU number",color=:black)

return p1,SS
end
```

    PlotHosp! (generic function with 1 method)

Now we can plot the results.

## Low R0 dynamics

```julia
#Make plots
txtsize=10
SCALING = 100_000
p1,p2,p3,DD1,DD2,DD3 = PlotResults2!(sol_alph0,sol_alph2,sol_alph20,params0,params2,params20,MiddleAge,"COVID-19 Epidemic",txtsize,SCALING);
ll = @layout[a;b;c]
plot(p1,p2,p3,layout=ll,size=(300,600))
savefig("core_model_epidemic_lowR0.pdf")
current() #display the figure
```

Numbers shown are per 100,000 people. $\alpha$ is the level of serological shielding.

```julia
LL = @layout[a; b; c]
p1,s1 = PlotHosp!(sol_alph0)
p2,s2 = PlotHosp!(sol_alph2)
p3,s3 = PlotHosp!(sol_alph20)
plot(p1,p2,p3,layout=LL,size = (300,600))
savefig("hospital_peak_demand_lowR0.pdf")
current()
```

Age structured subacute and critical hospitalised cases at epdiemic peaks for the 3 scenarios. Time shown is that of the epidemic peak. Left shows high transmission epidemiological scenarios, from top to bottom $\alpha=0$, $\alpha=2$, $\alpha=20$. Right shows low transmission epidemiological scenarios, from top to bottom $\alpha=0$, $\alpha=2$, $\alpha=20$. Horizontal line indicates total ICU beds required (across all age classes) at this time.

## High R0 dynamics

```julia
#Make plots
txtsize=10
SCALING = 100_000
p1,p2,p3,DD1,DD2,DD3 = PlotResults2!(sol_alph0h,sol_alph2h,sol_alph20h,params0h,params2h,params20h,MiddleAge,"COVID-19 Epidemic",txtsize,SCALING);
ll = @layout[a;b;c]
plot(p1,p2,p3,layout=ll,size=(300,600))
savefig("core_model_epidemic_highR0.pdf")
current() #display the figure
```

```julia
LL = @layout[a; b; c]
p1,s1 = PlotHosp!(sol_alph0h)
p2,s2 = PlotHosp!(sol_alph2h)
p3,s3 = PlotHosp!(sol_alph20h)
plot(p1,p2,p3,layout=LL,size = (300,600))
savefig("hospital_peak_demand_highR0.pdf")
current()
```

```julia

```

core_model/core_julia/Serological Shields - Core Model.ipynb


# Modelos con variables latentes y repaso del algoritmo K-Means

> **¿Qué significa variable latente?**

> Para responder a esta pregunta, nos remontamos hacia la raiz *latina* etimológica de la palabra latente. Esta palabra viene la palabra en Latín **latens** que significa escondido u oculto.

> En el contexto del modelado probabilístico nos referimos con variables latentes a variables que nunca observamos, pero que (inferimos) están ahí.

## 1. Variables latentes

### ¿Porqué consideramos variables latentes?

Hay diversas razones por las que permitirnos incluir variables latentes en nuestros modelos cobra muchísima importancia. Algunas de ellas:

1. **No porque no observemos una variable significa que no exista.**

2. **Muchas veces nos permiten conseguir modelos más simples.**

**Ejemplo:**

Una empresa acaba de abrir una posición en el equipo de ciencia de datos. En este sentido, el departamento de RH está interesado en entrevistar a varios candidatos para encontrar a alguien idóneo para la posición.

Para ello, ya se tiene un tabulador que involucra varias variables:
- Grado académico.
- Promedio de calificaciones del último grado académico.
- Entrevista telefónica.
- Entrevista en vivo.

Sin embargo, la entrevista en vivo es un evento que puede llegar a involucrar muchos recursos económicos, y por experiencia, hay varios candidatos que se pueden descartar con solo el conocimiento de las otras variables. La idea es desarrollar un modelo usando datos históricos del departamento de RH:

| Candidato | Grado | Promedio | E. Telefónica | E. Vivo |
| --------- | ----- | -------- | ------------- | ------- |
| 1         | Lic   | 8.4      | 7             | 5       |
| 2         | Maes  | 8.0      | 7             | 6       |
| 3         | Lic   | 9.5      | 8             | 9       |
| 4         | Doc   | 8.9      | 9             | 10      |

Si intentamos establecer un modelo que relacione estas variables, después de revisarlo un poco, llegaríamos a que todas estas variables están relacionadas entre sí, obteniendo un modelo completamente conectado,

con lo que no tendríamos ni una sola pista de la estructura del modelo, y tendríamos que definir la probabilidad conjunta sobre todas las variables (número exponencial de parámetros).

*Alternativa 1*: considerar un modelo estructurado del tipo

$$
p(x_1, x_2, x_3, x_4) = \frac{\exp\{-w^T x\}}{Z},
$$

con lo que solo tendríamos 5 parámetros $w_0, w_1, w_2, w_3, w_4$. Sin embargo, $Z$ es una constante de normalización que involucra una suma sobre todos los posibles valores de las cuatro variables aleatorias.

*Alternativa 2*: considerar una variable latente de Inteligencia

con lo que el modelo sería:

$$
p(x_1, x_2, x_3, x_4) = \sum_{I} p(x_1, x_2, x_3, x_4 | I) p(I) = \sum_{I} p(x_1 | I)p(x_2 | I)p(x_3 | I)p(x_4 | I) p(I)
$$

Con esto reducimos notablemente la complejidad del modelo.

3. **Aplicaciones prácticas -> Clustering -> Segmentación de clientes, Motores de búsqueda, Sistemas de recomendación, ...**

En aplicaciones clustering pretendemos descubrir segmentaciones en los datos. Esta segmentación la podemos entender como una variable latente.

## 2. Clustering

Probablemente ya hayan escuchado hablar de clustering. Como tal, es de las aplicaciones más importantes en aprendizaje **no supervisado**, y seguramente en un proyecto de análisis de datos no va a pasar más de un mes para cuando necesiten utilizar este tipo de técnicas. Así que vamos a estudiar un par de ellas.

**Ejemplo:**

Los departamentos de créditos en general (personas, pymes, empresarial) normalmente estudian la relación entre ingresos y deuda del candidato a crédito para decidir si acreditan o no a la persona.

Hay varias heurísticas que se utilizan. Por ejemplo:

- Si las deudas rebasan el 40% del apalancamiento (capital social + ingresos + pasivos), es una empresa de alto riesgo.
- Si las deudas rebasan 3 meses de ingresos, es una empresa de alto riesgo.

Se puede estudiar esta relación, y segmentar a los clientes de acuerdo a su perfil en estas variables.

```python
# Importamos función para generar datos
from bank_customer_data import generate_bank_customer_data
# Importamos pyplot
from matplotlib import pyplot as plt
```

```python
# Generamos datos
data = generate_bank_customer_data()
```

```python
data.head(10)
```

      income
      debt
      labels

      0
      2.041717
      2.560312
      0.0

      1
      2.357892
      5.772011
      1.0

      2
      2.932307
      3.789670
      0.0

      3
      1.287167
      3.991068
      1.0

      4
      3.629376
      5.221854
      1.0

      5
      3.310683
      3.268733
      0.0

      6
      2.280852
      1.657472
      0.0

      7
      6.721162
      5.810731
      2.0

      8
      4.525707
      3.605872
      2.0

      9
      3.369601
      0.964994
      0.0

```python
# Datos
plt.scatter(data["income"], data["debt"], c="gray", alpha=0.6)
plt.xlabel("Ingresos mensuales (x100k MXN)")
plt.ylabel("Deuda (x100k MXN)")
```

Lo que queremos en clustering es identificar los grupos a los que pertenecen cada uno de los clientes.

```python
# Grupos "reales"
plt.scatter(data["income"], data["debt"], c=data["labels"], cmap="Accent", alpha=0.6)
plt.xlabel("Ingresos mensuales (x100k MXN)")
plt.ylabel("Deuda (x100k MXN)")
```

```python
data[["income", "debt"]].head(10)
```

      income
      debt

      0
      2.041717
      2.560312

      1
      2.357892
      5.772011

      2
      2.932307
      3.789670

      3
      1.287167
      3.991068

      4
      3.629376
      5.221854

      5
      3.310683
      3.268733

      6
      2.280852
      1.657472

      7
      6.721162
      5.810731

      8
      4.525707
      3.605872

      9
      3.369601
      0.964994

Esta idea se conoce como **Hard clustering**; bajo este esquema, identificamos para cada punto un único grupo al que pertenece, es decir:

$$
\text{cluster_id}_x = f(x)
$$

- Los puntos verdes son 100% verdes.
- Los puntos azules son 100% azules.
- Los puntos grises son 100% grises.

Sin embargo, fijemos un momento nuestra atención en los recuadros a continuación ($[2, 3] \times [4, 5]$ y $[4, 5] \times [3, 4]$):

```python
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.scatter(data["income"], data["debt"], c=data["labels"], cmap="Accent", alpha=0.6)
plt.axis([1, 3, 4, 5])
plt.xlabel("Ingresos mensuales (x100k MXN)")
plt.ylabel("Deuda (x100k MXN)")

plt.subplot(1, 2, 2)
plt.scatter(data["income"], data["debt"], c=data["labels"], cmap="Accent", alpha=0.6)
plt.axis([3.5, 5, 2, 4])
plt.xlabel("Ingresos mensuales (x100k MXN)")
plt.ylabel("Deuda (x100k MXN)")
```

Desde un punto de vista intuitivo, los puntos en estos sectores no es muy claro a qué grupo pertenecen. Estaríamos tentados a decir que pertenecen a un grupo con cierta probabilidad y con cierta probabilidad a otro.

Esta idea se conoce como **Soft clustering**, y está estrechamente relacionado a **clustering probabilístico**.

$$
p(\text{cluster_id}_x |x)
$$

Al darle un enfoque probabilístico, tenemos varias ventajas colaterales:

- Sintonización de hiperparámetros.
- Modelo generativo.

## 3. Un algoritmo de hard clustering: K-Means

Aunque el K-Means es uno de los algoritmos de hard clustering más conocidos y usados, lo veremos en un par de sesiones más desde una perspectiva probabilística.

De manera que nos conviene estudiarlo antes.

**Problema:** dado un conjunto de observaciones $x_1, x_2, \dots, x_N \in \mathbb{R}^d$, se debe particionar las $N$ observaciones en $k$ ($\leq N$) clusters $\{1, 2, \dots, k\}$, de manera que se minimice la suma de distancias al cuadrado (varianza).

**Algoritmo:**

1. Inicializar los parámetros $\theta = \{\mu_1, \dots, \mu_k\}$ de manera aleatoria.
2. Repetir hasta la convergencia (hasta que los parámetros no varíen):
   1. Para cada punto calcule el centroide más cercano:
      $$
      c_i = \arg \min_{c} ||x_i - \mu_c||.
      $$

   2. Actualizar centroides:
      $$
      \mu_c = \frac{\sum_{i: c_i = c} x_i}{\sum_{i: c_i = c} 1}
      $$

Nosotros usaremos sklearn durante la clase:

```python
# Importamos sklearn.cluster.KMeans
from sklearn.cluster import KMeans
```

```python
# Algoritmo de sklearn
KMeans?
```

```python
# Instanciamos el algoritmo
kmeans = KMeans(n_clusters=3)
```

```python
# Entrenamos
kmeans.fit(X=data[["income", "debt"]])
```

    KMeans(algorithm='auto', copy_x=True, init='k-means++', max_iter=300,
           n_clusters=3, n_init=10, n_jobs=None, precompute_distances='auto',
           random_state=None, tol=0.0001, verbose=0)

```python
# Gráfico
plt.scatter(data["income"], data["debt"], c=kmeans.labels_, cmap="Accent", alpha=0.6)
plt.plot(kmeans.cluster_centers_[0, 0], kmeans.cluster_centers_[0, 1], "*g", ms=20)
plt.plot(kmeans.cluster_centers_[1, 0], kmeans.cluster_centers_[1, 1], "*b", ms=20)
plt.plot(kmeans.cluster_centers_[2, 0], kmeans.cluster_centers_[2, 1], "*k", ms=20)
plt.xlabel("Ingresos mensuales (x100k MXN)")
plt.ylabel("Deuda (x100k MXN)")
```

Todo se ve bien hasta acá. **¿Qué pasa si aumentamos el número de clusters?**

Una métrica que podemos usar para ver qué tan bueno está siendo el agrupado es la suma de las distancias al cuadrado de cada punto a su centroide respectivo:

$$
\frac{1}{N}\sum_{i=1}^N ||x_i - \mu_{c_i}||^2.
$$

```python
from sklearn.model_selection import train_test_split
import numpy as np
```

```python
def msd(X, cluster_id, centroids):
    """
    Mean squared distance.
    :param data: Data.
    :param centroids: Centroids.
    :return: Mean squared distance.
    """
    # Number of clusters
    k = centroids.shape[0]
    # Number of points
    N = X.shape[0]

    # Distances initialization
    distances = np.zeros(N)

    # Compute distances to corresponding cluster
    for j in range(k):
        distances[cluster_id == j] = np.linalg.norm(X[cluster_id == j] - centroids[j, :], axis=1)

    return (distances**2).mean()
```

```python
X_train, X_test = train_test_split(data[["income", "debt"]], test_size=0.2)

msd_train = []
msd_test = []
for k in range(2, 20):
    # Instanciamos el algoritmo
    kmeans = KMeans(n_clusters=k)

    # Entrenamos
    kmeans.fit(X=X_train)

    # Métrica con datos de entrenamiento
    msd_train.append(msd(X_train, kmeans.labels_, kmeans.cluster_centers_))

    # Métrica con datos de prueba
    msd_test.append(msd(X_test, kmeans.predict(X_test), kmeans.cluster_centers_))
```

```python
plt.plot(range(2, 20), msd_train, label="train")
plt.plot(range(2, 20), msd_test, label="test")
plt.legend()
plt.xlabel("Número de clusters")
plt.ylabel("Suma de distancias cuadradas")
```

```python
dist_orig = np.array(msd_test)**2 + np.arange(2, 20)**2
dist_orig
```

    array([ 18.965874  ,  12.76259407,  18.58556801,  26.86658899,
            37.54766072,  50.21327897,  64.92856705,  81.72498327,
           100.59615233, 121.45944426, 144.40851097, 169.30948257,
           196.25913515, 225.29083048, 256.21522949, 289.20114374,
           324.1752161 , 361.19051482])

```python
np.arange(2, 20)[dist_orig.argmin()]
```

    3

```python
# Gráfico
plt.scatter(X_train["income"], X_train["debt"], c=kmeans.labels_, cmap="inferno", alpha=0.6)
plt.xlabel("Ingresos mensuales (x100k MXN)")
plt.ylabel("Deuda (x100k MXN)")
```

Como observamos, esta métrica siempre decrece con la cantidad de clusters, lo que hace bastante complejo elegir un número de clusters adecuado cuando este es desconocido.

# 4. Modelo de mezcla Gaussiana (GMM)

Como vimos, el K-Means (y en general los algoritmos de hard clustering) tienen varios detalles:

- No es claro cómo elegir el número de clusters.
- Hay puntos que podrían estar en una frontera entre dos o más clusters, y el hard clustering no nos permite tener incertidumbre en la pertenencia.

Para lidiar con estos problemas, podemos plantear un modelo probablístico de nuestros datos.

Hasta ahora, conocemos algunas distribuciones. Entre ellas la Gaussiana, para la cual ya sabemos como estimar sus parámetros.

¿Qué pasa si intentamos ajustar una **distribución Gaussiana** a los datos? Es decir, si modelamos los datos con

$$
p(x|\theta) = \mathcal{N}(x|\mu, \Sigma), \qquad \theta=\{\mu, \Sigma\}
$$

```python
# Datos
plt.scatter(data["income"], data["debt"], c="gray", alpha=0.6)
plt.xlabel("Ingresos mensuales (x100k MXN)")
plt.ylabel("Deuda (x100k MXN)")
```

```python
from scipy.stats import multivariate_normal
import numpy as np
```

```python
# Ajustamos parámetros
mu = data[["income", "debt"]].mean()
cov = data[["income", "debt"]].cov()
# Definimos VA
X = multivariate_normal(mean=mu, cov=cov)
```

```python
# Datos
plt.scatter(data["income"], data["debt"], c="gray", alpha=0.6)

# Gaussiana
x = np.linspace(0, 8, 100)
y = np.linspace(0, 9, 100)
x, y = np.meshgrid(x, y)
z = X.pdf(np.dstack([x, y]))
plt.contour(x, y, z)
plt.xlabel("Ingresos mensuales (x100k MXN)")
plt.ylabel("Deuda (x100k MXN)")
```

```python
mu
```

    income    2.982204
    debt      4.124967
    dtype: float64

Sin embargo, este modelo no parece corresponder con nuestros datos. La región de máxima probabilidad (media) cae en un punto medio entre los clusters, y allí no hay muchos datos.

### ¿Qué pasa si usamos varias Gaussianas?

```python
# Ajustamos parámetros
mu1 = data.loc[data["labels"] == 0, ["income", "debt"]].mean()
mu2 = data.loc[data["labels"] == 1, ["income", "debt"]].mean()
mu3 = data.loc[data["labels"] == 2, ["income", "debt"]].mean()
cov1 = data.loc[data["labels"] == 0, ["income", "debt"]].cov()
cov2 = data.loc[data["labels"] == 1, ["income", "debt"]].cov()
cov3 = data.loc[data["labels"] == 2, ["income", "debt"]].cov()
# Definimos VA
X1 = multivariate_normal(mean=mu1, cov=cov1)
X2 = multivariate_normal(mean=mu2, cov=cov2)
X3 = multivariate_normal(mean=mu3, cov=cov3)
```

```python
# Datos
plt.scatter(data["income"], data["debt"], c="gray", alpha=0.6)

# Gaussiana 1
x = np.linspace(0, 6, 100)
y = np.linspace(0, 5, 100)
x, y = np.meshgrid(x, y)
z = X1.pdf(np.dstack([x, y]))
plt.contour(x, y, z)

# Gaussiana 2
x = np.linspace(0, 4, 100)
y = np.linspace(4, 9, 100)
x, y = np.meshgrid(x, y)
z = X2.pdf(np.dstack([x, y]))
plt.contour(x, y, z)

# Gaussiana 3
x = np.linspace(3, 8, 100)
y = np.linspace(2, 8, 100)
x, y = np.meshgrid(x, y)
z = X3.pdf(np.dstack([x, y]))
plt.contour(x, y, z)

plt.xlabel("Ingresos mensuales (x100k MXN)")
plt.ylabel("Deuda (x100k MXN)")
```

**¡Mucho mejor!**

Cada Gaussiana explica un cluster de puntos, y el modelo general sería una suma ponderada de estas densidades Gaussianas:

$$
p(x | \theta) = \sum_{c=1}^{3} \pi_c \mathcal{N}(x | \mu_c, \Sigma_c), \qquad \theta = \{\pi_1, \pi_2, \pi_3, \mu_1, \mu_2, \mu_3, \Sigma_1, \Sigma_2, \Sigma_3\}
$$

**¿Y esto cómo lo interpretamos?**

Bueno, pues si logramos encontrar los parámetros $\pi_1, \pi_2, \pi_3, \mu_1, \mu_2, \mu_3, \Sigma_1, \Sigma_2, \Sigma_3$ para este conjunto de datos, habremos resuelto el problema de (soft) clustering, ya que encontraremos para cada punto la probabilidad de que venga de cada una de las Gaussianas.

**¿Qué ventajas tenemos?**

Como ventaja respecto a usar una sola Gaussiana, hemos añadido flexibilidad a nuestro modelo. Es decir, con esta estuctura podemos representar conjuntos de datos complejos.

> En efecto, podemos aproximar casi cualquier distribución continua con una mezcla de Gaussianas con precisión arbitraria, dado que incluyamos un número suficiente de Gaussianas en la mezcla.

**¿A qué costo?**

La cantidad de parámetros que debemos estimar se multiplica por la cantidad de Gaussianas en la mezcla.

### ¿Cómo encontramos (entrenamos) los parámetros?

Podemos maximizar la función de verosimilitud (suposición de independencia):

$$
\max_{\theta} p(X | \theta) = \prod_{i=1}^N p(x_i | \theta) = \prod_{i=1}^N \sum_{c=1}^{3} \pi_c \mathcal{N}(x_i | \mu_c, \Sigma_c)
$$

sujeto a:

\begin{align}
\sum_{c=1}^3 \pi_c & = 1 \\
\pi_c & \geq 0 \quad \text{for } c=1,2,3\\
\Sigma_c & \succ 0 \quad \text{for } c=1,2,3
\end{align}

Es decir, las matrices de covarianza deben ser definidas positivas (¿por qué?).

**Complejidades numéricas:**

Este problema de optimización puede ser resuelto numéricamente con un algoritmo como el gradiente descendiente. Sin embargo,

1. La restricción sobre la matriz de covarianzas hace el problema de optimización muy complejo de resolver numéricamente hablando.

   Una simplificación para poder trabajar con esta restricción es suponer que las matrices de covarianza son diagonales:

$$
\Sigma_c = \text{diag}(\sigma_{c1}, \sigma_{c2}, \dots, \sigma_{cn}),
$$

2. La suma dentro del producto también hace bastante complejo el cálculo de los gradientes. Comúnmente, para evitar el producto se toma logaritmo de la verosimilitud:

   $$
   \log p(X | \theta) = \log \left(\prod_{i=1}^N p(x_i | \theta) \right)= \sum_{i=1}^N \log\left(\sum_{c=1}^{3} \pi_c \mathcal{N}(x_i | \mu_c, \Sigma_c)\right)
   $$

   y con esto, podemos observar que permanece una suma ponderada dentro del logaritmo.

### ¿Y entonces?

Afortunadamente, existe un algoritmo alternativo con base probabilística llamado **algoritmo de maximización de la esperanza**, el cual estaremos estudiando en las próximas clases no solo para el problema de mezclas Gaussianas, sino para entrenar cualquier modelo con **variables latentes**.

**¿Variables latentes?**

Recordamos que propusimos el siguiente modelo:

$$
p(x | \theta) = \sum_{c=1}^{3} \pi_c \mathcal{N}(x | \mu_c, \Sigma_c), \qquad \theta = \{\pi_1, \pi_2, \pi_3, \mu_1, \mu_2, \mu_3, \Sigma_1, \Sigma_2, \Sigma_3\}
$$

Este modelo en realidad, lo podemos pensar como un modelo con una variable latente $t$ que determina a cuál Gaussiana pertenece cada punto:

Entonces, razonablemente podemos atribuirle a $t$ tres posibles valores (1, 2, y 3), que nos dicen de qué Gaussiana viene el punto. Recordamos que $t$ es una variable latente, nunca la observamos.

Sin embargo, razonando probabilísticamente, después de entrenar nuestra mezcla Gaussiana, podríamos preguntarle al modelo, ¿Cuál es el valor más probable de $t$ dado el punto $x$? --> **Clustering**

Con este modelo, podemos asignar las siguientes probabilidades:

- Previa:

  $$
  p(t=c | \theta) = \pi_c
  $$

- Verosimilitud:

  $$
  p(x | t=c, \theta) = \mathcal{N}(x | \mu_c, \Sigma_c)
  $$

Razonable, ¿no?

Y con lo anterior,

$$
p(x | \theta) = \sum_{c=1}^3 p(x, t=c | \theta) = \sum_{c=1}^3 \underbrace{p(x | t=c, \theta)}_{\mathcal{N}(x | \mu_c, \Sigma_c)} \underbrace{p(t=c | \theta)}_{\pi_c},
$$

justo como el modelo intuitivo que habíamos propuesto.

### Algoritmo de maximización de la esperanza para mezclas Gaussianas - Intuición

Supongamos que tenemos los siguientes puntos de tamaños de playeras, y queremos definir cuáles son talla chica y cuales son talla grande:

```python
from shirts_size_data import generate_shirts_data
from scipy.stats.distributions import norm
```

```python
shirts_data = generate_shirts_data()
shirts_data.head()
```

      size
      labels

      0
      28.648354
      0.0

      1
      39.421816
      1.0

      2
      42.228652
      1.0

      3
      28.078481
      0.0

      4
      29.224360
      0.0

```python
plt.scatter(shirts_data["size"], np.zeros(len(shirts_data)), c="gray", alpha=0.6)
plt.ylim([-0.01, 0.3])
plt.xlabel("Tamaño (cm)")
```

¿Cómo estimamos los parámetros de nuestro modelo de variable latente?

Analicemos varios casos:

1. Si de entrada supiéramos cuáles playeras son chicas y cuáles grandes:

```python
plt.scatter(shirts_data.loc[shirts_data["labels"] == 0, "size"],
            np.zeros((shirts_data["labels"] == 0).sum()),
            alpha=0.6)
plt.scatter(shirts_data.loc[shirts_data["labels"] == 1, "size"],
            np.zeros((shirts_data["labels"] == 1).sum()),
            alpha=0.6)
x = shirts_data["size"].copy().values
x.sort()
mu1 = shirts_data.loc[shirts_data["labels"] == 0, "size"].mean()
mu2 = shirts_data.loc[shirts_data["labels"] == 1, "size"].mean()
s1 = shirts_data.loc[shirts_data["labels"] == 0, "size"].std()
s2 = shirts_data.loc[shirts_data["labels"] == 1, "size"].std()
plt.plot(x, norm.pdf(x, loc=mu1, scale=s1))
plt.plot(x, norm.pdf(x, loc=mu2, scale=s2))
plt.ylim([-0.01, 0.3])
plt.xlabel("Tamaño (cm)")
```

$$
p(x | t=1, \theta) = \mathcal{N}(x | \mu_1, \sigma_1)
$$

Con lo cual:

$$
\mu_1 = \frac{\sum_{i: t_i = 1} x_i}{\sum_{i: t_i = 1} 1}, \qquad \sigma_1^2 = \frac{\sum_{i: t_i = 1} (x_i - \mu_1)^2}{\sum_{i: t_i = 1} 1},
$$

y

$$
\mu_2 = \frac{\sum_{i: t_i = 2} x_i}{\sum_{i: t_i = 2} 1}, \qquad \sigma_2^2 = \frac{\sum_{i: t_i = 2} (x_i - \mu_1)^2}{\sum_{i: t_2 = 1} 1}
$$

2. Como sablemos, en el algoritmo de mezclas Gaussianas nunca sabremos si un punto pertenece a cierto cluster o no, sino que conoceremos las probabilidades de que pertenezca a cada cluster.

   De modo que si conocemos la posterior $p(t | x, \theta)$, entonces ponderamos lo anterior por esta probabilidad:

   $$
\mu_1 = \frac{\sum_{i} p(t_i=1 | x_i, \theta)x_i}{\sum_{i} p(t_i=1 | x_i, \theta)}, \qquad \sigma_1^2 = \frac{\sum_{i} p(t_i=1 | x_i, \theta) (x_i - \mu_1)^2}{\sum_{i} p(t_i=1 | x_i, \theta)}.
   $$

3. ¿Y cómo conocemos la posterior $p(t | x, \theta)$?

   Bueno, pues si conocemos los parámetros, es bastante fácil:

   $$
   p(t=c | x, \theta) = \frac{p(x | t=c, \theta) p(t=c | \theta)}{Z} = \frac{\pi_c \mathcal{N}(x | \mu_c, \sigma_c)}{Z}.
   $$

Tenemos un razonamiento circular (un problema del tipo, ¿Qué fue primero?, ¿El huevo?, ¿O la gallina?).

**¿Cómo lo resolvemos? Iterando...**

**Algoritmo de maximización de la esperanza:**

1. Inicializamos los parámetros de cada Gaussiana aleatoriamente.

2. Repetir hasta la convergencia:

   - Calcular para cada punto la probabilidad posterior $p(t_i=c | x_i, \theta)$.
   - Actualizar los parámetros de las Gaussianas con las probabildades calculadas.

Created with Jupyter by Esteban Jiménez Rodríguez.