In the following we model the spread of a disease across a two dimensional space in discrete time.
import numpy as np
import scipy.signal as spg
import scipy.stats as sps
p = 0.05 #probability of disease transmission at a single meeting
M = 5 #number of meetings per day for an individual
T = 100 #length of simulation run
gridsize = 200 #10x10 matrix representing geography
p_death = 0.0 #after 5 days infection either die or recover
recovery_time = 14
Let's now look at the population distribution. We'll use trigonometric functions to give us an undulating population spread, high points perhaps representing cities and low points the countryside. Our infected population will be modelled using a 3d array, 1 layer for each day the infection lasts.
population = (100+25*np.fromfunction(lambda x,y: np.sin(x/20)+np.sin(y/20), (gridsize,gridsize), dtype=float)).astype(int)
infected = np.zeros((gridsize,gridsize, recovery_time), dtype = float) #3d array - 1 layer for each day of infection
susceptible = population - infected.sum(axis=2)
this is what the starting population looks like
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1,1,figsize=(15,15))
ax.grid(True, linestyle='--', linewidth=0.5)
plt.title('population density')
plt.pcolormesh(population, cmap=plt.cm.copper, vmin=0, vmax=200)
plt.colorbar()
Now let's seed the population with a small number of initial infections. The rare infection events are modelled using the poisson distribution.
infected[:,:,0] = np.random.poisson(0.001,(gridsize,gridsize))
we'll use a 2d convolution to model the localised mixing of the population. Mixing of individuals occurs within a cell and between the cell and its eight adjacent cells.
weights = np.ones((3,3), dtype=float)
We're ready to start our main simulation loop. On each run we will for each cell (i) calculate the at risk population
results = np.zeros((T+1,2))
results[0] = susceptible.sum(), infected.sum()
for i in np.arange(T):
#calculate local subpopulation relevant for each cell (cell plus immediate neighbours)
local_total = spg.convolve2d(population - dead, weights, mode='same', boundary='fill', fillvalue=0)
local_infected = spg.convolve2d(infected.sum(axis=2), weights, mode='same', boundary='fill', fillvalue=0)
#calculate number of new infections
p_infected = 1 - sps.binom.pmf(k=0, n=M, p=p*local_infected/local_total)
new_infections = sps.binom.rvs(n=susceptible.astype(int), p=p_infected)
#those on last day of infection all recover
susceptible = susceptible + infected[:,:,recovery_time-1]
#age existing infections by 1 day and insert new infection data
infected = np.roll(infected, 1, axis=2)
infected[:,:,0] = new_infections
susceptible = susceptible - new_infections
#store a summary of the position
results[i+1] = susceptible.sum(), infected.sum()
Looking at the summary results, we can see that the disease spreads through the population and stabilises at just over 80%.
If we interrupt the simulation, we can see how the disease spreads geographically. Notice how the 14 day illness duration induces wavefronts in the spread. These wavefronts dissipate as the disease reaches a steady state where the number of new infections balances twith he number of recovers
We can modify our simulation to introduce immunity. Let's say that at the end of a 14 day illness there is a 20% chance of developing immunity.
p_immune = 0.2
immune = np.zeros((gridsize,gridsize), dtype=float)
And our state transition code is augmented to account for the immune group
new_immune = np.floor(infected[:,:,recovery_time-1] * p_death + np.random.uniform(0,1,(gridsize,gridsize)))
immune = immune + new_immune
susceptible = susceptible + (infected[:,:,recovery_time-1] - new_immune)
The result is a wave propagating through the population.