Skip to content

Writing and Debugging EpiModel Code

Samuel Jenness edited this page Oct 12, 2026 · 9 revisions

Who this is for: anyone changing model code in EpiModelHIV-p (or EpiModelCOVID). Before you start: a working project from Getting Started with EpiModelHIV.

This page describes how we write and debug EpiModel module code interactively. The examples use EpiModelHIV in a project built from the EpiModelHIV-Template, but the same tools work for EpiModelCOVID and for any model built with EpiModel. If your model runs without errors but produces strange results, continue with Diagnostic of an EpiModel Module after this page.

For general background on debugging in R, see the debugging chapter of Advanced R and the RStudio debugging guide.

Loading your updated module code

Every applied project pairs a project repo (scripts, inputs, and outputs) with a software repo (your branch of EpiModelHIV-p, which holds the modules). Keep both open in separate RStudio windows, each opened from its .Rproj file. You edit modules in the software repo and run them from scripts in the project repo, so after every edit the project's R session has to load the new code. There are two ways to do that.

While you are developing: load_all()

pkgload::load_all() loads a package directly from its source folder without installing it. The Template wraps it in a helper, load_local_EpiModelHIV(), which reads the location of your EpiModelHIV-p clone from the EPIMODELHIV_DIR variable in the project's .Renviron file. To set it up once, run usethis::edit_r_environ("project"), add a line such as EPIMODELHIV_DIR="~/git/EpiModelHIV-p", save the file, and restart R.

The development loop, as in the Template's R/B-model_dev/2-debug_modules.R, is:

  1. Edit a module in your EpiModelHIV-p clone and save the file.
  2. In the project repo, reload the package with load_local_EpiModelHIV().
  3. Re-create the control settings with control_msm().
  4. Run netsim().
load_local_EpiModelHIV()

source("R/shared_variables.R", local = TRUE)
source("R/B-model_dev/z-context.R", local = TRUE)
source("R/netsim_settings.R", local = TRUE)   # creates param and init
est <- readRDS(path_to_est)

control <- control_msm(nsteps = 52, ncores = 1)
sim <- netsim(est, param, init, control)

Step 3 matters. control_msm() stores the module functions themselves (for example control$hivtest.FUN), not their names. A control object created before the reload keeps running the old version of every module, and debugonce() on the reloaded function will not trigger. You do not need to re-create param, init, or the network estimates.

Two things to know about load_all():

  • Always use ncores = 1. With ncores > 1, netsim() runs simulations in separate R workers, and those workers load the installed package, not your development code.
  • It lasts only for the current R session and only on your computer. To run the new code on the HPC, or to share it, install it into the project library as described next.

When a change is ready: install it into the project library

  1. Commit the change in the software repo and push it to your branch on GitHub.
  2. In the project repo, restart R (Session > Restart R, or Cmd+Shift+0 on a Mac, Ctrl+Shift+F10 on Windows).
  3. Install your branch into the project's renv library with renv::install("EpiModel/EpiModelHIV-p@<your-branch>"). The GitHub repository is called EpiModelHIV-p, but the R package it contains is EpiModelHIV. The repository is private, so this needs a GitHub token; see Using renv for Reproducible R Environments.
  4. Restart R and run your scripts from the top, this time with library(EpiModelHIV) instead of load_local_EpiModelHIV().
  5. If everything works, record the new version with renv::snapshot(), then commit and push renv.lock.

The HPC runs whatever version is installed in your local project library: the workflow scripts build renv.lock.hpc from it (see Running Project Workflows on the HPC). Code that you have only loaded with load_all() never reaches the cluster.

When you change the engine, also bump the Version field in EpiModelHIV-p's DESCRIPTION (for example from 3.3.2 to 3.3.2.9000), so that installed builds can be told apart with packageVersion("EpiModelHIV").

Stepping through a module

debugonce()

load_local_EpiModelHIV()
control <- control_msm(nsteps = 52, ncores = 1)
debugonce(hivtest_msm)
sim <- netsim(est, param, init, control)

