individual
| Language | R, with a C++ (Rcpp) core |
| Paradigms | Agent-based, discrete-time, state-and-event |
| Specification style | Construct variables, events, and processes; hand them to a simulation loop |
| Version reviewed | 0.1.19 |
| License | MIT |
| Code | https://github.com/mrc-ide/individual |
| Docs | https://mrc-ide.github.io/individual/ |
| Paper | 10.21105/joss.03539 |
| Downstream | malariasimulation and much of the malariaverse are built on it |
What it’s for
individual is a toolkit for building fast individual-based models in R. It contains no epidemiology at all: no compartments, no diseases, no networks, no transmission. It provides typed per-agent variables backed by bitsets, a scheduled-event mechanism, and a simulation loop, and the modeler writes everything else.
It is here as the minimum: the building blocks an agent-based epidemiological model needs, with nothing above them. Every other agent-based framework here can be read as individual plus some epidemiological vocabulary.
How a model is specified
The full SIR model from the package tutorial:
# State: a categorical variable over the population
health <- CategoricalVariable$new(categories = c("S","I","R"), initial_values = health_states_t0)
# A process: a function of time that queues state updates
infection_process <- function(t) {
I <- health$get_size_of("I")
foi <- beta * I / N
S <- health$get_index_of("S") # a Bitset
S$sample(rate = pexp(q = foi * dt)) # thin it stochastically, in place
health$queue_update(value = "I", index = S)
}
# An event: something scheduled to happen to specific individuals later
recovery_event <- TargetedEvent$new(population_size = N)
recovery_event$add_listener(function(t, target) {
health$queue_update("R", target)
})
recovery_process <- function(t) {
I <- health$get_index_of("I")
already_scheduled <- recovery_event$get_scheduled()
I$and(already_scheduled$not(inplace = TRUE)) # bitset algebra
rec_times <- rgeom(n = I$size(), prob = pexp(q = gamma * dt)) + 1
recovery_event$schedule(target = I, delay = rec_times)
}
# Output
health_render <- Render$new(timesteps = steps)
health_render_process <- categorical_count_renderer_process(health_render, health, health_states)
simulation_loop(
variables = list(health),
events = list(recovery_event),
processes = list(infection_process, recovery_process, health_render_process),
timesteps = steps
)Core abstractions
There are exactly five:
| Concept | individual name |
Notes |
|---|---|---|
| Variable | CategoricalVariable, IntegerVariable, DoubleVariable, RaggedInteger, RaggedDouble |
Typed per-agent state. Categorical variables are stored as bitsets |
| Bitset | Bitset |
A set of individuals, with and, or, not, xor, sample, size, and in-place variants |
| Process | function(t) |
Runs every timestep; reads state, queues updates |
| Event | Event, TargetedEvent with listeners |
Scheduled with a delay; fires on specific individuals |
| Render | Render |
Collects time series output |
There are also ready-made processes (bernoulli_process, fixed_probability_multinomial_process, multi_probability_bernoulli_process, infection_age_process, categorical_count_renderer_process, update_category_listener, reschedule_listener) for the most common patterns.
The two-phase update
This is the most important detail. Processes do not change state directly; they queue updates (health$queue_update(...)), which the loop applies after every process has run. So every process in a step sees the same start-of-step state, and the result does not depend on the order of processes.
individual is the only agent-based framework in the review that does this. Starsim’s Loop, EpiModel’s module.order, and Vivarium’s priorities make the order explicit, and it still matters. Here it does not.
Bitsets as the population algebra
Bitset is the second important idea. Sets of individuals are bitsets, and selecting a group uses and, or, and not:
I <- health$get_index_of("I")
I$and(already_scheduled$not(inplace = TRUE)) # infectious AND NOT already scheduledThen $sample(rate = p) thins the set at random. Together, these cover a remarkable share of what agent-based epidemiological code does, at C++ speed, in short, readable R.
EpiHiper expresses the same operation as sets defined by conditions on the network, and EMOD as Targeting_Config. Here it is a sequence of calls, but the logic is identical.
Time
Unitless integer timesteps. In the tutorial, dt is a number the modeler multiplies rates by, steps = tmax/dt, and transition probabilities are pexp(q = rate * dt).
Using the exponential CDF to convert a rate to a per-step probability is correct, and it is written out in the model each time, as in odin. No dimensional types.
Strengths
- Minimal and complete. Five building blocks, and
malariasimulation, one of the most detailed disease models in the field, is built on them. - Order does not matter, through queued updates. This is the strongest correctness property of any agent-based framework in the review.
- Bitsets for selecting groups: fast, and a direct match for how modelers think about who is eligible.
- Thinning at random (
$sample(rate = p)) to go with selection. TargetedEventwith delays: schedule a future change for specific individuals, a natural way to express dwell times, and what Starsim does withti_*arrays.- Ready-made processes for the common patterns.
- Checkpointing (
save_object_state/restore_object_state) built in. - Fast for R, through its C++ bitset core.
- Deliberately free of disease concepts, which makes it reusable.
Limitations
- No epidemiology. Nothing for disease, networks, transmission, interventions, or demographics. Every model is written from the building blocks, and two models of the same thing may share no structure.
- The model is entirely code. Processes are R functions, so a model cannot be read by software, saved as data, drawn, or converted.
- No support for transmission. The tutorial’s
foi <- beta * I / Nis homogeneous mixing by hand, and contact structure is left to the modeler. - Unitless time, with the
dtconvention documented but not enforced. - Few result tools beyond
Render, which collects numbers a process gives it. - No inference, calibration, or scenario tools.
- Tied to R: the C++ core is not meant as a standalone library.
Implications for the lingua franca
- Adopt queued updates, so that order within a step does not matter. This is
individual’s most valuable contribution, and it addresses a gap in Starsim: if every process sees the start-of-step state and updates apply at the end, order stops being hidden behavior. Chains within a step (“infect, then immediately test the newly infected”) then have to be written as sub-steps, which is a good trade, because the chain becomes visible. - Selecting sets of people is a core operation, and three frameworks agree.
individual’sBitset, EpiHiper’ssets, and EMOD’sTargeting_Configare the same concept. The lingua franca should have a declared set expression over agent state (as EpiHiper does), with bitsets as a natural implementation. - “Select a set, then thin it at random” is the basic agent-based operation.
S$sample(rate = pexp(q = foi * dt))is what nearly every agent-based process comes down to. A building block of this shape (for agents matching this condition, with this probability, do this) would cover most interventions, progressions, and transmission. - Scheduled events are the right way to handle dwell times for agents.
recovery_event$schedule(target = I, delay = rec_times)is the agent-based form of EpiHiper’sdwellTimeand camdl’svia erlang. One declaration (“the dwell time in I is Gamma(α, β)”) should become a scheduled event for agents, Erlang sub-compartments in an ODE, and a memory kernel in an IDE. This is a concrete test of paradigm independence, and worth prototyping early. individualsets the minimum, and the gap above it is the project’s work. Every agent-based framework here adds its own vocabulary to roughly these five building blocks (Starsim’s modules, epiworld’s viruses and tools, EMOD’s campaigns, EpiHiper’s rules). A lingua franca aims to make those differences unnecessary.