Source code for bayesflow.simulators.benchmark_simulators.sir

import numpy as np
from scipy.integrate import odeint

from .benchmark_simulator import BenchmarkSimulator


[docs] class SIR(BenchmarkSimulator): def __init__( self, N: float = 1e6, T: int = 160, I0: float = 1.0, R0: float = 0.0, subsample: int | str = "original", total_count: int = 1000, scale_by_total: bool = True, rng: np.random.Generator = None, ): """SIR simulated benchmark See: https://arxiv.org/pdf/2101.04653.pdf, Task T.9 NOTE: the simulator scales outputs between 0 and 1. Parameters ---------- N: float, optional, default: 1e6 The size of the simulated population. T: int, optional, default: 160 The duration (time horizon) of the simulation. The last time-point is not included. I0: float, optional, default: 1.0 The number of initially infected individuals. R0: float, optional, default: 0.0 The number of initially recovered individuals. subsample: int, str or None, optional, default: 'original' The number of evenly spaced time points to return. If `None`, no subsampling will be performed, all `T` timepoints will be returned and a trailing dimension will be added. If an integer is provided, subsampling is performed and no trailing dimension will be added. 'original' reproduces the original benchmark task subsampling of 10 points. total_count: int, optional, default: 1000 The `N` parameter of the binomial noise distribution. Used just for scaling the data and magnifying the effect of noise, such that `max infected == total_count`. scale_by_total: bool, optional, default: True Scales the outputs by `total_count` if set to True. rng: np.random.Generator or None, optional, default: None An optional random number generator to use. """ self.N = N self.T = T self.I0 = I0 self.R0 = R0 self.subsample = subsample self.total_count = total_count self.scale_by_total = scale_by_total self.rng = rng if self.rng is None: self.rng = np.random.default_rng() def _deriv(self, x, t, N, beta, gamma): """Helper function for scipy.integrate.odeint.""" s, i, r = x dS = -beta * s * i / N dI = beta * s * i / N - gamma * i dR = gamma * i return dS, dI, dR
[docs] def prior(self): """Generates a random draw from a 2-dimensional (independent) lognormal prior which represents the contact and recovery rate parameters of a basic SIR model. Returns ------- params : np.ndarray of shape (2, ) A single draw from the 2-dimensional prior. """ return self.rng.lognormal(mean=[np.log(0.4), np.log(1 / 8)], sigma=[0.5, 0.2])
[docs] def observation_model(self, params: np.ndarray): """Runs a basic SIR model simulation for T time steps and returns `subsample` evenly spaced points from the simulated trajectory, given disease parameters (contact and recovery rate) `params`. Parameters ---------- params : np.ndarray of shape (2,) The 2-dimensional vector of disease parameters. Returns ------- x : np.ndarray of shape (subsample, ) or (T, 1) if subsample=None The time series of simulated infected individuals. A trailing dimension of 1 should be added by a BayesFlow configurator if the data is (properly) to be treated as time series. """ # Create vector (list) of initial conditions x0 = self.N - self.I0 - self.R0, self.I0, self.R0 # Unpack parameter vector into scalars beta, gamma = params # Prepare time vector between 0 and T of length T t_vec = np.arange(0, self.T) # Integrate using scipy and retain only infected (2-nd dimension) irt = odeint(self._deriv, x0, t_vec, args=(self.N, beta, gamma))[:, 1] # Subsample evenly the specified number of points, if specified if self.subsample == "original": irt = irt[::17] elif self.subsample is not None: irt = irt[:: (self.T // self.subsample)] else: irt = irt[:, None] # Truncate irt, so that small underflow below zero becomes zero irt = np.maximum(irt, 0.0) # Add noise and scale, if indicated x = self.rng.binomial(n=self.total_count, p=irt / self.N) if self.scale_by_total: x = x / self.total_count return x