When netsim() reaches the HIV testing module, R pauses on the first line of hivtest_msm(), opens the function in the editor, and changes the console prompt to Browse[1]>. A toolbar appears above the console with Next (n), Step into function (s), Finish function or loop (f), Continue (c), and Stop (Q). You can click the buttons or type the letters at the prompt.

While paused, you can:

  • run the module one line at a time with Next;
  • print any object by typing its name at the prompt, for example table(race) or summary(hiv.test.last);
  • see the module's local variables in the Environment pane;
  • try new code by typing it at the prompt, or by selecting lines in the editor and running them with Cmd+Enter (Ctrl+Enter on Windows).

Editing the source file while paused does not change the code that is running. When you have a fix, save the file, reload with load_local_EpiModelHIV(), re-create control, and run netsim() again.

debugonce() pauses only the first time the function is called. debug(hivtest_msm) pauses every time it is called, which for a module means every time step, until you run undebug(hivtest_msm).

browser(): pausing at a specific line or time step

To start in the middle of a module, or only when some condition holds, add a call to browser() in the module source. For example, in hivtest_msm(), after test_pos_ids is created:

if (at == 25 && length(test_pos_ids) > 0) browser()

Save the file, reload, re-create control, and run netsim(). R pauses at that line at time step 25 (at is the current time step), and only if someone tested positive. Any condition works. Remove the browser() line before you commit.

options(error = recover): entering a module when it crashes

Our models are stochastic, so an error may appear at step 10 in one run and at step 57 in the next. Instead of stepping through every time step, tell R to pause wherever the error happens:

options(error = recover)
sim <- netsim(est, param, init, control)

When an error occurs, netsim() reports the module and time step, and R lists the calls that were running at that moment. For example, after adding if (at == 4) get_param(dat, "new.param") to the top of hivtest_msm():

A ERROR occured in module 'hivtest.FUN' at step 4
Error in get_param(dat, "new.param") : 
  There is no parameter called `new.param` in the parameter list of the main list object (dat)

Enter a frame number, or 0 to exit

1: netsim(est, param, init, control)
2: Map(netsim_run, dat = dat_list, s = seq_along(dat_list))
3: mapply(FUN = f, ..., SIMPLIFY = FALSE)
4: (function (dat, s = 1) 
{
    while (get_current_timestep(dat) < get_control(
5: netsim_run_nsteps(dat, steps_to_run, s)
6: netsim_run_modules(dat, s)
7: withCallingHandlers(expr = {
    current_mod <- "epimodel.internal"
    dat <
8: modules[[i]](dat, at)
9: mod.hivtest.R#18: get_param(dat, "new.param")

Selection: 

Frames 1 to 7 are EpiModel's simulation loop. The frame that calls the module (here, frame 8, which shows (dat, at) as its arguments) is the module that failed: enter its number to browse inside hivtest_msm() as it was at the moment of the error, with dat, at, and all of the module's local variables available. The last frame is the call that raised the error; when the package is loaded with load_all(), it is labeled with the file and line number (mod.hivtest.R#18). If the error happened inside a helper function that your module calls, the helper gets its own frame after the module's. Enter 0 to leave the menu. When you are done, switch this behavior off with options(error = NULL).

Errors in batch runs and on the HPC

recover needs an interactive session. For runs on the HPC, or with ncores > 1, add two control settings so that a failing simulation leaves evidence behind:

control <- control_msm(
  nsteps = 52 * 10,
  .traceback.on.error = TRUE,
  .dump.frame.on.error = TRUE
)

.traceback.on.error prints the full call stack to the console or job log. .dump.frame.on.error saves the state of every frame of the failing simulation to a file named like dump_20261012_100411_1.rda in the working directory. Copy that file to your computer and explore it with:

library(EpiModelHIV)
load("dump_20261012_100411_1.rda")
debugger(last.dump)

debugger() shows the same frame menu as recover. Dump files contain the whole dat object, so they can be large; delete them when you are done.

Clone this wiki locally