Exercises part I: discretization

Exercises part I: discretization#

While discrete-time simulations of continuous models should be avoided as much as possible, they remain incredibly common in practice because they are conceptually simply to implement. (Note that they are not actually simpler: Event-driven simulations often require less coding and calibration!) It is therefore useful to know what the effect and danger of discretization can look like.

Consider the code below, which compares three simulations of a continuous-time logistic growth model: discrete-time agent-based, discrete-time composition, and next-reaction method simulations.

import random
import matplotlib.pyplot as plt
import numpy as np
plt.style.use(['ggplot', 'seaborn-talk'])

#parameters
capacity = 20.0
growth_rate = 1.0/capacity

#Pick timestep based on parameters and the criteria above
dt = 1.0/(growth_rate*capacity)
tmax = 20

#next-reaction method
for sims in range(10):
    t=0
    population = 1
    history_nr = np.ones(1)
    times = np.zeros(1)
    while t<tmax:
        #calculate rate 
        R = population*growth_rate*(capacity-population)
        #draw time to next event and event type
        if R>0.0:
            tau = np.random.exponential(scale=1/R)
            #update history
            population+=1
            t = t+tau
            history_nr = np.append(history_nr, population)
            times = np.append(times, t)
        else:
            history_nr = np.append(history_nr, population)
            times = np.append(times, t)
            break
    #print time series
    plt.plot(times,history_nr, marker="o", ls='--', color='black', alpha=0.2)

#agent-based simulation
for sims in range(10):
    population = 1
    history_ab = np.ones(1)
    #for each generation
    for t in range(int(tmax/dt)):
        #for each active cell
        for agent in range(population):
            #test division
            if random.random() < growth_rate*(capacity-population)*dt:
                population += 1
        #update history
        history_ab = np.append(history_ab, population)
    #print time series
    plt.plot(dt*np.arange(len(history_ab)),history_ab, marker="o", ls='--', color='orange', alpha=0.5)
    
    
#composition simulation
for sims in range(10):
    population = 1
    history_comp = np.ones(1)
    #for each generation
    for t in range(int(tmax/dt)):
        #calculate and add the number of reproduction
        prob = growth_rate*(capacity-population)*dt
        if prob>=0.0:
            population += np.random.binomial(population, growth_rate*(capacity-population)*dt)
        #update history
        history_comp = np.append(history_comp, population)
    #print time series
    plt.plot(dt*np.arange(len(history_comp)),history_comp, marker="o", ls='--', color='blue', alpha=0.5)


#add null simulation with label in legend and label the axes
plt.plot(0,1, marker="o", ls='--', color='black', alpha=0.2, label='Next-reaction method')
plt.plot(0,1, marker="o", ls='--', color='orange', alpha=0.5, label='Discrete agent-based')
plt.plot(0,1, marker="o", ls='--', color='blue', alpha=0.5, label='Discrete composition')
#plot carrying capacity
plt.hlines(capacity, 0, tmax, linestyles='--', label='Carrying capacity')
plt.legend()
plt.ylabel('Population')
plt.xlabel('Time')
plt.show()
../_images/8999161890d578ffa00c1d634cb7998f0ae411af4da6fbd2eecdeddf389d0895.png

Do they look different? Can you figure out why? Which one is giving us the right answer? To break down the different issues at play, you may want to try the following:

  • Average the simulations over multiple realizations and ask how long it takes for the population to reach carrying capacity. Why are they different?

  • Inspect the values produced. Do they confirm our intuition of what logistic growth should be doing?

  • Inspect the code. Are there some checks implemented to avoid getting errors?

  • Play with the discretization of the first two methods. Are there values such that the discrete simulations are close enough to the continuous-time process? If so, which ones?