From e4e08daef3dd8ed1e6ba62a8fff64e0b48856e0a Mon Sep 17 00:00:00 2001 From: roberthinch Date: Mon, 17 Feb 2025 15:22:36 +0000 Subject: [PATCH 1/7] Initial version of stochastic SEIR model without interventions --- DESCRIPTION | 2 +- NAMESPACE | 1 + R/model_stochastic_seir.R | 305 +++++++++++++++++++++++++++++++++++ man/model_stochastic_seir.Rd | 138 ++++++++++++++++ 4 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 R/model_stochastic_seir.R create mode 100644 man/model_stochastic_seir.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 8117964a..6bbc1552 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -72,4 +72,4 @@ Config/testthat/edition: 3 Encoding: UTF-8 Language: en-GB Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.1 +RoxygenNote: 7.3.2 diff --git a/NAMESPACE b/NAMESPACE index 1fcc5134..6423963d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -19,6 +19,7 @@ export(is_vaccination) export(model_default) export(model_diphtheria) export(model_ebola) +export(model_stochastic_seir) export(model_vacamole) export(new_infections) export(outcomes_averted) diff --git a/R/model_stochastic_seir.R b/R/model_stochastic_seir.R new file mode 100644 index 00000000..5ae40bdf --- /dev/null +++ b/R/model_stochastic_seir.R @@ -0,0 +1,305 @@ +#' @title Model an stochastic SEIR epidemic with interventions +#' +#' @name model_stochastic_seir +#' @rdname model_stochastic_seir +#' +#' @description Simulate an epidemic using a stochastic, compartmental +#' epidemic model with the compartments +#' "susceptible", "exposed", "infectious", and "recovered" +#' This model can accommodate heterogeneity in social contacts among demographic +#' groups, as well as differences in the sizes of demographic groups. +#' +#' The `population`, `transmission_rate`, `infectiousness_rate`, and +#' `recovery_rate` +#' arguments are mandatory, while passing an `intervention` +#' is optional and can be used to simulate scenarios with different epidemic +#' responses or different levels of the same type of response. +#' See **Details** for more information. +#' +#' @param population An object of the `population` class, which holds a +#' population contact matrix, a demography vector, and the initial conditions +#' of each demographic group. See [population()]. +#' @param transmission_rate A numeric for the rate at which individuals +#' move from the susceptible to the exposed compartment upon contact with an +#' infectious individual. Often denoted as \eqn{\beta}, with +#' \eqn{\beta = R_0 / \text{infectious period}}. See **Details** for default +#' values. +#' @param infectiousness_rate A numeric for the rate at which individuals +#' move from the exposed to the infectious compartment. Often denoted as +#' \eqn{\sigma}, with \eqn{\sigma = 1.0 / \text{pre-infectious period}}. +#' This value does not depend upon the number of infectious individuals in the +#' population. See **Details** for default values. +#' @param recovery_rate A numeric for the rate at which individuals move +#' from the infectious to the recovered compartment. Often denoted as +#' \eqn{\gamma}, with \eqn{\gamma = 1.0 / \text{infectious period}}. +#' See **Details** for default values. +#' @param intervention A named list of ``s representing optional +#' non-pharmaceutical or pharmaceutical interventions applied during the +#' epidemic. Only a single intervention on social contacts of the class +#' `` is allowed as the named element "contacts". +#' Multiple `` on the model parameters are allowed; see +#' **Details** for the model parameters for which interventions are supported. +#' @param time_dependence A named list where each name +#' is a model parameter, and each element is a function with +#' the first two arguments being the current simulation `time`, and `x`, a value +#' that is dependent on `time` (`x` represents a model parameter). +#' See **Details** for more information, as well as the vignette on time- +#' dependence \code{vignette("time_dependence", package = "epidemics")}. +#' @param time_end The maximum number of timesteps over which to run the model. +#' Taken as days, with a default value of 100 days. May be a numeric vector. +#' @details +#' +#' # Details: Stochastic SEIR model suitable for directly transmitted infections +#' +#' ## Model parameters +#' +#' This model only allows for single, population-wide rates of +#' transitions between compartments per model run. +#' +#' However, model parameters may be passed as numeric vectors. These vectors +#' must follow Tidyverse recycling rules: all vectors must have the same length, +#' or, vectors of length 1 will be recycled to the length of any other vector. +#' +#' The default values are: +#' +#' - Transmission rate (\eqn{\beta}, `transmission_rate`): 0.186, assuming an +#' \eqn{R_0} = 1.3 and an infectious period of 7 days. +#' +#' - Infectiousness rate (\eqn{\sigma}, `infectiousness_rate`): 0.5, assuming +#' a pre-infectious period of 2 days. +#' +#' - Recovery rate (\eqn{\gamma}, `recovery_rate`): 0.143, assuming an +#' infectious period of 7 days. +#' +#' @return A ``. +#' If the model parameters and composable elements are all scalars, a single +#' `` with the columns "time", "compartment", "age_group", and +#' "value", giving the number of individuals per demographic group +#' in each compartment at each timestep in long (or "tidy") format is returned. +#' +#' If the model parameters or composable elements are lists or list-like, +#' a nested `` is returned with a list column "data", which holds +#' the compartmental values described above. +#' Other columns hold parameters and composable elements relating to the model +#' run. Columns "scenario" and "param_set" identify combinations of composable +#' elements (population, interventions), and infection +#' parameters, respectively. +#' @examples +#' # create a population +#' uk_population <- population( +#' name = "UK population", +#' contact_matrix = matrix(1), +#' demography_vector = 67e6, +#' initial_conditions = matrix( +#' c(0.9999, 0.0001, 0, 0, 0), +#' nrow = 1, ncol = 5L +#' ) +#' ) +#' +#' # run epidemic simulation with no vaccination or intervention +#' # and three discrete values of transmission rate +#' data <- model_stochastic_seir( +#' population = uk_population, +#' transmission_rate = c(1.3, 1.4, 1.5) / 7.0, # uncertainty in R0 +#' ) +#' +#' # view some data +#' data +#' +#' # run epidemic simulations with differences in the end time +#' # may be useful when considering different start dates with a fixed end point +#' data <- model_stochastic_seir( +#' population = uk_population, +#' time_end = c(50, 100, 150) +#' ) +#' +#' data +#' @export +model_stochastic_seir <- function(population, + transmission_rate = 1.3 / 7.0, + infectiousness_rate = 1.0 / 2.0, + recovery_rate = 1.0 / 7.0, + intervention = NULL, + time_dependence = NULL, + time_end = 100 , + n_samples = 1000 ) { + # get compartment names + compartments <- c( + "susceptible", "exposed", "infectious", "recovered" + ) + assert_population(population, compartments) + + # NOTE: model rates very likely bounded 0 - 1 but no upper limit set for now + checkmate::assert_numeric(transmission_rate, lower = 0, finite = TRUE) + checkmate::assert_numeric(infectiousness_rate, lower = 0, finite = TRUE) + checkmate::assert_numeric(recovery_rate, lower = 0, finite = TRUE) + checkmate::assert_integerish(time_end, lower = 0) + + # check the time end + # restrict increment to lower limit of 1e-6 + checkmate::assert_integerish(time_end, lower = 0) + + # check all vector lengths are equal or 1L + params <- list( + transmission_rate = transmission_rate, + infectiousness_rate = infectiousness_rate, + recovery_rate = recovery_rate, + time_end = time_end + ) + # take parameter names here as names(DT) updates by reference! + param_names <- names(params) + + # Check if `intervention` is a single intervention set or a list of such sets + # NULL is allowed; + is_lofints <- checkmate::test_list( + intervention, "intervention", + all.missing = FALSE, null.ok = TRUE + ) + # allow some NULLs (a valid no intervention scenario) but not all NULLs + is_lofls <- checkmate::test_list( + intervention, + types = c("list", "null"), all.missing = FALSE + ) && + # Check that all elements of intervention sets are either `` + # or NULL + all( + vapply( + unlist(intervention, recursive = FALSE), + FUN = function(x) { + is_intervention(x) || is.null(x) + }, TRUE + ) + ) + + # Check if parameters can be recycled; + stopifnot( + "All parameters must be of the same length, or must have length 1" = + .test_recyclable(params), + "`intervention` must be a list of s or a list of such lists" = + is_lofints || is_lofls + ) + + # make lists if not lists + if (is_lofints) { + intervention <- list(intervention) + } + + # check that time-dependence functions are passed as a list with at least the + # arguments `time` and `x`, in order as the first two args + # NOTE: this functionality is not vectorised; + # convert to list for data.table list column + checkmate::assert_list( + time_dependence, "function", + null.ok = TRUE, + any.missing = FALSE, names = "unique" + ) + # lapply on null returns an empty list + invisible( + lapply(time_dependence, checkmate::assert_function, + args = c("time", "x"), ordered = TRUE + ) + ) + time_dependence <- list( + .cross_check_timedep( + time_dependence, + c("transmission_rate", "infectiousness_rate", "recovery_rate") + ) + ) + + # set up matrices for the state + n_groups <- length( population$demography_vector ) + n_cells <- n_groups * n_samples + pop <- population$demography_vector + + # TO MIMIC BEHAVIOUR OF model_default(), NEED TO DEAL WITH THE INITIAL CONDITIONS + # BEING UNNAMED OR NAMED S,E,I,R (I.E. NOT THE COMPARTMENT NAMES) + initial_conds <- population$initial_conditions + name_overlap <- intersect( compartments, colnames( population$initial_conditions ) ) + if( length( name_overlap ) == 0 ) { + initial_conds <- `colnames<-`( initial_conds, compartments ) + } else checkmate::assert( length( name_overlap ) == ncol( initial_conds), name = "initial_conditions column names") + + # put in initial conditions + states <- list() + for( state in compartments ) { + states[[ state ]] <- matrix( ceiling( initial_conds[ , state ] * pop ), + nrow = n_groups, ncol = n_samples ) + } + + # set up rates + rates <- list( + SE = transmission_rate, + EI = infectiousness_rate, + IR = recovery_rate + ) + + # adjust the contact to includes rates and population information + contact_matrix <- population$contact_matrix + contact_matrix <- diag( 1 / mean( contact_matrix) / n_groups / pop ) %*% contact_matrix + + # add time dependence to rates and contat matrix + time_dep_rate <- .process_time_dependent_rates( contact_matrix, rates, time_end, interventions ) + rates <- time_dep_rate$rate + contact_matrix <- time_dep_rate$contact_matrix + + # set up flows + flows <- list() + flow_names <- names( rates ) + for( flow in flow_names ) { + flows[[ flow ]] <- matrix( NA, nrow = n_groups, ncol = n_samples ) + } + + # set up output + outputs <- vector( mode = "list", length = time_end + 1 ) + output_template <- data.table( + sample = rep( 1:n_samples, each = n_groups, length( compartments ) ), + demography_group = rep( names( pop ), n_samples * length( compartments ) ), + compartment = rep( compartments, each = n_groups * n_samples ) + ) + outputs[[ 1 ]] <- copy( output_template ) + outputs[[ 1 ]][ , time := 0 ] + outputs[[ 1 ]][ , value := unlist( lapply( states, as.vector ) )] + + for( tdx in 1:time_end ) { + # calculate the transisiotns between compartments + infectious_contacts <- ( ( contact_matrix[[ tdx ]] * rates[[ tdx ]]$SE ) %*% states[[ "infectious" ]] ) + flows[[ "SE" ]] <- matrix( rbinom( n_cells, states[[ "susceptible" ]], infectious_contacts ), nrow = n_groups ) + flows[[ "EI" ]] <- matrix( rbinom( n_cells, states[[ "exposed" ]], rates[[ tdx ]]$EI ), nrow = n_groups ) + flows[[ "IR" ]] <- matrix( rbinom( n_cells, states[[ "infectious" ]], rates[[ tdx ]]$IR ), nrow = n_groups ) + + # update the number in each state + states[[ "susceptible" ]] <- states[[ "susceptible" ]] - flows[[ "SE" ]] + states[[ "exposed" ]] <- states[[ "exposed" ]] + flows[[ "SE" ]] - flows[[ "EI" ]] + states[[ "infectious" ]] <- states[[ "infectious" ]] + flows[[ "EI" ]] - flows[[ "IR" ]] + states[[ "recovered" ]] <- states[[ "recovered" ]] + flows[[ "IR" ]] + + # record output in a new data table + outputs[[ tdx+1 ]] <- copy( output_template ) + outputs[[ tdx+1 ]][ , time := tdx ] + outputs[[ tdx+1 ]][ , value := unlist( lapply( states, as.vector ) )] + } + outputs <- rbindlist( outputs ) + + return( outputs ) +} + +#' .process_time_dependent_rates +#' +#' @description Converts interventions in to time dependent changes in parameters +#' and transmission rates +#' @param contact_matrix The base contact matrix +#' @param rates The base transition rates +#' @param time_end The length of the simulation +#' @param interventions The interverntions applied +#' +#' @return A list of the contact matrix and transition rates applicable for +#' each step of the simulation +#' @keywords internal +.process_time_dependent_rates = function( contact_matrix, rates, time_end, interventions ) { + # start with simply copying them + contact_matrix <- lapply( 1:time_end, function( x ) contact_matrix ) + rates <- lapply( 1:time_end, function( x ) rates ) + + return( list( contact_matrix = contact_matrix, rates = rates ) ) +} + diff --git a/man/model_stochastic_seir.Rd b/man/model_stochastic_seir.Rd new file mode 100644 index 00000000..7a3232f9 --- /dev/null +++ b/man/model_stochastic_seir.Rd @@ -0,0 +1,138 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model_stochastic_seir.R +\name{model_stochastic_seir} +\alias{model_stochastic_seir} +\title{Model an stochastic SEIR epidemic with interventions} +\usage{ +model_stochastic_seir( + population, + transmission_rate = 1.3/7, + infectiousness_rate = 1/2, + recovery_rate = 1/7, + intervention = NULL, + time_dependence = NULL, + time_end = 100, + n_samples = 1000 +) +} +\arguments{ +\item{population}{An object of the \code{population} class, which holds a +population contact matrix, a demography vector, and the initial conditions +of each demographic group. See \code{\link[=population]{population()}}.} + +\item{transmission_rate}{A numeric for the rate at which individuals +move from the susceptible to the exposed compartment upon contact with an +infectious individual. Often denoted as \eqn{\beta}, with +\eqn{\beta = R_0 / \text{infectious period}}. See \strong{Details} for default +values.} + +\item{infectiousness_rate}{A numeric for the rate at which individuals +move from the exposed to the infectious compartment. Often denoted as +\eqn{\sigma}, with \eqn{\sigma = 1.0 / \text{pre-infectious period}}. +This value does not depend upon the number of infectious individuals in the +population. See \strong{Details} for default values.} + +\item{recovery_rate}{A numeric for the rate at which individuals move +from the infectious to the recovered compartment. Often denoted as +\eqn{\gamma}, with \eqn{\gamma = 1.0 / \text{infectious period}}. +See \strong{Details} for default values.} + +\item{intervention}{A named list of \verb{}s representing optional +non-pharmaceutical or pharmaceutical interventions applied during the +epidemic. Only a single intervention on social contacts of the class +\verb{} is allowed as the named element "contacts". +Multiple \verb{} on the model parameters are allowed; see +\strong{Details} for the model parameters for which interventions are supported.} + +\item{time_dependence}{A named list where each name +is a model parameter, and each element is a function with +the first two arguments being the current simulation \code{time}, and \code{x}, a value +that is dependent on \code{time} (\code{x} represents a model parameter). +See \strong{Details} for more information, as well as the vignette on time- +dependence \code{vignette("time_dependence", package = "epidemics")}.} + +\item{time_end}{The maximum number of timesteps over which to run the model. +Taken as days, with a default value of 100 days. May be a numeric vector.} +} +\value{ +A \verb{}. +If the model parameters and composable elements are all scalars, a single +\verb{} with the columns "time", "compartment", "age_group", and +"value", giving the number of individuals per demographic group +in each compartment at each timestep in long (or "tidy") format is returned. + +If the model parameters or composable elements are lists or list-like, +a nested \verb{} is returned with a list column "data", which holds +the compartmental values described above. +Other columns hold parameters and composable elements relating to the model +run. Columns "scenario" and "param_set" identify combinations of composable +elements (population, interventions), and infection +parameters, respectively. +} +\description{ +Simulate an epidemic using a stochastic, compartmental +epidemic model with the compartments +"susceptible", "exposed", "infectious", and "recovered" +This model can accommodate heterogeneity in social contacts among demographic +groups, as well as differences in the sizes of demographic groups. + +The \code{population}, \code{transmission_rate}, \code{infectiousness_rate}, and +\code{recovery_rate} +arguments are mandatory, while passing an \code{intervention} +is optional and can be used to simulate scenarios with different epidemic +responses or different levels of the same type of response. +See \strong{Details} for more information. +} +\section{Details: Stochastic SEIR model suitable for directly transmitted infections}{ +\subsection{Model parameters}{ + +This model only allows for single, population-wide rates of +transitions between compartments per model run. + +However, model parameters may be passed as numeric vectors. These vectors +must follow Tidyverse recycling rules: all vectors must have the same length, +or, vectors of length 1 will be recycled to the length of any other vector. + +The default values are: +\itemize{ +\item Transmission rate (\eqn{\beta}, \code{transmission_rate}): 0.186, assuming an +\eqn{R_0} = 1.3 and an infectious period of 7 days. +\item Infectiousness rate (\eqn{\sigma}, \code{infectiousness_rate}): 0.5, assuming +a pre-infectious period of 2 days. +\item Recovery rate (\eqn{\gamma}, \code{recovery_rate}): 0.143, assuming an +infectious period of 7 days. +} +} +} + +\examples{ +# create a population +uk_population <- population( + name = "UK population", + contact_matrix = matrix(1), + demography_vector = 67e6, + initial_conditions = matrix( + c(0.9999, 0.0001, 0, 0, 0), + nrow = 1, ncol = 5L + ) +) + +# run epidemic simulation with no vaccination or intervention +# and three discrete values of transmission rate +data <- model_default( + population = uk_population, + transmission_rate = c(1.3, 1.4, 1.5) / 7.0, # uncertainty in R0 +) + +# view some data +data + +# run epidemic simulations with differences in the end time +# may be useful when considering different start dates with a fixed end point +data <- model_stochastic_seir( + population = uk_population, + time_end = c(50, 100, 150) +) + +data +} From ec97d3bd3a39ade0272a7561493db18ceaa8ecae Mon Sep 17 00:00:00 2001 From: roberthinch Date: Mon, 17 Feb 2025 16:47:25 +0000 Subject: [PATCH 2/7] Add support for contact rate intervention --- R/model_stochastic_seir.R | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/R/model_stochastic_seir.R b/R/model_stochastic_seir.R index 5ae40bdf..448374a5 100644 --- a/R/model_stochastic_seir.R +++ b/R/model_stochastic_seir.R @@ -180,7 +180,7 @@ model_stochastic_seir <- function(population, ) # make lists if not lists - if (is_lofints) { + if (!is_lofints) { intervention <- list(intervention) } @@ -238,7 +238,7 @@ model_stochastic_seir <- function(population, contact_matrix <- diag( 1 / mean( contact_matrix) / n_groups / pop ) %*% contact_matrix # add time dependence to rates and contat matrix - time_dep_rate <- .process_time_dependent_rates( contact_matrix, rates, time_end, interventions ) + time_dep_rate <- .process_time_dependent_rates( contact_matrix, rates, time_end, intervention ) rates <- time_dep_rate$rate contact_matrix <- time_dep_rate$contact_matrix @@ -295,9 +295,25 @@ model_stochastic_seir <- function(population, #' @return A list of the contact matrix and transition rates applicable for #' each step of the simulation #' @keywords internal -.process_time_dependent_rates = function( contact_matrix, rates, time_end, interventions ) { +.process_time_dependent_rates <- function( contact_matrix, rates, time_end, interventions ) { # start with simply copying them - contact_matrix <- lapply( 1:time_end, function( x ) contact_matrix ) + + # apply the interventions + supported_classes <- c( "contacts_intervention" ) + contact_reduction <- lapply( 1:time_end, function( x ) rep( 1, nrow( contact_matrix ) ) ) + for( intervention in interventions ) { + # check we support this type + type <- class( intervention )[1] + checkmate::assert( type %in% supported_classes, name = sprintf( "<%s> interventions not supported", type ) ) + + if( type == "contacts_intervention" ) { + times <- seq( intervention$time_begin, intervention$time_end ) + red_func <- function( x ) x - as.vector( intervention$reduction ) + contact_reduction[ times ] <- lapply( contact_reduction[ times ], red_func ) + } + } + + contact_matrix <- lapply( contact_reduction, function( x ) diag( x ) %*% contact_matrix ) rates <- lapply( 1:time_end, function( x ) rates ) return( list( contact_matrix = contact_matrix, rates = rates ) ) From ce567c7842511dbd0bd1423e635f907d0834a499 Mon Sep 17 00:00:00 2001 From: roberthinch Date: Mon, 17 Feb 2025 18:08:41 +0000 Subject: [PATCH 3/7] Add support for rate_interventions and multiple interventions of the same type --- R/model_stochastic_seir.R | 67 +++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/R/model_stochastic_seir.R b/R/model_stochastic_seir.R index 448374a5..e56eefdf 100644 --- a/R/model_stochastic_seir.R +++ b/R/model_stochastic_seir.R @@ -228,9 +228,9 @@ model_stochastic_seir <- function(population, # set up rates rates <- list( - SE = transmission_rate, - EI = infectiousness_rate, - IR = recovery_rate + transmission_rate = transmission_rate, + infectiousness_rate = infectiousness_rate, + recovery_rate = recovery_rate ) # adjust the contact to includes rates and population information @@ -238,8 +238,8 @@ model_stochastic_seir <- function(population, contact_matrix <- diag( 1 / mean( contact_matrix) / n_groups / pop ) %*% contact_matrix # add time dependence to rates and contat matrix - time_dep_rate <- .process_time_dependent_rates( contact_matrix, rates, time_end, intervention ) - rates <- time_dep_rate$rate + time_dep_rate <- .process_time_dependent_rates( contact_matrix, rates, time_end, intervention ) + rates <- time_dep_rate$rates contact_matrix <- time_dep_rate$contact_matrix # set up flows @@ -262,10 +262,10 @@ model_stochastic_seir <- function(population, for( tdx in 1:time_end ) { # calculate the transisiotns between compartments - infectious_contacts <- ( ( contact_matrix[[ tdx ]] * rates[[ tdx ]]$SE ) %*% states[[ "infectious" ]] ) + infectious_contacts <- ( ( contact_matrix[[ tdx ]] * rates$transmission_rate[ tdx ] ) %*% states[[ "infectious" ]] ) flows[[ "SE" ]] <- matrix( rbinom( n_cells, states[[ "susceptible" ]], infectious_contacts ), nrow = n_groups ) - flows[[ "EI" ]] <- matrix( rbinom( n_cells, states[[ "exposed" ]], rates[[ tdx ]]$EI ), nrow = n_groups ) - flows[[ "IR" ]] <- matrix( rbinom( n_cells, states[[ "infectious" ]], rates[[ tdx ]]$IR ), nrow = n_groups ) + flows[[ "EI" ]] <- matrix( rbinom( n_cells, states[[ "exposed" ]], rates$infectiousness_rate[ tdx ] ), nrow = n_groups ) + flows[[ "IR" ]] <- matrix( rbinom( n_cells, states[[ "infectious" ]], rates$recovery_rate[ tdx ] ), nrow = n_groups ) # update the number in each state states[[ "susceptible" ]] <- states[[ "susceptible" ]] - flows[[ "SE" ]] @@ -296,25 +296,50 @@ model_stochastic_seir <- function(population, #' each step of the simulation #' @keywords internal .process_time_dependent_rates <- function( contact_matrix, rates, time_end, interventions ) { - # start with simply copying them - - # apply the interventions - supported_classes <- c( "contacts_intervention" ) + # contact reductions + n_groups <- nrow( contact_matrix ) contact_reduction <- lapply( 1:time_end, function( x ) rep( 1, nrow( contact_matrix ) ) ) - for( intervention in interventions ) { - # check we support this type + intervention <- interventions[[ "contacts" ]] + + if( !is.null( intervention ) ) { + # check it is of the correct type type <- class( intervention )[1] - checkmate::assert( type %in% supported_classes, name = sprintf( "<%s> interventions not supported", type ) ) - - if( type == "contacts_intervention" ) { - times <- seq( intervention$time_begin, intervention$time_end ) - red_func <- function( x ) x - as.vector( intervention$reduction ) + checkmate::assert( type == "contacts_intervention", name = "contacts_inteverntion expected" ) + + for( jdx in seq( 1, length( intervention$time_begin ) ) ) { + # apply them + times <- seq( intervention$time_begin[jdx], intervention$time_end[jdx] ) + red_func <- function( x ) x - as.vector( intervention$reduction[,jdx] ) contact_reduction[ times ] <- lapply( contact_reduction[ times ], red_func ) } + + # the cumulative effect of interventions is capped at 100% + contact_reduction <- lapply( contact_reduction, function( x ) pmax( x, 0 ) ) } - + # apply reductions contact_matrix <- lapply( contact_reduction, function( x ) diag( x ) %*% contact_matrix ) - rates <- lapply( 1:time_end, function( x ) rates ) + + # rate reductions + for( rate_type in names( rates ) ) { + rate_reduction <- rep( 1, time_end ) + intervention <- interventions[[ rate_type ]] + if( !is.null( intervention) ) { + # check it is of the correct type + type <- class( intervention )[1] + checkmate::assert( type == "rate_intervention", name = "rate_inteverntion expected" ) + + for( jdx in seq( 1, length( intervention$time_begin ) ) ) { + # apply them + times <- seq( intervention$time_begin[jdx], intervention$time_end[jdx] ) + rate_reduction[ times ] <- rate_reduction[ times ] - intervention$reduction[jdx] + } + + # the cumulative effect of interventions is capped at 100% + rate_reduction <- pmax( rate_reduction, 0 ) + } + # apply reductions + rates[[ rate_type ]] <- rates[[ rate_type ]] * rate_reduction + } return( list( contact_matrix = contact_matrix, rates = rates ) ) } From 4d9eb74b972fa8b08dcd1193d521e8c02df0b806 Mon Sep 17 00:00:00 2001 From: roberthinch Date: Tue, 18 Feb 2025 16:04:40 +0000 Subject: [PATCH 4/7] Add tests for model_stochastic_seir based on those for model_default Do not include time_depedence and vectorised parameter functionallity --- R/model_stochastic_seir.R | 108 ++------ tests/testthat/test-model_stochastic_seir.R | 293 ++++++++++++++++++++ 2 files changed, 316 insertions(+), 85 deletions(-) create mode 100644 tests/testthat/test-model_stochastic_seir.R diff --git a/R/model_stochastic_seir.R b/R/model_stochastic_seir.R index e56eefdf..62f316c1 100644 --- a/R/model_stochastic_seir.R +++ b/R/model_stochastic_seir.R @@ -39,12 +39,6 @@ #' `` is allowed as the named element "contacts". #' Multiple `` on the model parameters are allowed; see #' **Details** for the model parameters for which interventions are supported. -#' @param time_dependence A named list where each name -#' is a model parameter, and each element is a function with -#' the first two arguments being the current simulation `time`, and `x`, a value -#' that is dependent on `time` (`x` represents a model parameter). -#' See **Details** for more information, as well as the vignette on time- -#' dependence \code{vignette("time_dependence", package = "epidemics")}. #' @param time_end The maximum number of timesteps over which to run the model. #' Taken as days, with a default value of 100 days. May be a numeric vector. #' @details @@ -120,7 +114,6 @@ model_stochastic_seir <- function(population, infectiousness_rate = 1.0 / 2.0, recovery_rate = 1.0 / 7.0, intervention = NULL, - time_dependence = NULL, time_end = 100 , n_samples = 1000 ) { # get compartment names @@ -134,77 +127,13 @@ model_stochastic_seir <- function(population, checkmate::assert_numeric(infectiousness_rate, lower = 0, finite = TRUE) checkmate::assert_numeric(recovery_rate, lower = 0, finite = TRUE) checkmate::assert_integerish(time_end, lower = 0) - - # check the time end - # restrict increment to lower limit of 1e-6 - checkmate::assert_integerish(time_end, lower = 0) - - # check all vector lengths are equal or 1L - params <- list( - transmission_rate = transmission_rate, - infectiousness_rate = infectiousness_rate, - recovery_rate = recovery_rate, - time_end = time_end - ) - # take parameter names here as names(DT) updates by reference! - param_names <- names(params) - - # Check if `intervention` is a single intervention set or a list of such sets - # NULL is allowed; - is_lofints <- checkmate::test_list( - intervention, "intervention", - all.missing = FALSE, null.ok = TRUE - ) - # allow some NULLs (a valid no intervention scenario) but not all NULLs - is_lofls <- checkmate::test_list( - intervention, - types = c("list", "null"), all.missing = FALSE - ) && - # Check that all elements of intervention sets are either `` - # or NULL - all( - vapply( - unlist(intervention, recursive = FALSE), - FUN = function(x) { - is_intervention(x) || is.null(x) - }, TRUE - ) - ) - - # Check if parameters can be recycled; - stopifnot( - "All parameters must be of the same length, or must have length 1" = - .test_recyclable(params), - "`intervention` must be a list of s or a list of such lists" = - is_lofints || is_lofls - ) - - # make lists if not lists - if (!is_lofints) { - intervention <- list(intervention) - } - - # check that time-dependence functions are passed as a list with at least the - # arguments `time` and `x`, in order as the first two args - # NOTE: this functionality is not vectorised; - # convert to list for data.table list column - checkmate::assert_list( - time_dependence, "function", - null.ok = TRUE, - any.missing = FALSE, names = "unique" - ) - # lapply on null returns an empty list - invisible( - lapply(time_dependence, checkmate::assert_function, - args = c("time", "x"), ordered = TRUE - ) - ) - time_dependence <- list( - .cross_check_timedep( - time_dependence, - c("transmission_rate", "infectiousness_rate", "recovery_rate") - ) - ) + checkmate::assert_integerish(n_samples, lower = 1) + + # only support scalar parameters + checkmate::assert_scalar(transmission_rate) + checkmate::assert_scalar(infectiousness_rate) + checkmate::assert_scalar(recovery_rate) + checkmate::assert_scalar(n_samples) # set up matrices for the state n_groups <- length( population$demography_vector ) @@ -235,10 +164,11 @@ model_stochastic_seir <- function(population, # adjust the contact to includes rates and population information contact_matrix <- population$contact_matrix + group_names <- colnames( contact_matrix ) contact_matrix <- diag( 1 / mean( contact_matrix) / n_groups / pop ) %*% contact_matrix # add time dependence to rates and contat matrix - time_dep_rate <- .process_time_dependent_rates( contact_matrix, rates, time_end, intervention ) + time_dep_rate <- .prepare_interventions( contact_matrix, rates, time_end, intervention ) rates <- time_dep_rate$rates contact_matrix <- time_dep_rate$contact_matrix @@ -253,10 +183,10 @@ model_stochastic_seir <- function(population, outputs <- vector( mode = "list", length = time_end + 1 ) output_template <- data.table( sample = rep( 1:n_samples, each = n_groups, length( compartments ) ), - demography_group = rep( names( pop ), n_samples * length( compartments ) ), + demography_group = rep( group_names, n_samples * length( compartments ) ), compartment = rep( compartments, each = n_groups * n_samples ) ) - outputs[[ 1 ]] <- copy( output_template ) + outputs[[ 1 ]] <- data.table::copy( output_template ) outputs[[ 1 ]][ , time := 0 ] outputs[[ 1 ]][ , value := unlist( lapply( states, as.vector ) )] @@ -274,16 +204,16 @@ model_stochastic_seir <- function(population, states[[ "recovered" ]] <- states[[ "recovered" ]] + flows[[ "IR" ]] # record output in a new data table - outputs[[ tdx+1 ]] <- copy( output_template ) + outputs[[ tdx+1 ]] <- data.table::copy( output_template ) outputs[[ tdx+1 ]][ , time := tdx ] outputs[[ tdx+1 ]][ , value := unlist( lapply( states, as.vector ) )] } - outputs <- rbindlist( outputs ) + outputs <- data.table::rbindlist( outputs ) return( outputs ) } -#' .process_time_dependent_rates +#' .prepare_interventions #' #' @description Converts interventions in to time dependent changes in parameters #' and transmission rates @@ -295,7 +225,7 @@ model_stochastic_seir <- function(population, #' @return A list of the contact matrix and transition rates applicable for #' each step of the simulation #' @keywords internal -.process_time_dependent_rates <- function( contact_matrix, rates, time_end, interventions ) { +.prepare_interventions <- function( contact_matrix, rates, time_end, interventions ) { # contact reductions n_groups <- nrow( contact_matrix ) contact_reduction <- lapply( 1:time_end, function( x ) rep( 1, nrow( contact_matrix ) ) ) @@ -304,9 +234,14 @@ model_stochastic_seir <- function(population, if( !is.null( intervention ) ) { # check it is of the correct type type <- class( intervention )[1] + checkmate::assert( type == "contacts_intervention", name = "contacts_inteverntion expected" ) + checkmate::assert( nrow( intervention$reduction ) == nrow( contact_matrix), name = "incorrect rows in reduction of contact_intervention") for( jdx in seq( 1, length( intervention$time_begin ) ) ) { + if( intervention$time_begin[jdx] > intervention$time_end[jdx] ) + next; + # apply them times <- seq( intervention$time_begin[jdx], intervention$time_end[jdx] ) red_func <- function( x ) x - as.vector( intervention$reduction[,jdx] ) @@ -329,6 +264,9 @@ model_stochastic_seir <- function(population, checkmate::assert( type == "rate_intervention", name = "rate_inteverntion expected" ) for( jdx in seq( 1, length( intervention$time_begin ) ) ) { + if( intervention$time_begin[jdx] > intervention$time_end[jdx] ) + next; + # apply them times <- seq( intervention$time_begin[jdx], intervention$time_end[jdx] ) rate_reduction[ times ] <- rate_reduction[ times ] - intervention$reduction[jdx] diff --git a/tests/testthat/test-model_stochastic_seir.R b/tests/testthat/test-model_stochastic_seir.R new file mode 100644 index 00000000..6726e8e3 --- /dev/null +++ b/tests/testthat/test-model_stochastic_seir.R @@ -0,0 +1,293 @@ +#### Tests for the stochastic SEIR model #### +# Prepare contact matrix and demography vector +polymod <- socialmixr::polymod +contact_data <- socialmixr::contact_matrix( + polymod, + countries = "United Kingdom", + age.limits = c(0, 60), + symmetric = TRUE +) +contact_matrix <- t(contact_data$matrix) +demography_vector <- contact_data$demography$population + +# make initial conditions - order is important +initial_conditions <- c( + S = 1 - 1e-6, E = 0, + I = 1e-6, R = 0 +) +initial_conditions <- rbind( + initial_conditions, + initial_conditions +) + +# create a population +uk_population <- population( + name = "UK population", + contact_matrix = contact_matrix, + demography_vector = demography_vector, + initial_conditions = initial_conditions +) + +# prepare a two dose vaccination regime for three age groups +single_vaccination <- vaccination( + name = "double_vaccination", + nu = matrix(1e-3, nrow = 2), + time_begin = matrix(0, nrow = 2), + time_end = matrix(100, nrow = 2) +) + +# model run time +time_end <- 100L +compartments <- c( + "susceptible", "exposed", "infectious", "recovered" +) +n_samples <- 100L + +test_that("Stochastic SEIR model: basic expectations, scalar arguments", { + # expect run with no conditions for default arguments + expect_no_condition(model_stochastic_seir(uk_population)) + + # expect data.frame-nheriting output with 4 cols; C++ model time begins at 0 + data <- model_stochastic_seir(uk_population, n_samples = n_samples ) + expect_s3_class(data, "data.frame") + expect_identical(length(data), 5L) + expect_named( + data, c( "sample", "time", "demography_group", "compartment", "value" ), + ignore.order = TRUE + ) + expect_identical( + nrow(data), + length(demography_vector) * (time_end + 1L) * length(compartments) * n_samples + ) + expect_identical(unique(data$compartment), compartments) + expect_true( + checkmate::test_numeric( + data$value, + upper = max(demography_vector), lower = 0, any.missing = FALSE + ) + ) + expect_identical( + unique(data$demography_group), rownames(contact_matrix) + ) + + # expect constant population size overall and per demography-group + expect_identical( + sum(data[data$time == min(data$time), ]$value), + sum(data[data$time == max(data$time), ]$value), + tolerance = 1e-6 + ) + ############ + final_state <- data[ time == max( time ), .( count = sum( value ) ), + by = c("demography_group", "sample" ) ] + dt_demography <- data.table( pop = uk_population$demography_vector, + demography_group = rownames(contact_matrix)) + final_state <- dt_demography[ final_state, on = "demography_group"] + + expect_identical( + final_state[, count], final_state[ ,pop], + tolerance = 1e-6 + ) +}) + +# NOTE: statistical correctness is not expected to change for vectorised input +test_that("Stochastic SEIR model: statistical correctness, parameters", { + # expect final size increases with transmission_rate + size_beta_low <- epidemic_size( + model_stochastic_seir(uk_population, transmission_rate = 1.3 / 7.0, n_samples = n_samples ) + ) + size_beta_high <- epidemic_size( + model_stochastic_seir(uk_population, transmission_rate = 1.5 / 7.0, n_samples = n_samples ) + ) + expect_true( + all(size_beta_high > size_beta_low) + ) + + # expect final size increases with infectiousness rate (lower incubation time) + size_sigma_low <- epidemic_size( + model_stochastic_seir(uk_population, infectiousness_rate = 1 / 5, n_samples = n_samples ) + ) + size_sigma_high <- epidemic_size( + model_stochastic_seir(uk_population, infectiousness_rate = 1 / 2, , n_samples = n_samples ) + ) + expect_true( + all(size_sigma_high > size_sigma_low) + ) + + # expect final size increases with initial infections + initial_conditions_high <- c( + S = 1 - 10e-6, E = 0, I = 10e-6, + R = 0 + ) + initial_conditions_high <- rbind( + initial_conditions_high, + initial_conditions_high + ) + uk_population_high_infections <- population( + name = "UK population", + contact_matrix = contact_matrix, + demography_vector = demography_vector, + initial_conditions = initial_conditions_high + ) + size_infections_low <- epidemic_size( + model_stochastic_seir(uk_population, n_samples = n_samples ) + ) + size_infections_high <- epidemic_size( + model_stochastic_seir(uk_population_high_infections, n_samples = n_samples ) + ) + expect_true( + all(size_infections_high > size_infections_low) + ) +}) + +# prepare baseline for comparison of against intervention scenarios +data_baseline <- model_stochastic_seir(uk_population, n_samples = n_samples ) + +test_that("Stochastic SEIR model: contacts interventions and stats. correctness", { + intervention <- intervention( + "school_closure", "contacts", 0, time_end, c(0.5, 0.0) + ) + # repeat some basic checks from default case with no intervention + # expect run with no conditions for default arguments + expect_no_condition( + model_stochastic_seir( + uk_population, + intervention = list(contacts = intervention), + n_samples = n_samples + ) + ) + + # expect data.frame-inheriting output with 5 cols + data <- model_stochastic_seir( + uk_population, + intervention = list(contacts = intervention), + n_samples = n_samples + ) + expect_s3_class(data, "data.frame") + expect_identical(length(data), 5L) + + # expect final size is lower with intervention + expect_true( + all(epidemic_size(data_baseline) > epidemic_size(data)) + ) + + # expect model runs with multiple contacts interventions + # expect that effect of multiple interventions is greater than single + intervention_02 <- intervention( + "work_closure", "contacts", 0, time_end, c(0.1, 0.5) + ) + combined_interventions <- c(intervention, intervention_02) + + expect_no_condition( + model_stochastic_seir( + uk_population, + intervention = list(contacts = combined_interventions), + n_samples = n_samples + ) + ) + data_combined <- model_stochastic_seir( + uk_population, + intervention = list(contacts = combined_interventions), + n_samples = n_samples + ) + # expect epidemic size is lower for combined intervention + expect_true( + all(epidemic_size(data_combined) < epidemic_size(data)) + ) +}) + +test_that("Stochastic SEIR model: rate interventions", { + intervention_01 <- intervention( + "mask_mandate", "rate", 0, time_end, 0.5 + ) + intervention_02 <- intervention( + "mask_mandate", "rate", time_end / 2, time_end, 0.1 + ) + intervention <- c(intervention_01, intervention_02) + # repeat some basic checks from default case with no intervention + # expect run with no conditions for default arguments + expect_no_condition( + model_stochastic_seir( + uk_population, + intervention = list(transmission_rate = intervention), + n_samples = n_samples + ) + ) + + # expect data.frame-inheriting output with 4 cols; C++ model time begins at 0 + data <- model_stochastic_seir( + uk_population, + intervention = list(transmission_rate = intervention), + n_samples = n_samples + ) + expect_s3_class(data, "data.frame") + expect_identical(length(data), 5L) + + # expect final size is lower with intervention + expect_true( + all(epidemic_size(data_baseline) > epidemic_size(data)) + ) +}) + +test_that("Stochastic SEIR model: errors and warnings, scalar arguments", { + # expect errors on basic input checking + expect_error( + model_stochastic_seir(population = "population",n_samples = n_samples), + regexp = "(Assertion on 'population' failed)*(Must inherit)*(population)" + ) + expect_error( + model_stochastic_seir(population = population,n_samples = n_samples), + regexp = "(Assertion on 'population' failed)*(Must inherit)*(population)" + ) + pop_wrong_compartments <- uk_population + pop_wrong_compartments$initial_conditions <- initial_conditions[, -1] + expect_error( + model_stochastic_seir(pop_wrong_compartments,n_samples = n_samples), + regexp = "(Assertion on)*(initial_conditions)*failed" + ) + + # expect errors for infection parameters + expect_error( + model_stochastic_seir(uk_population, transmission_rate = "0.19",n_samples = n_samples), + regexp = "Must be of type 'numeric'" + ) + expect_error( + model_stochastic_seir(uk_population, infectiousness_rate = list(0.2),n_samples = n_samples), + regexp = "Must be of type 'numeric'" + ) + expect_error( + model_stochastic_seir(uk_population, recovery_rate = "0.19",n_samples = n_samples), + regexp = "Must be of type 'numeric'" + ) + + # expect error on time parameters + expect_error( + model_stochastic_seir(uk_population, time_end = "100",n_samples = n_samples), + regexp = "Must be of type 'integerish'" + ) + expect_error( + model_stochastic_seir(uk_population, time_end = 100.5,n_samples = n_samples), + regexp = "Must be of type 'integerish'" + ) + expect_error( + model_stochastic_seir(uk_population, time_end = c(100, -100, 10),n_samples = n_samples), + regexp = "(Element)*(is not >= 0)" + ) + # expect error on poorly specified interventions + intervention <- intervention( + "school_closure", "contacts", 0, time_end, 0.5 # needs two effects + ) + expect_error( + model_stochastic_seir( + uk_population, + intervention = list(contacts = intervention), + n_samples = n_samples + ) + ) + expect_error( + model_stochastic_seir( + uk_population, + intervention = list(transmission_rate = intervention), + n_samples = n_samples + ) + ) +}) From 53602711e7bc6db960bda32849d4a3a3f14b3636 Mon Sep 17 00:00:00 2001 From: roberthinch Date: Tue, 18 Feb 2025 17:03:59 +0000 Subject: [PATCH 5/7] Update documentation and run roxygen Fix edge case for when the population is unstructured (i.e. a single group) --- R/model_stochastic_seir.R | 55 ++++++++++++---------------- man/dot-prepare_interventions.Rd | 26 ++++++++++++++ man/model_stochastic_seir.Rd | 62 +++++++++++--------------------- 3 files changed, 69 insertions(+), 74 deletions(-) create mode 100644 man/dot-prepare_interventions.Rd diff --git a/R/model_stochastic_seir.R b/R/model_stochastic_seir.R index 62f316c1..d6efc608 100644 --- a/R/model_stochastic_seir.R +++ b/R/model_stochastic_seir.R @@ -3,11 +3,18 @@ #' @name model_stochastic_seir #' @rdname model_stochastic_seir #' -#' @description Simulate an epidemic using a stochastic, compartmental +#' @description Simulate an epidemic using a stochastic compartmental #' epidemic model with the compartments -#' "susceptible", "exposed", "infectious", and "recovered" -#' This model can accommodate heterogeneity in social contacts among demographic +#' "susceptible", "exposed", "infectious", and "recovered". +#' The model can accommodate heterogeneity in social contacts among demographic #' groups, as well as differences in the sizes of demographic groups. +#' Each individual within a compartment is assumed to be independent of the +#' others, with inter-compartment transition times being geometrically distributed. +#' For exposures to infections, the compartments are assumed to be well-mixed, +#' thus the level of exposure felt be each individual within a compartment is +#' the same. Again, we assume individuals within a compartment are independent +#' of others in the compartment, thus the number of new infections is binomially +#' distributed. #' #' The `population`, `transmission_rate`, `infectiousness_rate`, and #' `recovery_rate` @@ -40,7 +47,8 @@ #' Multiple `` on the model parameters are allowed; see #' **Details** for the model parameters for which interventions are supported. #' @param time_end The maximum number of timesteps over which to run the model. -#' Taken as days, with a default value of 100 days. May be a numeric vector. +#' Taken as days, with a default value of 100 days. +#' @param n_samples The number of stochastic replicates of the model (default = 1,000). #' @details #' #' # Details: Stochastic SEIR model suitable for directly transmitted infections @@ -49,10 +57,8 @@ #' #' This model only allows for single, population-wide rates of #' transitions between compartments per model run. -#' -#' However, model parameters may be passed as numeric vectors. These vectors -#' must follow Tidyverse recycling rules: all vectors must have the same length, -#' or, vectors of length 1 will be recycled to the length of any other vector. +#' Additionally, the transmission, infectiousness and recovery rates must +#' be scalars. #' #' The default values are: #' @@ -66,18 +72,11 @@ #' infectious period of 7 days. #' #' @return A ``. -#' If the model parameters and composable elements are all scalars, a single -#' `` with the columns "time", "compartment", "age_group", and -#' "value", giving the number of individuals per demographic group +#' `` with the columns "sample", time", "compartment", "age_group", +#' and"value", giving the number of individuals per demographic group #' in each compartment at each timestep in long (or "tidy") format is returned. #' -#' If the model parameters or composable elements are lists or list-like, -#' a nested `` is returned with a list column "data", which holds -#' the compartmental values described above. -#' Other columns hold parameters and composable elements relating to the model -#' run. Columns "scenario" and "param_set" identify combinations of composable -#' elements (population, interventions), and infection -#' parameters, respectively. +# #' @examples #' # create a population #' uk_population <- population( @@ -85,29 +84,19 @@ #' contact_matrix = matrix(1), #' demography_vector = 67e6, #' initial_conditions = matrix( -#' c(0.9999, 0.0001, 0, 0, 0), -#' nrow = 1, ncol = 5L +#' c(0.9999, 0.0001, 0, 0), +#' nrow = 1, ncol = 4L #' ) #' ) #' #' # run epidemic simulation with no vaccination or intervention -#' # and three discrete values of transmission rate #' data <- model_stochastic_seir( #' population = uk_population, -#' transmission_rate = c(1.3, 1.4, 1.5) / 7.0, # uncertainty in R0 +#' transmission_rate = 1.5 / 7.0 #' ) #' #' # view some data #' data -#' -#' # run epidemic simulations with differences in the end time -#' # may be useful when considering different start dates with a fixed end point -#' data <- model_stochastic_seir( -#' population = uk_population, -#' time_end = c(50, 100, 150) -#' ) -#' -#' data #' @export model_stochastic_seir <- function(population, transmission_rate = 1.3 / 7.0, @@ -165,7 +154,7 @@ model_stochastic_seir <- function(population, # adjust the contact to includes rates and population information contact_matrix <- population$contact_matrix group_names <- colnames( contact_matrix ) - contact_matrix <- diag( 1 / mean( contact_matrix) / n_groups / pop ) %*% contact_matrix + contact_matrix <- diag( 1 / mean( contact_matrix) / n_groups / pop, nrow = n_groups ) %*% contact_matrix # add time dependence to rates and contat matrix time_dep_rate <- .prepare_interventions( contact_matrix, rates, time_end, intervention ) @@ -252,7 +241,7 @@ model_stochastic_seir <- function(population, contact_reduction <- lapply( contact_reduction, function( x ) pmax( x, 0 ) ) } # apply reductions - contact_matrix <- lapply( contact_reduction, function( x ) diag( x ) %*% contact_matrix ) + contact_matrix <- lapply( contact_reduction, function( x ) diag( x, nrow = n_groups ) %*% contact_matrix ) # rate reductions for( rate_type in names( rates ) ) { diff --git a/man/dot-prepare_interventions.Rd b/man/dot-prepare_interventions.Rd new file mode 100644 index 00000000..11b55d14 --- /dev/null +++ b/man/dot-prepare_interventions.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/model_stochastic_seir.R +\name{.prepare_interventions} +\alias{.prepare_interventions} +\title{.prepare_interventions} +\usage{ +.prepare_interventions(contact_matrix, rates, time_end, interventions) +} +\arguments{ +\item{contact_matrix}{The base contact matrix} + +\item{rates}{The base transition rates} + +\item{time_end}{The length of the simulation} + +\item{interventions}{The interverntions applied} +} +\value{ +A list of the contact matrix and transition rates applicable for +each step of the simulation +} +\description{ +Converts interventions in to time dependent changes in parameters +and transmission rates +} +\keyword{internal} diff --git a/man/model_stochastic_seir.Rd b/man/model_stochastic_seir.Rd index 7a3232f9..6b973cd2 100644 --- a/man/model_stochastic_seir.Rd +++ b/man/model_stochastic_seir.Rd @@ -10,7 +10,6 @@ model_stochastic_seir( infectiousness_rate = 1/2, recovery_rate = 1/7, intervention = NULL, - time_dependence = NULL, time_end = 100, n_samples = 1000 ) @@ -44,37 +43,30 @@ epidemic. Only a single intervention on social contacts of the class Multiple \verb{} on the model parameters are allowed; see \strong{Details} for the model parameters for which interventions are supported.} -\item{time_dependence}{A named list where each name -is a model parameter, and each element is a function with -the first two arguments being the current simulation \code{time}, and \code{x}, a value -that is dependent on \code{time} (\code{x} represents a model parameter). -See \strong{Details} for more information, as well as the vignette on time- -dependence \code{vignette("time_dependence", package = "epidemics")}.} - \item{time_end}{The maximum number of timesteps over which to run the model. -Taken as days, with a default value of 100 days. May be a numeric vector.} +Taken as days, with a default value of 100 days.} + +\item{n_samples}{The number of stochastic replicates of the model (default = 1,000).} } \value{ A \verb{}. -If the model parameters and composable elements are all scalars, a single -\verb{} with the columns "time", "compartment", "age_group", and -"value", giving the number of individuals per demographic group +\verb{} with the columns "sample", time", "compartment", "age_group", +and"value", giving the number of individuals per demographic group in each compartment at each timestep in long (or "tidy") format is returned. - -If the model parameters or composable elements are lists or list-like, -a nested \verb{} is returned with a list column "data", which holds -the compartmental values described above. -Other columns hold parameters and composable elements relating to the model -run. Columns "scenario" and "param_set" identify combinations of composable -elements (population, interventions), and infection -parameters, respectively. } \description{ -Simulate an epidemic using a stochastic, compartmental +Simulate an epidemic using a stochastic compartmental epidemic model with the compartments -"susceptible", "exposed", "infectious", and "recovered" -This model can accommodate heterogeneity in social contacts among demographic +"susceptible", "exposed", "infectious", and "recovered". +The model can accommodate heterogeneity in social contacts among demographic groups, as well as differences in the sizes of demographic groups. +Each individual within a compartment is assumed to be independent of the +others, with inter-compartment transition times being geometrically distributed. +For exposures to infections, the compartments are assumed to be well-mixed, +thus the level of exposure felt be each individual within a compartment is +the same. Again, we assume individuals within a compartment are independent +of others in the compartment, thus the number of new infections is binomially +distributed. The \code{population}, \code{transmission_rate}, \code{infectiousness_rate}, and \code{recovery_rate} @@ -88,10 +80,8 @@ See \strong{Details} for more information. This model only allows for single, population-wide rates of transitions between compartments per model run. - -However, model parameters may be passed as numeric vectors. These vectors -must follow Tidyverse recycling rules: all vectors must have the same length, -or, vectors of length 1 will be recycled to the length of any other vector. +Additionally, the transmission, infectiousness and recovery rates must +be scalars. The default values are: \itemize{ @@ -112,27 +102,17 @@ uk_population <- population( contact_matrix = matrix(1), demography_vector = 67e6, initial_conditions = matrix( - c(0.9999, 0.0001, 0, 0, 0), - nrow = 1, ncol = 5L + c(0.9999, 0.0001, 0, 0), + nrow = 1, ncol = 4L ) ) # run epidemic simulation with no vaccination or intervention -# and three discrete values of transmission rate -data <- model_default( - population = uk_population, - transmission_rate = c(1.3, 1.4, 1.5) / 7.0, # uncertainty in R0 -) - -# view some data -data - -# run epidemic simulations with differences in the end time -# may be useful when considering different start dates with a fixed end point data <- model_stochastic_seir( population = uk_population, - time_end = c(50, 100, 150) + transmission_rate = 1.5 / 7.0 ) +# view some data data } From 6f8b1fbfd50d2b27dfd773e8a9d54160379791fe Mon Sep 17 00:00:00 2001 From: roberthinch Date: Tue, 18 Feb 2025 17:53:18 +0000 Subject: [PATCH 6/7] Minor tweaks so check packages passes --- R/model_stochastic_seir.R | 10 ++++++---- man/dot-prepare_interventions.Rd | 2 +- tests/testthat/test-model_stochastic_seir.R | 8 ++++++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/R/model_stochastic_seir.R b/R/model_stochastic_seir.R index d6efc608..4c913be2 100644 --- a/R/model_stochastic_seir.R +++ b/R/model_stochastic_seir.R @@ -176,15 +176,17 @@ model_stochastic_seir <- function(population, compartment = rep( compartments, each = n_groups * n_samples ) ) outputs[[ 1 ]] <- data.table::copy( output_template ) + time <- NA # HACK TO PREVENT CHECK PACKAGE COMPLAINING ABOUT + value <- NA # data.table SYNTAX outputs[[ 1 ]][ , time := 0 ] outputs[[ 1 ]][ , value := unlist( lapply( states, as.vector ) )] for( tdx in 1:time_end ) { # calculate the transisiotns between compartments infectious_contacts <- ( ( contact_matrix[[ tdx ]] * rates$transmission_rate[ tdx ] ) %*% states[[ "infectious" ]] ) - flows[[ "SE" ]] <- matrix( rbinom( n_cells, states[[ "susceptible" ]], infectious_contacts ), nrow = n_groups ) - flows[[ "EI" ]] <- matrix( rbinom( n_cells, states[[ "exposed" ]], rates$infectiousness_rate[ tdx ] ), nrow = n_groups ) - flows[[ "IR" ]] <- matrix( rbinom( n_cells, states[[ "infectious" ]], rates$recovery_rate[ tdx ] ), nrow = n_groups ) + flows[[ "SE" ]] <- matrix( stats::rbinom( n_cells, states[[ "susceptible" ]], infectious_contacts ), nrow = n_groups ) + flows[[ "EI" ]] <- matrix( stats::rbinom( n_cells, states[[ "exposed" ]], rates$infectiousness_rate[ tdx ] ), nrow = n_groups ) + flows[[ "IR" ]] <- matrix( stats::rbinom( n_cells, states[[ "infectious" ]], rates$recovery_rate[ tdx ] ), nrow = n_groups ) # update the number in each state states[[ "susceptible" ]] <- states[[ "susceptible" ]] - flows[[ "SE" ]] @@ -209,7 +211,7 @@ model_stochastic_seir <- function(population, #' @param contact_matrix The base contact matrix #' @param rates The base transition rates #' @param time_end The length of the simulation -#' @param interventions The interverntions applied +#' @param interventions The interventions to be applied #' #' @return A list of the contact matrix and transition rates applicable for #' each step of the simulation diff --git a/man/dot-prepare_interventions.Rd b/man/dot-prepare_interventions.Rd index 11b55d14..2b403b00 100644 --- a/man/dot-prepare_interventions.Rd +++ b/man/dot-prepare_interventions.Rd @@ -13,7 +13,7 @@ \item{time_end}{The length of the simulation} -\item{interventions}{The interverntions applied} +\item{interventions}{The interventions to be applied} } \value{ A list of the contact matrix and transition rates applicable for diff --git a/tests/testthat/test-model_stochastic_seir.R b/tests/testthat/test-model_stochastic_seir.R index 6726e8e3..b23a1d0d 100644 --- a/tests/testthat/test-model_stochastic_seir.R +++ b/tests/testthat/test-model_stochastic_seir.R @@ -1,4 +1,8 @@ #### Tests for the stochastic SEIR model #### + +base_seed <- .Random.seed +set.seed(1) + # Prepare contact matrix and demography vector polymod <- socialmixr::polymod contact_data <- socialmixr::contact_matrix( @@ -291,3 +295,7 @@ test_that("Stochastic SEIR model: errors and warnings, scalar arguments", { ) ) }) + +# reset seed not to disturb other tests +.Random.seed <- base_seed + From f7bfd4748391da8b28d2ab92815a0872b044ff30 Mon Sep 17 00:00:00 2001 From: roberthinch Date: Tue, 18 Feb 2025 18:14:42 +0000 Subject: [PATCH 7/7] Add new model to NEWS.md --- NEWS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.md b/NEWS.md index 3cfde3e8..caaec1d2 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,6 +8,10 @@ Maintainer is changing to @rozeggo. 1. Internal model functions for the models which allow vaccination have been corrected to prevent vaccination introducing negative values of susceptibles; tests added to check for this (#235, initially reported by @avallecam). +## Model structures + +1. Added `model_stochastic_seir()` which is a stochastic SEIR model with population structure (i.e. stochastic version of `model_default()` without vaccine compartment) (#260). + ## Helper functions 1. Added the `epidemic_peak()` function to calculate the timing and size of the largest peak in each compartment in an scenario model (#240) by @bahadzie.