Extending the Library
Custom Agent Behaviors¶
In epiworld, agents are the fundamental units of simulation. Each Agent<TSeq> represents an individual in the modeled population, and each agent’s state determines its behavior at a given time step. The system is designed so that the logic controlling what an agent does—how it becomes infected, recovers, or interacts with others—is completely user-definable. This flexibility makes it possible to implement models ranging from classical SIR structures to highly customized behavioral or biological mechanisms.
Custom agent behavior is defined through state update functions, which are registered using Model<TSeq>::add_state(). Each state (for example, Susceptible, Infected, Recovered) is associated with a specific function that implements the transition logic for agents currently in that state. These functions must match the signature:
template<typename TSeq>
void custom_update(epiworld::Agent<TSeq>* agent, epiworld::Model<TSeq>* model);
Within this function, the agent pointer can be used to inspect or modify the agent’s state and attributes. The model pointer gives access to global simulation parameters, random number generators, or the full population. This allows for complex interactions between an agent and its neighbors or environment.
A basic example is creating a new “Isolated” state, where agents skip infection checks and have reduced contact activity:
template<typename TSeq>
void update_isolated(Agent<TSeq>* agent, Model<TSeq>* model) {
/* Skip infection spread, but possibly leave isolation. */
if (model->runif() < 0.05)
agent->change_status(model->get_state_id("Susceptible"));
}
You would then register this state:
Each day, every agent calls the function associated with its current state exactly once. The Agent class exposes methods such as:
get_status()andchange_status(new_state)add_virus()andrm_virus()get_neighbors()to access linked agents- and
get_model()to retrieve the owning model.
These make it straightforward to implement infection dynamics, behavior changes, or adaptive actions (e.g., adopting a tool, changing contact patterns).
Custom behaviors can also interact with tools or viruses to model policy or health effects. For instance, an agent’s infection probability might depend on whether it carries a specific tool, like vaccination status. Users can extend this logic arbitrarily to encode complex biological or social behavior. Because the entire mechanism is based on function pointers, it’s easy to mix built-in and user-defined update functions in a single model. The engine manages function dispatch and queuing automatically.
Custom Network Structures¶
-
template<typename TSeq>
void custom_rewire(std::vector<epiworld::Agent<TSeq>>* agents,
epiworld::Model<TSeq>* model,
epiworld_double proportion);
Inside this function, users can delete, swap, or create links based on model conditions (e.g., infection status, behavioral changes, or policy interventions). For instance:
template<typename TSeq>
void quarantine_rewire(std::vector<epiworld::Agent<TSeq>>* agents,
epiworld::Model<TSeq>* model,
epiworld_double proportion) {
for (auto &agent : *agents) {
if (agent.get_status() == model->get_state_id("Infected")) {
agent.clear_neighbors();
}
}
}
You can then attach it to your model:
When model.rewire() is called (either manually or as part of a daily update), the provided function executes, potentially modifying the contact structure for that timestep. This mechanism allows for modeling adaptive behaviors like social distancing, network fragmentation under containment policies, or contact restoration after recovery.
Because all of these mechanisms operate through the same Model abstraction, users can easily combine static and dynamic structural definitions. The system imposes no constraints on how edges are stored beyond ensuring that the network is represented as a set of agent-level connections. This means users can, for example, initialize a small-world network and then rewire a fixed percentage of edges each day according to a behavioral rule, or dynamically switch between directed and undirected configurations if required by their modeling assumptions. The same model can thus represent anything from simple compartmental interactions to fully heterogeneous, temporally varying social graphs.
Implementing New Global Events¶
Global events represent actions that occur at the level of the entire simulation, rather than being tied to individual agents. They are executed either on specific dates or repeatedly at the end of every day, depending on how they are configured. The intent behind this system is to provide a clean and flexible way to modify the model state during runtime, such as changing parameters, adding or removing viruses, deploying tools, or otherwise altering the environment in which agents operate. This allows users to model interventions, seasonal effects, or policy implementations without having to modify agent-level logic directly.
A global event is defined as a function matching the signature void fun(Model<TSeq>*). Within this function, the user has access to the entire model object, and can therefore manipulate global variables, change parameters, modify the population, or trigger any built-in methods. For instance, a simple "lockdown" event might reduce the transmission rate parameter or rewire the network to lower contact density. To register such an event, the user calls add_globalevent(fun, name, date), where fun is the function to execute, name is a descriptive label, and date determines when it will be triggered. If the date is a nonnegative integer, the event will be called at the end of that specific day; if it is negative (typically -1), it will be called after every simulation step.
template<typename TSeq>
void lockdown_event(epiworld::Model<TSeq>* model) {
double tr = model->get_param("Transmission rate");
model->set_param("Transmission rate", tr * 0.5);
}
model.add_globalevent(lockdown_event<TSeq>, "Lockdown Policy", 30);
Once registered, the event becomes part of the model’s internal schedule and will automatically execute at the designated time. The model also allows retrieval and management of global events through get_globalevent() and rm_globalevent() functions, which can be used to inspect or remove events dynamically.
Global events can also interact with the random number generator, the database, and other components of the model. For example, an event could sample a subset of agents and assign them a new tool, or it could introduce a second virus strain midway through the simulation.
template<typename TSeq>
void new_variant_event(epiworld::Model<TSeq>* model) {
epiworld::Virus<TSeq> variant("Variant B", 0.01, true);
variant.set_prob_infecting(&model->operator()("Transmission rate"));
model->add_virus(variant);
}
Because global events execute in the same runtime context as the model itself, they can modify any aspect of it—including parameters, the population vector, the event queue, and even the set of available tools or viruses. This flexibility allows epiworld to support sophisticated intervention-driven or phased modeling workflows that go beyond static parameterization.
Creating Custom Data Collection Strategies¶
Data collection in epiworld is handled primarily through the DataBase<TSeq> component, which records simulation results such as infection counts, transmission histories, and per-agent or per-virus summaries. The built-in mechanisms already support a range of outputs—total histories, virus-specific and tool-specific information, transmission trees, and more—but users can also extend or replace these mechanisms to collect custom data according to their own needs. Custom data collection strategies are useful for recording specialized statistics, debugging model behavior, or integrating the simulation with external analysis workflows.
Each Model<TSeq> object owns a DataBase<TSeq> instance accessible via get_db(). By default, the model writes data to this database automatically as the simulation progresses. However, users can modify or extend this behavior by interacting directly with the database object or by defining custom recorders. The database keeps track of simulation-level quantities and agent-level states through standardized internal tables, which can be written to disk using model.write_data() after a run. The output can include files for total counts over time, transmission histories, transition matrices, and per-virus or per-tool data, depending on which options are enabled.
For models requiring custom tracking, users can either subclass DataBase<TSeq> or define their own collection logic via the Model's hooks. A simple approach is to register a global event that records quantities of interest each day—for instance, the proportion of vaccinated agents, or the number of currently infected nodes with a particular attribute. The event function can append results to an external data structure or to the user_data table within the model. The Model class provides convenience methods such as set_user_data(), add_user_data(), and get_user_data() to store arbitrary numeric variables associated with each timestep. This system is lightweight but flexible enough for most purposes.
template<typename TSeq>
void record_daily_prevalence(epiworld::Model<TSeq>* model) {
int infected = 0;
for (auto &agent : model->get_agents()) {
if (agent.get_status() == model->get_state_id("Infected"))
infected++;
}
model->add_user_data(0, (double)infected / model->size());
}
model.set_user_data({"Prevalence"});
model.add_globalevent(record_daily_prevalence<TSeq>, "Daily prevalence", -1);
For more advanced data collection, users can directly access the model’s internals through the get_agents(), get_entities(), and get_viruses() functions to query agent states, infection statuses, or tool distributions at any point during the simulation. Because epiworld maintains all these objects in contiguous containers, iterating over them to compute aggregate statistics is efficient. Users can combine these features to implement real-time monitoring or adaptive experiments where results from one run inform modifications to subsequent runs.
The make_save_run() utility function offers another extension point. It returns a callable that can be used in conjunction with run_multiple() to define how output is written after each replicate. For example:
auto custom_saver = [](size_t i, epiworld::Model<>* model) {
model->write_data(
"virus_info.csv",
"virus_hist.csv",
"tool_info.csv",
"tool_hist.csv",
"total_hist.csv",
"transmission.csv",
"transition.csv",
"rnum.csv",
"generation.csv"
);
};
model.run_multiple(100, 5, 123, custom_saver);
By providing a custom function of type std::function<void(size_t, Model<TSeq>*)>, users can collect or export data in any desired format—CSV, binary, or in-memory. This design encourages users to treat data collection as part of the modeling logic rather than as a post-processing step.
Overall, custom data collection in epiworld is best thought of as an open system rather than a closed reporting interface. The built-in database handles common epidemiological summaries automatically, but because the Model exposes its entire internal state, users can track virtually anything they need without modifying the core simulation engine, from degree distributions to conditional infection probabilities.