In this vingette, we address how to perform longitudinal sampling. In most applications, data is not taken from a single time point, but rather taken over investigations and multiple years.
We’ll look at how to implement each of these components in SEEPS.
The basic simulation function
gen_transmission_history_exponential_constant generates a
transmission history for a single population. However, the act of
sampling a population removes a set of individuals from a population.
Usually, we assume that sampling an individual would effectively remove
them from the population (from the large-population view). To properly
account for this, we need to use the more detailed API for designing and
implementing the simulation.
The lower level simulation API is concentrated on the
step function. Users seeking to develop new simulation
methods should concentrate on using this function.
The step function takes in a state at integer time
t with a list of parameters, and returns the state at time
t+1. This setup is designed to be comfortable to users
familiar with dynamical systems which often express \(x(t_{n+1}) = f(x(t_n); \theta)\).
The wrap_parameters function is provided to encapsulate
simulation parameters into a named list. Given the list of parameters,
the initialize method generates an initial state
x0 with introduced one active infection.
SEEPS provides two additional functions to help with simulations that
can perform multiple steps. * gen_const_phase performs a
fixed number of steps and returns the final state. *
gen_exp_phase performs steps until the the number of active
cases is at least the minimum_population (a parameter) and
is at least 90% of the maximum_population_target (another
parameter).
Observing the simulation to draw a sample generally removes the
individual from the active population. SEEPS provides the
remove_samples function to perform this operation.
remove_samples accepts a state and a vector of sample
indices to remove from the simulation. A new state is returned with the
samples removed.
We’ll start with a simple example. We’ll simulate a small population with a a target maximum population size of 20 individuals and sample 5 individuals every 12 months, starting when the population is at least 18 individuals (90% of 20), with \(R_0 = 3\).
states_sampled <- list()
samples_all <- list()
samples_per_year <- 5
# Discover each contact with 80% probability
contact_tracing_parameter <- 0.8
sim_parameters <- SEEPS::wrap_parameters(
minimum_population = 6,
offspring_rate_fn = SEEPS::get_biphasic_HIV_rate(params = list("R0" = 3)),
maximum_population_target = 20,
total_steps = 0, # We are going to manually control this so the value will not be used
spike_root = FALSE
)
state <- SEEPS::initialize(sim_parameters) # Setup
state <- SEEPS::gen_exp_phase(state, sim_parameters) # Run until we have at least 90% of the maximum population
# Take a first sample at the end of the exponential phase
sample_taken <- SEEPS::contact_traced_uniform_restarts_ids(
state$active, state$parents, samples_per_year, contact_tracing_parameter
)
# Record the sampling
samples_all[[1]] <- list("curr_step" = state$curr_step, "samples" = sample_taken$samples)
# Remove from the population
state <- SEEPS::remove_samples(state, sample_taken$samples)
# Now simulate forward
for (time_pointer in 1:3) {
state <- SEEPS::gen_const_phase(state, sim_parameters, num_steps = 12)
sample_taken <- SEEPS::contact_traced_uniform_restarts_ids(
state$active, state$parents, samples_per_year, contact_tracing_parameter
)
# Record the sampling
samples_all[[time_pointer + 1]] <- list("curr_step" = state$curr_step, "samples" = sample_taken$samples)
# Remove from the population
state <- SEEPS::remove_samples(state, sample_taken$samples)
}
# Now that we have collected all of the sampling data, we can flatten it into a single list
sample_times_ <- list()
sample_ids <- list()
ptr <- 1
for (i in seq_along(samples_all)) {
sample_times_[[ptr]] <- samples_all[[i]]$curr_step
sample_ids[[ptr]] <- samples_all[[i]]$samples
ptr <- ptr + 1
}
# Convert to
genealogy <- SEEPS::reduce_transmission_history_bpb2(
samples = sample_ids,
parents = state$parents,
current_step = sample_times_
)
# Convert to a phylogeny
phylogeny <- SEEPS::geneology_to_phylogeny_bpb(
transmission_history = genealogy$parents,
infection_times = genealogy$transmission_times,
sample_times = genealogy$sample_times,
a = 5, b = 5,
leaf_sample_ids = genealogy$transformed_sample_indices
)
# Convert to a newick string for plotting
newick_tree_string <- SEEPS::phylogeny_to_newick(phylogeny$phylogeny, mode = "mean", label_mode = "abs")
# Plot the tree
library(ape)
tree <- drop.tip(read.tree(text = newick_tree_string), "")
plot(tree, cex = 0.5)
axis(1, las = 1, col.axis = "black", col.ticks = "black")One can also use this approach to record when every individual is removed from the population. Rather than recording a set of samples, we record whenever an individual is removed from the active population.
We define an auxiliary function for simulations.
gen_transmission_history_exponential_constant_local <- function(minimum_population, # nolint: object_length_linter
offspring_rate_fn, maximum_population_target, total_steps,
spike_root = FALSE) {
# Top level/user function. Users should start with this function.
parameters <- SEEPS::wrap_parameters(
minimum_population, offspring_rate_fn,
maximum_population_target, total_steps,
spike_root
)
state <- SEEPS::initialize(parameters)
SEEPS::validate_state(state) # Check that the state is valid
all_states <- list()
all_states[[1]] <- state
state <- gen_exp_phase(state, parameters, all_states = TRUE)
all_states <- c(all_states, state)
state <- gen_const_phase(all_states[[length(all_states)]], parameters, num_steps = total_steps, all_states = TRUE)
all_states <- c(all_states, state)
return(lapply(all_states, SEEPS::clean_up))
}We’ll use a different rate function, where the rate is 50 times higher in the first 3 months than in the remainder of the infection. We set the expected length of the each infection to 30 months (2.5 years).
# maximum_population_target <- 30
maximum_population_target <- 45
simulator_result <- gen_transmission_history_exponential_constant_local(
minimum_population = 6,
offspring_rate_fn = offspring_rate_fn,
# offspring_rate_fn = SEEPS::get_biphasic_HIV_rate(params = list("R0" = 1.3)),
# offspring_rate_fn = SEEPS::get_biphasic_HIV_rate(params = list("R0" = 1.2)),
maximum_population_target = maximum_population_target,
# total_steps = 48
total_steps = 60
)
removed_list <- list()
sample_times <- list()
birth_list <- list()ptr <- 1
# Don't include the 0 individual as they are not part of the population
# 0 is the root of the first infection.
removed_list[[ptr]] <- c()
sample_times[[ptr]] <- c()
birth_list[[ptr]] <- c(1)
ptr <- 2
for (i in 2:length(simulator_result)) {
# Get the active nodes at i-1 and i
active_i <- simulator_result[[i]]$active
active_i_1 <- simulator_result[[i - 1]]$active
# Compare the active nodes at i-1 and i
# If the active node is in i-1 but not i, add it to a "removed" list at position i
removed_active <- active_i_1[!(active_i_1 %in% active_i)]
new_active <- active_i[!(active_i %in% active_i_1)]
if (length(removed_active) == 0 && length(new_active) == 0) {
next # Nothing to add to the samples list
}
# Add to removed list
removed_list[[ptr]] <- removed_active
birth_list[[ptr]] <- new_active
# Record the sample time
sample_times[[ptr]] <- simulator_result[[i]]$t_end
ptr <- ptr + 1
}
# Final step - get the active nodes at the last step
active_final <- simulator_result[[length(simulator_result)]]$active
# Store it
removed_list[[ptr]] <- active_final
# Record the sample time
sample_times[[ptr]] <- simulator_result[[length(simulator_result)]]$t_end# Now record the transmission history
print(removed_list)
#> [[1]]
#> NULL
#>
#> [[2]]
#> numeric(0)
#>
#> [[3]]
#> numeric(0)
#>
#> [[4]]
#> numeric(0)
#>
#> [[5]]
#> numeric(0)
#>
#> [[6]]
#> numeric(0)
#>
#> [[7]]
#> numeric(0)
#>
#> [[8]]
#> numeric(0)
#>
#> [[9]]
#> numeric(0)
#>
#> [[10]]
#> numeric(0)
#>
#> [[11]]
#> numeric(0)
#>
#> [[12]]
#> numeric(0)
#>
#> [[13]]
#> numeric(0)
#>
#> [[14]]
#> numeric(0)
#>
#> [[15]]
#> numeric(0)
#>
#> [[16]]
#> [1] 8
#>
#> [[17]]
#> [1] 6
#>
#> [[18]]
#> numeric(0)
#>
#> [[19]]
#> [1] 10 19
#>
#> [[20]]
#> [1] 4 17
#>
#> [[21]]
#> numeric(0)
#>
#> [[22]]
#> [1] 14
#>
#> [[23]]
#> [1] 18
#>
#> [[24]]
#> [1] 11 43
#>
#> [[25]]
#> [1] 30
#>
#> [[26]]
#> [1] 20 44
#>
#> [[27]]
#> numeric(0)
#>
#> [[28]]
#> [1] 2 39
#>
#> [[29]]
#> [1] 36 38
#>
#> [[30]]
#> [1] 41 46
#>
#> [[31]]
#> [1] 23
#>
#> [[32]]
#> [1] 1 5 12
#>
#> [[33]]
#> [1] 3 9 29
#>
#> [[34]]
#> [1] 47
#>
#> [[35]]
#> [1] 7 16 22 45
#>
#> [[36]]
#> [1] 13 24 51
#>
#> [[37]]
#> [1] 15 26 28
#>
#> [[38]]
#> [1] 31 40
#>
#> [[39]]
#> [1] 34 48
#>
#> [[40]]
#> [1] 25 32 49
#>
#> [[41]]
#> [1] 21 54
#>
#> [[42]]
#> [1] 35
#>
#> [[43]]
#> [1] 27 52
#>
#> [[44]]
#> [1] 33
#>
#> [[45]]
#> [1] 42 60
#>
#> [[46]]
#> [1] 37
#>
#> [[47]]
#> [1] 56
#>
#> [[48]]
#> numeric(0)
#>
#> [[49]]
#> [1] 57
#>
#> [[50]]
#> [1] 58 62 73
#>
#> [[51]]
#> [1] 64 74
#>
#> [[52]]
#> [1] 50
#>
#> [[53]]
#> numeric(0)
#>
#> [[54]]
#> [1] 69 84 85
#>
#> [[55]]
#> [1] 63 79 82
#>
#> [[56]]
#> numeric(0)
#>
#> [[57]]
#> [1] 65 66 67 75
#>
#> [[58]]
#> [1] 83 90
#>
#> [[59]]
#> [1] 53 61
#>
#> [[60]]
#> [1] 55
#>
#> [[61]]
#> [1] 72 87 89
#>
#> [[62]]
#> [1] 68 77 80 92
#>
#> [[63]]
#> [1] 59
#>
#> [[64]]
#> [1] 76
#>
#> [[65]]
#> numeric(0)
#>
#> [[66]]
#> [1] 88
#>
#> [[67]]
#> [1] 78 91
#>
#> [[68]]
#> [1] 99
#>
#> [[69]]
#> [1] 70 71 109
#>
#> [[70]]
#> numeric(0)
#>
#> [[71]]
#> [1] 81 86 93 94 95 96 97 98 100 101 102 103 104 105 106 107 108 110 111
#> [20] 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
#> [39] 131 132 133 134 135 136
print(sample_times)
#> [[1]]
#> NULL
#>
#> [[2]]
#> [1] 2
#>
#> [[3]]
#> [1] 3
#>
#> [[4]]
#> [1] 4
#>
#> [[5]]
#> [1] 5
#>
#> [[6]]
#> [1] 6
#>
#> [[7]]
#> [1] 7
#>
#> [[8]]
#> [1] 8
#>
#> [[9]]
#> [1] 9
#>
#> [[10]]
#> [1] 10
#>
#> [[11]]
#> [1] 11
#>
#> [[12]]
#> [1] 12
#>
#> [[13]]
#> [1] 13
#>
#> [[14]]
#> [1] 14
#>
#> [[15]]
#> [1] 15
#>
#> [[16]]
#> [1] 20
#>
#> [[17]]
#> [1] 21
#>
#> [[18]]
#> [1] 22
#>
#> [[19]]
#> [1] 23
#>
#> [[20]]
#> [1] 24
#>
#> [[21]]
#> [1] 25
#>
#> [[22]]
#> [1] 26
#>
#> [[23]]
#> [1] 27
#>
#> [[24]]
#> [1] 28
#>
#> [[25]]
#> [1] 29
#>
#> [[26]]
#> [1] 30
#>
#> [[27]]
#> [1] 31
#>
#> [[28]]
#> [1] 32
#>
#> [[29]]
#> [1] 33
#>
#> [[30]]
#> [1] 34
#>
#> [[31]]
#> [1] 35
#>
#> [[32]]
#> [1] 36
#>
#> [[33]]
#> [1] 37
#>
#> [[34]]
#> [1] 38
#>
#> [[35]]
#> [1] 39
#>
#> [[36]]
#> [1] 40
#>
#> [[37]]
#> [1] 41
#>
#> [[38]]
#> [1] 42
#>
#> [[39]]
#> [1] 43
#>
#> [[40]]
#> [1] 44
#>
#> [[41]]
#> [1] 45
#>
#> [[42]]
#> [1] 46
#>
#> [[43]]
#> [1] 47
#>
#> [[44]]
#> [1] 48
#>
#> [[45]]
#> [1] 49
#>
#> [[46]]
#> [1] 50
#>
#> [[47]]
#> [1] 51
#>
#> [[48]]
#> [1] 52
#>
#> [[49]]
#> [1] 53
#>
#> [[50]]
#> [1] 54
#>
#> [[51]]
#> [1] 55
#>
#> [[52]]
#> [1] 56
#>
#> [[53]]
#> [1] 57
#>
#> [[54]]
#> [1] 58
#>
#> [[55]]
#> [1] 59
#>
#> [[56]]
#> [1] 60
#>
#> [[57]]
#> [1] 61
#>
#> [[58]]
#> [1] 62
#>
#> [[59]]
#> [1] 63
#>
#> [[60]]
#> [1] 64
#>
#> [[61]]
#> [1] 65
#>
#> [[62]]
#> [1] 66
#>
#> [[63]]
#> [1] 67
#>
#> [[64]]
#> [1] 68
#>
#> [[65]]
#> [1] 69
#>
#> [[66]]
#> [1] 71
#>
#> [[67]]
#> [1] 72
#>
#> [[68]]
#> [1] 73
#>
#> [[69]]
#> [1] 74
#>
#> [[70]]
#> [1] 75
#>
#> [[71]]
#> [1] 75
full_transmission_history <- reduce_transmission_history_mt(
parents = simulator_result[[length(simulator_result)]]$parents,
samples = removed_list,
current_step = sample_times
)
#> [1] "Samples"
#> [1] 8 6 10 19 4 17 14 18 11 43 30 20 44 2 39 36 38 41
#> [19] 46 23 1 5 12 3 9 29 47 7 16 22 45 13 24 51 15 26
#> [37] 28 31 40 34 48 25 32 49 21 54 35 27 52 33 42 60 37 56
#> [55] 57 58 62 73 64 74 50 69 84 85 63 79 82 65 66 67 75 83
#> [73] 90 53 61 55 72 87 89 68 77 80 92 59 76 88 78 91 99 70
#> [91] 71 109 81 86 93 94 95 96 97 98 100 101 102 103 104 105 106 107
#> [109] 108 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
#> [127] 127 128 129 130 131 132 133 134 135 136
#> [1] "Sample times"
#> [1] 20 21 23 23 24 24 26 27 28 28 29 30 30 32 32 33 33 34 34 35 36 36 36 37 37
#> [26] 37 38 39 39 39 39 40 40 40 41 41 41 42 42 43 43 44 44 44 45 45 46 47 47 48
#> [51] 49 49 50 51 53 54 54 54 55 55 56 58 58 58 59 59 59 61 61 61 61 62 62 63 63
#> [76] 64 65 65 65 66 66 66 66 67 68 71 72 72 73 74 74 74 75 75 75 75 75 75 75 75
#> [101] 75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 75 75
#> [126] 75 75 75 75 75 75 75 75 75 75 75Now we can visualize the entire transmission history.
string <- SEEPS::phylogeny_to_newick(full_transmission_history$geneology, mode = "mean", label_mode = "abs")
tree <- read.tree(text = string)
plot(ladderize(tree))
axis(1, las = 1, col.axis = "black", col.ticks = "black")We can also plot out the number of new infections per time step.
births_ <- lapply(birth_list, length)
deaths <- lapply(removed_list, length)
# births_ <- births[1:length(births) - 1] # Don't report the final step
deaths_ <- deaths[1:length(deaths) - 1] # Don't report the final step
sample_times_ <- sample_times[1:length(sample_times) - 1] # Don't report the final step
deaths_ <- unlist(deaths_)
births_ <- unlist(births_)
print(length(births_))
#> [1] 70
print(length(deaths_))
#> [1] 70
plot(seq_along(births_), births_, col = "black", type = "S") # label="Births"
lines(seq_along(deaths_), deaths_, col = "red", type = "S") # label="Deaths"
legend(39, y = NULL, legend = c("Births", "Deaths"), col = c("black", "red"), lty = 1:2, cex = 0.8)
print(length(births_))
#> [1] 70
print(length(deaths_))
#> [1] 70
deltas <- births_ - deaths_
print(length(deltas))
#> [1] 70
plot(1:length(deltas), cumsum(deltas), ylab = "Number of active infections", xlab = "Time index [steps]", ylim = c(-1, maximum_population_target + 2), type = "S")
# Draw a horizontal line at the maximum population target
abline(h = maximum_population_target, col = "red", lty = 2)