Source code for seirmo._gillespie

#
# This file is part of seirmo (https://github.com/SABS-R3-Epidemiology/seirmo/)
# which is released under the BSD 3-clause license. See accompanying LICENSE.md
# for copyright notice and full license details.
#

import numpy as np
import typing


[docs] def solve_gillespie(propensities: typing.Callable[[np.ndarray], np.ndarray], initial_cond: np.ndarray, t_span: typing.List[float], max_t_step: float = 0.01): """ Solve an initial_cond value problem using gillespie algorithm This function numerically integrates a system of ordinary differential equations given initial_cond conditions. We use the Gillespie algorithm with tau-leaping, so that the time step is not uniform but sampled randomly N.B If all propensities are zero, this would give an infinite timestep. We allow the simulation to continue at a large time_step, to allow for time dependant rates that may be non-zero at later times This time step is drawn from an exp dist based on ((t_stop-t_start)/100) Parameters ---------- propensities : callable of form propensities(state) State is array of length (N+1) of time and count per compartment (for N compartments) Will return an NxN array as a transition matrix for propensities t_span : 2-list of floats Internal of integration (t_start, t_end). The solver starts at t_start and will finish once the first random time_step exceeds t_end (Note that the final value may be greater than t_end) initial_cond : array_like, shape (N,) Initial state, gives counts in each of N compartments max_t_step : float (default value 0.01) This is the maximum allowed timestep, as a fraction of the total t_span Default value is 0.01, so (t_span[1] - t_span[0])/100 Note that the exact time_steps are sampled from an exponential distribution, and so may be smaller than this Yields ------- Numpy array of form [Time, Compartment_1, ...], ie [time, S, E, I, R] Note that time steps are random (not uniform), and final timestep lie after the end of the time_span given """ if len(t_span) != 2: raise ValueError("`t_span must be 2-dimensional - form [start, end]") try: float(t_span[1]) - float(t_span[0]) except ValueError: raise TypeError("Cannot convert t_span values to float") if t_span[0] >= t_span[1]: raise ValueError("End time must be after start time") if t_span[0] < 0: raise ValueError(f"Start time (t = {t_span[0]}) cannot be negative") if np.any(np.array(initial_cond) < 0): raise ValueError("Cannot have negative elements in initial_cond") state = np.zeros(len(initial_cond) + 1) state[0] = t_span[0] state[1:] = initial_cond while state[0] < t_span[1]: propensity_values = propensities(state) if np.any(propensity_values < 0): raise ValueError('Propensity function should not \ return neagtive values') total_rate = np.sum(propensity_values) if total_rate == 0: total_rate = 1 / ((t_span[1] - t_span[0]) * max_t_step) time_step = np.log(1 / np.random.rand()) / total_rate state[0] += time_step normal_prop = propensity_values / total_rate running_tot = 0 random_proc = np.random.rand() for index, value in np.ndenumerate(normal_prop): running_tot += value if value != 0 and running_tot > random_proc: loss, gain = index state[loss + 1] -= 1 # +1 to skip past time index state[gain + 1] += 1 break yield state