Repository: UofUEpiBio/epiworldR
Pull request #105: Improve measles docs and added some tests
Author: login: gvegayon
Created: 2025-05-01T16:09:13Z | Merged: 2025-05-05T21:31:56Z

- Improved docs for the measles model.
- Added a few tests using the measles model to check that the distribution functions for virus and tools work as expected (in combination with a couple of other functions to increas test coverage).
- Modified the output from the `saver` class, and added a note to the print of `epiworld_model` to indicate if a saver is available or not.

----

co-pilot's

This pull request introduces enhancements to the `ModelMeaslesQuarantine` model, including improved parameterization, documentation, and test coverage. It also refines related functionality and output formatting for better usability. Below are the most important changes grouped by theme:

### Enhancements to `ModelMeaslesQuarantine` Model:
* Updated the model to explicitly describe it as a Susceptible-Exposed-Infectious-Hospitalized-Recovered (SEIHR) model, adding details about isolation and quarantine processes. Improved descriptions for parameters like `contact_rate`, `days_undetected`, and `isolation_period` to provide clearer explanations and defaults. [[1]](diffhunk://#diff-1ebfee7146850e422094b76441569da2daea9738256af2e7fbf077120a0a04d9L3-R41) [[2]](diffhunk://#diff-930139830897c57e6e0f75076825f4c4eb4b168ab32ff2c26e2dab3d5cd142d7L32-R33) [[3]](diffhunk://#diff-930139830897c57e6e0f75076825f4c4eb4b168ab32ff2c26e2dab3d5cd142d7L46-R48) [[4]](diffhunk://#diff-930139830897c57e6e0f75076825f4c4eb4b168ab32ff2c26e2dab3d5cd142d7L59-R88)
* Added references to epidemiological studies and public health resources in the documentation to support the model's design and assumptions. [[1]](diffhunk://#diff-1ebfee7146850e422094b76441569da2daea9738256af2e7fbf077120a0a04d9R51-R69) [[2]](diffhunk://#diff-930139830897c57e6e0f75076825f4c4eb4b168ab32ff2c26e2dab3d5cd142d7R113-R126)

### Documentation and Consistency Improvements:
* Improved the documentation for `ModelMeaslesQuarantine` in the `NEWS.md` file and clarified parameter naming for better consistency.

### Testing and Validation:
* Expanded test coverage for the `ModelMeaslesQuarantine` model, including tests for virus distribution and agent states under different scenarios. These tests ensure the correctness of quarantine and isolation logic.

### Output and Logging Enhancements:
* Enhanced the `print.epiworld_model` function to indicate when a model has an attached saver, providing guidance on retrieving results.
* Updated the `print.epiworld_saver` function to include additional details such as the saver folder and ID for better traceability.


Diff:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ca6b31f7..9e0d567e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
 -   repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v3.2.0
+    rev: v5.0.0
     hooks:
     -   id: trailing-whitespace
         exclude: ^(inst/|man/|src/cpp11.cpp|playgroud/|R/cpp11.R)
@@ -16,7 +16,7 @@ repos:
 -   repo: https://github.com/lorenzwalthert/precommit
     # This version of precommit uses R 4.4.1 (renv.lock)
     # See the pre-commit.yaml workflow file for more details.
-    rev: v0.4.3
+    rev: v0.4.3.9009
     hooks:
       - id: style-files
         args:
diff --git a/NEWS.md b/NEWS.md
index aa930af9..b7dcec63 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -4,13 +4,13 @@
 
 * In the `ModelMeaslesQuarantine` model, isolated individuals now have a different parameter for the number of days in isolation. Previously, the same parameter was used for both isolated and quarantined individuals. The new parameter is `isolation_period`.
 
-* The `ModelMeaslesQuarantine` model changed a couple of parameter names to improve consistency.
+* The `ModelMeaslesQuarantine` model changed a couple of parameter names to improve consistency. We also improved the documentation of the model.
 
 * The mixing models were using the wrong sampling scheme for the contact event between agents. The new implementation allows using the regular basic reproductive number calculation for its calibration.
 
 ## Other Changes
 
-* Added more tests to the `ModelMeaslesQuarantine` model.
+* Added more tests to the `ModelMeaslesQuarantine` model, as well as to the `set_distribution*`, `distribute_*`, and `get_agents_states` functions.
 
 
 # epiworldR 0.8.1.0
diff --git a/R/ModelMeaslesQuarantine.R b/R/ModelMeaslesQuarantine.R
index 64bafc14..cb51db7f 100644
--- a/R/ModelMeaslesQuarantine.R
+++ b/R/ModelMeaslesQuarantine.R
@@ -1,35 +1,44 @@
 #' Measles model with quarantine
 #'
-#' The `ModelMeaslesQuarantine` function implements a connected version
-#' of a Measles model with quarantine.
+#' Implements a Susceptible-Exposed-Infectious-Hospitalized-Recovered (SEIHR)
+#' model for Measles within a school. The model includes isolation of
+#' detected cases and optional quarantine of unvaccinated individuals.
 #'
 #' @param n Number of agents in the model.
 #' @param prevalence Initial number of agents with the virus.
-#' @param contact_rate Average number of contacts per step.
+#' @param contact_rate Average number of contacts per step. Default is set to
+#' match the basic reproductive number (R0) of 15 (see details).
 #' @param transmission_rate Probability of transmission.
 #' @param vax_efficacy Probability of vaccine efficacy.
 #' @param vax_improved_recovery Increase in recovery rate due to vaccination.
 #' @param incubation_period Average number of incubation days.
 #' @param prodromal_period Average number of prodromal days.
 #' @param rash_period Average number of rash days.
-#' @param days_undetected Average number of days undetected.
+#' @param days_undetected Average number of days undetected. Detected cases
+#' are moved to isolation and trigger the quarantine process.
 #' @param hospitalization_rate Probability of hospitalization.
 #' @param hospitalization_period Average number of days in hospital.
 #' @param prop_vaccinated Proportion of the population vaccinated.
-#' @param quarantine_period Total duration of quarantine.
+#' @param quarantine_period Number of days an agent is in quarantine.
 #' @param quarantine_willingness Probability of accepting quarantine (
 #' see details).
-#' @param isolation_period Average number of days in isolation.
+#' @param isolation_period Number of days an agent is in isolation.
 #' @details
 #' This model can be described as a SEIHR model with isolation and quarantine.
 #' The infectious state is divided into prodromal and rash phases. Furthermore,
 #' the quarantine state includes exposed, susceptible, prodromal, and recovered
 #' states.
 #'
+#' The model is a perfect mixing model, meaning that all agents are in contact
+#' with each other. The model is designed to simulate the spread of Measles
+#' within a school setting, where the population is assumed to be homogeneous.
+#'
 #' The quarantine process is triggered any time that an agent with rash is
 #' detected. The agent is then isolated and all agents who are unvaccinated are
-#' quarantined. Isolated agents then may be moved out of the isolation in
-#' `isolation_period` days.
+#' quarantined (if willing). Isolated agents then may be moved out of the
+#' isolation in `isolation_period` days. The quarantine willingness parameter
+#' sets the probability of accepting quarantine. If a quarantined agent develops
+#' rash, they are moved to isolation, which triggers a new quarantine process.
 #'
 #' The basic reproductive number in Measles is estimated to be about 15.
 #' By default, the contact rate of the model is set so that the R0 matches
@@ -39,11 +48,25 @@
 #' there is no quarantine process. The same happens with `isolation_period`.
 #' Since the quarantine process is triggered by an isolation, then
 #' `isolation_period = -1` automatically sets `quarantine_period = -1`.
+#'
+#' @references
+#' Jones, Trahern W, and Katherine Baranowski. 2019. "Measles and Mumps: Old
+#' Diseases, New Outbreaks."
+#'
+#' Liu, Fengchen, Wayne T A Enanoria, Jennifer Zipprich, Seth Blumberg,
+#' Kathleen Harriman, Sarah F Ackley, William D Wheaton, Justine L
+#' Allpress, and Travis C Porco. 2015. "The Role of Vaccination Coverage,
+#' Individual Behaviors, and the Public Health Response in the Control of
+#' Measles Epidemics: An Agent-Based Simulation for California." *BMC
+#' Public Health* 15 (1): 447. \doi{10.1186/s12889-015-1766-6}.
+#'
+#' "Measles Disease Plan." 2019. Utah Department of Health and Human
+#' Services. <https://epi.utah.gov/wp-content/uploads/Measles-disease-plan.pdf>.
 #' @export
 #' @family Models
 #' @aliases epiworld_measlesquarantine
 #' @returns
-#' - The `ModelMeaslesQuarantine` function returns a model of class [epiworld_model].
+#' - The `ModelMeaslesQuarantine` function returns a model of classes [epiworld_model] and `epiworld_measlesquarantine`.
 #' @examples
 #' # An in a school with low vaccination
 #' model_measles <- ModelMeaslesQuarantine(
diff --git a/R/make_saver.R b/R/make_saver.R
index fe9109b8..8289ab99 100644
--- a/R/make_saver.R
+++ b/R/make_saver.R
@@ -345,10 +345,10 @@ make_saver <- function(
 print.epiworld_saver <- function(x, ...) {
 
   cat("A saver for -run_multiple-\n")
-  cat("Saves the following:", paste(x$what, sep = ", "), "\n")
-  cat("To file            :", ifelse(x$file_output, "yes", "no"), "\n")
-  if (x$file_output)
-    cat("Saver pattern      :", x$fn)
+  cat("Saves the following :", paste(x$what, sep = ", "), "\n")
+  cat("To file             :", ifelse(x$file_output, "yes", "no"), "\n")
+  cat("Saver folder        :", dirname(x$fn), "\n")
+  cat("Saver id            :", x$id, "\n")
 
   invisible(x)
 
diff --git a/R/model-methods.R b/R/model-methods.R
index 6069c483..45fe5fb4 100644
--- a/R/model-methods.R
+++ b/R/model-methods.R
@@ -178,6 +178,11 @@ run.epiworld_model <- function(model, ndays, seed = NULL) {
 #' @export
 print.epiworld_model <- function(x, ...) {
   print_cpp(x, lite = TRUE)
+
+  if (length(attr(x, "saver"))) {
+    cat("(the model has a saver attached. You can use `run_multiple_get_results`)\n")
+  }
+
   invisible(x)
 }
 
diff --git a/inst/tinytest/test-agents-and-dist-virus-tool.R b/inst/tinytest/test-agents-and-dist-virus-tool.R
new file mode 100644
index 00000000..9bd283d5
--- /dev/null
+++ b/inst/tinytest/test-agents-and-dist-virus-tool.R
@@ -0,0 +1,243 @@
+# Test just this file: tinytest::run_test_file("inst/tinytest/test-agents-and-dist-virus-tool.R")
+
+###############################################################################
+# Test Group 1: ModelMeaslesQuarantine
+###############################################################################
+# First case: Everyone is vaccinated, so only the 
+# index case is infected
+abm <- ModelMeaslesQuarantine(
+  n = 200,
+  prop_vaccinated = 1.0,
+  vax_efficacy = 1.0
+)
+
+# Changing the distribution of the virus
+set_distribution_virus(
+  get_virus(abm, 0),
+  distfun = distribute_virus_set(10L)
+)
+
+run(abm, 100, 200)
+
+states <- get_agents_states(abm)
+expect_true(
+  (states[11] != "Susceptible") && all(states[-11] == "Susceptible")
+)
+
+###############################################################################
+# Second case: Distribute the virus randomly to 5 agents
+set_distribution_virus(
+  get_virus(abm, 0),
+  distfun = distribute_virus_randomly(5L, FALSE)
+)
+
+run_multiple(
+  abm,
+  ndays = 50,
+  nsims = 100,
+  seed = 1122,
+  saver = make_saver("total_hist"),
+  nthreads = 2
+)
+
+ans <- run_multiple_get_results(abm, nthreads = 2)
+ans <- data.table::as.data.table(ans$total_hist)[date == 0]
+ans <- ans[, .(n = sum(counts)), by = .(sim_num, state)][n > 0]
+expect_equal(
+  ans[, table(n, state)],
+  structure(c(100L, 0L, 0L, 100L), dim = c(2L, 2L), dimnames = list(
+    n = c("5", "195"), state = c("Exposed", "Susceptible")), class = "table")
+)
+
+
+###############################################################################
+# Third case: Only agents 24 and 36 are vaccinated:
+
+set_distribution_tool(
+  get_tool(abm, 0),
+  distfun = distribute_tool_to_set(c(25L, 36L))
+)
+
+# Making sure that the agents become infected
+set_param(abm, "Transmission rate", 1.0)
+set_param(abm, "Contact rate", 100)
+
+run(abm, 100, 200)
+states <- get_agents_states(abm)
+
+expect_true(
+  all(states[c(26, 37)] == "Susceptible") &&
+  all(states[-c(26, 37)] == "Recovered")
+)
+
+expect_stdout(print(get_agents(abm)), "more agents ...")
+
+###############################################################################
+# Fourth case: A random set of two agents are vaccinated
+# - At least one susceptible always at the end.
+# - If bad luck, the single case doesn't spread (199 susceptible)
+# - Most of the time two susceptible.
+set_distribution_tool(
+  get_tool(abm, 0),
+  distfun = distribute_tool_randomly(2L, FALSE)
+)
+
+set_distribution_virus(
+  get_virus(abm, 0),
+  distfun = distribute_virus_set(2L)
+)
+
+run_multiple(
+  abm,
+  ndays = 50,
+  nsims = 100,
+  seed = 1122,
+  saver = make_saver("total_hist"),
+  nthreads = 2
+)
+
+ans <- run_multiple_get_results(abm, nthreads = 2)$total_hist |>
+  data.table::as.data.table()
+
+ans <- ans[date == 50 & state == "Susceptible"]
+expect_true(ans[, all(counts %in% c(1, 2, 199))])
+
+
+###############################################################################
+# Test Group 2: ModelSEIRMixing
+###############################################################################
+# First case: Everyone is vaccinated, so only the 
+# index case is infected
+e1 <- entity("Population 1", 300, FALSE)
+e2 <- entity("Population 2", 300, FALSE)
+e3 <- entity("Population 3", 300, FALSE)
+
+contact_matrix <- c(
+  c(1/3, 1/3, 1/3),
+  c(1/3, 1/3, 1/3),
+  c(1/3, 1/3, 1/3)
+) |> as.double() |> matrix(byrow = TRUE, nrow = 3)
+
+abm <- ModelSEIRMixing(
+  name = "Measles",
+  n = 900,
+  prevalence = 1 / 900,
+  contact_rate = 15 / 0.9 / 4,
+  transmission_rate = 0.9,
+  recovery_rate = 1 / 10,
+  incubation_days = 1/12,
+  contact_matrix = contact_matrix
+)
+
+measles_vaccine <- tool(
+  name = "Measles Vaccine",
+  susceptibility_reduction = 1.0,
+  transmission_reduction = 1.0,
+  recovery_enhancer = 1.0,
+  death_reduction = 1.0,
+  prevalence = 1.0,
+  as_proportion = TRUE
+)
+
+set_distribution_tool(
+  tool = measles_vaccine,
+  distfun = distribute_tool_randomly(2L, TRUE)
+)
+
+add_tool(abm, measles_vaccine)
+
+abm |>
+  add_entity(e1) |>
+  add_entity(e2) |>
+  add_entity(e3)
+
+# Changing the distribution of the virus
+set_distribution_virus(
+  get_virus(abm, 0),
+  distfun = distribute_virus_set(10L)
+)
+
+run(abm, 100, 200)
+
+states <- get_agents_states(abm)
+expect_true(
+  (states[11] != "Susceptible") && all(states[-11] == "Susceptible")
+)
+
+###############################################################################
+# Second case: Distribute the virus randomly to 5 agents
+set_distribution_virus(
+  get_virus(abm, 0),
+  distfun = distribute_virus_randomly(5L, FALSE)
+)
+
+run_multiple(
+  abm,
+  ndays = 50,
+  nsims = 100,
+  seed = 1122,
+  saver = make_saver("total_hist"),
+  nthreads = 2
+)
+
+ans <- run_multiple_get_results(abm, nthreads = 2)
+ans <- data.table::as.data.table(ans$total_hist)[date == 0]
+ans <- ans[, .(n = sum(counts)), by = .(sim_num, state)][n > 0]
+
+expect_equal(
+  ans[, table(n, state)],
+  structure(c(100L, 0L, 0L, 100L), dim = c(2L, 2L), dimnames = list(
+    n = c("5", "895"), state = c("Exposed", "Susceptible")), class = "table")
+)
+
+###############################################################################
+# Third case: Only agents 25 and 36 are vaccinated:
+
+set_distribution_tool(
+  get_tool(abm, 0),
+  distfun = distribute_tool_to_set(c(25L, 36L))
+)
+
+# Making sure that the agents become infected
+set_param(abm, "Prob. Transmission", 1.0)
+set_param(abm, "Contact rate", 100)
+
+run(abm, 100, 200)
+states <- get_agents_states(abm)
+
+expect_true(
+  all(states[c(26, 37)] == "Susceptible") &&
+    all(states[-c(26, 37)] == "Recovered")
+)
+
+expect_stdout(print(get_agents(abm)), "more agents ...")
+
+###############################################################################
+# Fourth case: A random set of two agents are vaccinated
+# - At least one susceptible always at the end.
+# - If bad luck, the single case doesn't spread (199 susceptible)
+# - Most of the time two susceptible.
+set_distribution_tool(
+  get_tool(abm, 0),
+  distfun = distribute_tool_randomly(2L, FALSE)
+)
+
+set_distribution_virus(
+  get_virus(abm, 0),
+  distfun = distribute_virus_set(2L)
+)
+
+run_multiple(
+  abm,
+  ndays = 50,
+  nsims = 100,
+  seed = 1122,
+  saver = make_saver("total_hist"),
+  nthreads = 2
+)
+
+ans <- run_multiple_get_results(abm, nthreads = 2)$total_hist |>
+  data.table::as.data.table()
+
+ans <- ans[date == 50 & state == "Susceptible"]
+expect_true(ans[, all(counts %in% c(1, 2, 199))])
diff --git a/man/ModelMeaslesQuarantine.Rd b/man/ModelMeaslesQuarantine.Rd
index 48120890..4fc5e9e0 100644
--- a/man/ModelMeaslesQuarantine.Rd
+++ b/man/ModelMeaslesQuarantine.Rd
@@ -29,7 +29,8 @@ ModelMeaslesQuarantine(
 
 \item{prevalence}{Initial number of agents with the virus.}
 
-\item{contact_rate}{Average number of contacts per step.}
+\item{contact_rate}{Average number of contacts per step. Default is set to
+match the basic reproductive number (R0) of 15 (see details).}
 
 \item{transmission_rate}{Probability of transmission.}
 
@@ -43,7 +44,8 @@ ModelMeaslesQuarantine(
 
 \item{rash_period}{Average number of rash days.}
 
-\item{days_undetected}{Average number of days undetected.}
+\item{days_undetected}{Average number of days undetected. Detected cases
+are moved to isolation and trigger the quarantine process.}
 
 \item{hospitalization_rate}{Probability of hospitalization.}
 
@@ -56,16 +58,17 @@ ModelMeaslesQuarantine(
 \item{quarantine_willingness}{Probability of accepting quarantine (
 see details).}
 
-\item{isolation_period}{Average number of days in isolation.}
+\item{isolation_period}{Total duration of isolation.}
 }
 \value{
 \itemize{
-\item The \code{ModelMeaslesQuarantine} function returns a model of class \link{epiworld_model}.
+\item The \code{ModelMeaslesQuarantine} function returns a model of classes \link{epiworld_model} and \code{epiworld_measlesquarantine}.
 }
 }
 \description{
-The \code{ModelMeaslesQuarantine} function implements a connected version
-of a Measles model with quarantine.
+Implements a Susceptible-Exposed-Infectious-Hospitalized-Recovered (SEIHR)
+model for Measles within a school. The model includes isolation of
+detected cases and optional quarantine of unvaccinated individuals.
 }
 \details{
 This model can be described as a SEIHR model with isolation and quarantine.
@@ -73,10 +76,16 @@ The infectious state is divided into prodromal and rash phases. Furthermore,
 the quarantine state includes exposed, susceptible, prodromal, and recovered
 states.
 
+The model is a perfect mixing model, meaning that all agents are in contact
+with each other. The model is designed to simulate the spread of Measles
+within a school setting, where the population is assumed to be homogeneous.
+
 The quarantine process is triggered any time that an agent with rash is
 detected. The agent is then isolated and all agents who are unvaccinated are
-quarantined. Isolated agents then may be moved out of the isolation in
-\code{isolation_period} days.
+quarantined (if willing). Isolated agents then may be moved out of the
+isolation in \code{isolation_period} days. The quarantine willingness parameter
+sets the probability of accepting quarantine. If a quarantined agent develops
+rash, they are moved to isolation, which triggers a new quarantine process.
 
 The basic reproductive number in Measles is estimated to be about 15.
 By default, the contact rate of the model is set so that the R0 matches
@@ -101,6 +110,20 @@ model_measles
 
 plot(model_measles)
 
+}
+\references{
+Jones, Trahern W, and Katherine Baranowski. 2019. "Measles and Mumps: Old
+Diseases, New Outbreaks."
+
+Liu, Fengchen, Wayne T A Enanoria, Jennifer Zipprich, Seth Blumberg,
+Kathleen Harriman, Sarah F Ackley, William D Wheaton, Justine L
+Allpress, and Travis C Porco. 2015. "The Role of Vaccination Coverage,
+Individual Behaviors, and the Public Health Response in the Control of
+Measles Epidemics: An Agent-Based Simulation for California." \emph{BMC
+Public Health} 15 (1): 447. \doi{10.1186/s12889-015-1766-6}.
+
+"Measles Disease Plan." 2019. Utah Department of Health and Human
+Services. \url{https://epi.utah.gov/wp-content/uploads/Measles-disease-plan.pdf}.
 }
 \seealso{
 epiworld-methods


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request improves the measles model documentation and expands test coverage.  
- Improved the documentation for the ModelMeaslesQuarantine model and clarified parameter naming in NEWS.md.  
- Added tests for virus distribution and agent state functions, and updated output messaging for the saver.

<details>
<summary>Files not reviewed (5)</summary>

* **R/ModelMeaslesQuarantine.R**: Language not supported
* **R/make_saver.R**: Language not supported
* **R/model-methods.R**: Language not supported
* **inst/tinytest/test-agents-and-dist-virus-tool.R**: Language not supported
* **man/ModelMeaslesQuarantine.Rd**: Language not supported
</details>



state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T16:23:40Z
user: login: apulsipher
body: A few things to fix, but otherwise looking good! I love adding tests :)
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-05T14:22:25Z
user: login: apulsipher
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T15:00:39Z
user: login: apulsipher
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T15:03:10Z
user: login: apulsipher
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-05T17:52:07Z
user: login: apulsipher
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T17:53:27Z
user: login: apulsipher
body: lgtm
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-05T21:31:43Z

Review comments:
diff_hunk: @@ -4,13 +4,13 @@
 
 * In the `ModelMeaslesQuarantine` model, isolated individuals now have a different parameter for the number of days in isolation. Previously, the same parameter was used for both isolated and quarantined individuals. The new parameter is `isolation_period`.
 
-* The `ModelMeaslesQuarantine` model changed a couple of parameter names to improve consistency.
+* The `ModelMeaslesQuarantine` model changed a couple of parameter names to improve consistency. We also improved the documentation of the model.
 
 * The mixing models were using the wrong sampling scheme for the contact event between agents. The new implementation allows using the regular basic reproductive number calculation for its calibration.
 
 ## Other Changes
 
-* Added more tests to the `ModelMeaslesQuarantine` model.
+* Added more tests to the `ModelMeaslesQuarantine` model, as well as to the `set_distribution*`, `distribute_*`, and `get_agents_states` functions.
path: NEWS.md
user: login: Copilot
body: [nitpick] Consider providing additional details on what behavior the added tests cover to help users better understand their scope and intent.
```suggestion
* Added more tests to the `ModelMeaslesQuarantine` model to validate the behavior of isolation and quarantine parameters, as well as to ensure consistency in parameter renaming. Tests were also added to the `set_distribution*` and `distribute_*` functions to verify correct distribution of entities among agents, and to the `get_agents_states` function to confirm accurate retrieval of agent states.
```
created_at: 2025-05-01T16:23:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,35 +1,44 @@
 #' Measles model with quarantine
 #'
-#' The `ModelMeaslesQuarantine` function implements a connected version
-#' of a Measles model with quarantine.
+#' Implements a Susceptible-Exposed-Infectious-Hospitalized-Recovered (SEIHR)
+#' model for Measles within a school. The model includes isolation of
+#' detected cases and optional quarantine of unvaccinated individuals.
 #'
 #' @param n Number of agents in the model.
 #' @param prevalence Initial number of agents with the virus.
-#' @param contact_rate Average number of contacts per step.
+#' @param contact_rate Average number of contacts per step. Default is set to
+#' match the basic reproductive number (R0) of 15 (see details).
 #' @param transmission_rate Probability of transmission.
 #' @param vax_efficacy Probability of vaccine efficacy.
 #' @param vax_improved_recovery Increase in recovery rate due to vaccination.
 #' @param incubation_period Average number of incubation days.
 #' @param prodromal_period Average number of prodromal days.
 #' @param rash_period Average number of rash days.
-#' @param days_undetected Average number of days undetected.
+#' @param days_undetected Average number of days undetected. Detected cases
+#' are moved to isolation and trigger the quarantine process.
 #' @param hospitalization_rate Probability of hospitalization.
 #' @param hospitalization_period Average number of days in hospital.
 #' @param prop_vaccinated Proportion of the population vaccinated.
 #' @param quarantine_period Total duration of quarantine.
path: R/ModelMeaslesQuarantine.R
user: login: apulsipher
body: Something to make it clearer that this is a fixed value (not an average, same for all agents)
```suggestion
#' @param quarantine_period Number of days an agent is in quarantine.
```
created_at: 2025-05-05T14:12:45Z
author_association: MEMBER
diff_hunk: @@ -345,10 +345,10 @@ make_saver <- function(
 print.epiworld_saver <- function(x, ...) {
 
   cat("A saver for -run_multiple-\n")
-  cat("Saves the following:", paste(x$what, sep = ", "), "\n")
-  cat("To file            :", ifelse(x$file_output, "yes", "no"), "\n")
-  if (x$file_output)
-    cat("Saver pattern      :", x$fn)
+  cat("Saves the following :", paste(x$what, sep = ", "), "\n")
+  cat("To file             :", ifelse(x$file_output, "yes", "no"), "\n")
+  cat("Saver folder        :", dirname(x$fn), "\n")
+  cat("Saver id            :", x$id, "\n")
path: R/make_saver.R
user: login: apulsipher
body: Thoughts on using `\t` at the beginning of all four lines after `cat("A saver for -run_multiple-\n")` to indent the subsequent text and better indicate that it all stays together? This might be my C brain talking, maybe it is less relevant for R.
created_at: 2025-05-05T14:20:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,102 @@
+
+###############################################################################
+# First case: Everyone is vaccinated, so only the 
+# index case is infected
+abm <- ModelMeaslesQuarantine(
+  n = 200,
+  prop_vaccinated = 1.0,
+  vax_efficacy = 1.0
+)
+
+# Changing the distribution of the virus
+set_distribution_virus(
+  get_virus(abm, 0),
+  distfun = distribute_virus_set(10L)
+)
+
+run(abm, 100, 200)
+
+states <- get_agents_states(abm)
+expect_true(
+  (states[11] != "Susceptible") && all(states[-11] == "Susceptible")
+)
+
+###############################################################################
+# Second case: Distribute the virus randomly to 5 agents
+set_distribution_virus(
+  get_virus(abm, 0),
+  distfun = distribute_virus_randomly(5L, FALSE)
+)
+
+run_multiple(
+  abm,
+  ndays = 50,
+  nsims = 100,
+  seed = 1122,
+  saver = make_saver("total_hist"),
+  nthreads = 2
+)
+
+ans <- run_multiple_get_results(abm, nthreads = 2)
+ans <- data.table::as.data.table(ans$total_hist)[date == 0]
+ans <- ans[, .(n = sum(counts)), by = .(sim_num, state)][n > 0]
+expect_equal(
+  ans[, table(n, state)],
+  structure(c(100L, 0L, 0L, 100L), dim = c(2L, 2L), dimnames = list(
+    n = c("5", "195"), state = c("Exposed", "Susceptible")), class = "table")
+)
+
+
+###############################################################################
+# Third case: Only agents 24 and 36 are vaccinated:
+
+set_distribution_tool(
+  get_tool(abm, 0),
+  distfun = distribute_tool_to_set(c(25L, 36L))
+)
+
+# Making sure that the agents become infected
+set_param(abm, "Transmission rate", 1.0)
+set_param(abm, "Contact rate", 100)
+
+run(abm, 100, 200)
+states <- get_agents_states(abm)
+
+expect_true(
+  all(states[c(26, 37)] == "Susceptible") &&
+  all(states[-c(26, 37)] == "Recovered")
+)
+
+expect_stdout(print(get_agents(abm)), "more agents ...")
+
+###############################################################################
+# Fourth case: A random set of two agents are vaccinated
+# - At least one susceptible always at the end.
+# - If bad luck, the single case doesn't spread (199 susceptible)
+# - Most of the time two susceptible.
+set_distribution_tool(
+  get_tool(abm, 0),
+  distfun = distribute_tool_randomly(1L, FALSE)
+)
+
+set_distribution_virus(
+  get_virus(abm, 0),
+  distfun = distribute_virus_set(2L)
+)
+
+run_multiple(
+  abm,
+  ndays = 50,
+  nsims = 100,
+  seed = 1122,
+  saver = make_saver("total_hist"),
+  nthreads = 2
+)
+
+ans <- run_multiple_get_results(abm, nthreads = 2)$total_hist |>
+  data.table::as.data.table()
+
+ans <- ans[date == 50 & state == "Susceptible"]
+ans[, all(counts %in% c(1, 2, 199))]
path: inst/tinytest/test-agents-and-dist-virus-tool.R
user: login: apulsipher
body: I'm not seeing an `expect_*` statement here. Is there supposed to be one?
created_at: 2025-05-05T14:22:02Z
author_association: MEMBER
diff_hunk: @@ -345,10 +345,10 @@ make_saver <- function(
 print.epiworld_saver <- function(x, ...) {
 
   cat("A saver for -run_multiple-\n")
-  cat("Saves the following:", paste(x$what, sep = ", "), "\n")
-  cat("To file            :", ifelse(x$file_output, "yes", "no"), "\n")
-  if (x$file_output)
-    cat("Saver pattern      :", x$fn)
+  cat("Saves the following :", paste(x$what, sep = ", "), "\n")
+  cat("To file             :", ifelse(x$file_output, "yes", "no"), "\n")
+  cat("Saver folder        :", dirname(x$fn), "\n")
+  cat("Saver id            :", x$id, "\n")
path: R/make_saver.R
user: login: apulsipher
body: make issue and resolve
created_at: 2025-05-05T15:00:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,102 @@
+
+###############################################################################
+# First case: Everyone is vaccinated, so only the 
+# index case is infected
+abm <- ModelMeaslesQuarantine(
+  n = 200,
+  prop_vaccinated = 1.0,
+  vax_efficacy = 1.0
+)
+
+# Changing the distribution of the virus
+set_distribution_virus(
+  get_virus(abm, 0),
+  distfun = distribute_virus_set(10L)
+)
+
+run(abm, 100, 200)
+
+states <- get_agents_states(abm)
+expect_true(
+  (states[11] != "Susceptible") && all(states[-11] == "Susceptible")
+)
+
+###############################################################################
+# Second case: Distribute the virus randomly to 5 agents
+set_distribution_virus(
+  get_virus(abm, 0),
+  distfun = distribute_virus_randomly(5L, FALSE)
+)
+
+run_multiple(
+  abm,
+  ndays = 50,
+  nsims = 100,
+  seed = 1122,
+  saver = make_saver("total_hist"),
+  nthreads = 2
+)
+
+ans <- run_multiple_get_results(abm, nthreads = 2)
+ans <- data.table::as.data.table(ans$total_hist)[date == 0]
+ans <- ans[, .(n = sum(counts)), by = .(sim_num, state)][n > 0]
+expect_equal(
+  ans[, table(n, state)],
+  structure(c(100L, 0L, 0L, 100L), dim = c(2L, 2L), dimnames = list(
+    n = c("5", "195"), state = c("Exposed", "Susceptible")), class = "table")
+)
+
+
+###############################################################################
+# Third case: Only agents 24 and 36 are vaccinated:
+
+set_distribution_tool(
+  get_tool(abm, 0),
+  distfun = distribute_tool_to_set(c(25L, 36L))
+)
+
+# Making sure that the agents become infected
+set_param(abm, "Transmission rate", 1.0)
+set_param(abm, "Contact rate", 100)
+
+run(abm, 100, 200)
+states <- get_agents_states(abm)
+
+expect_true(
+  all(states[c(26, 37)] == "Susceptible") &&
+  all(states[-c(26, 37)] == "Recovered")
+)
+
+expect_stdout(print(get_agents(abm)), "more agents ...")
+
+###############################################################################
+# Fourth case: A random set of two agents are vaccinated
+# - At least one susceptible always at the end.
+# - If bad luck, the single case doesn't spread (199 susceptible)
+# - Most of the time two susceptible.
+set_distribution_tool(
+  get_tool(abm, 0),
+  distfun = distribute_tool_randomly(1L, FALSE)
+)
+
+set_distribution_virus(
+  get_virus(abm, 0),
+  distfun = distribute_virus_set(2L)
+)
+
+run_multiple(
+  abm,
+  ndays = 50,
+  nsims = 100,
+  seed = 1122,
+  saver = make_saver("total_hist"),
+  nthreads = 2
+)
+
+ans <- run_multiple_get_results(abm, nthreads = 2)$total_hist |>
+  data.table::as.data.table()
+
+ans <- ans[date == 50 & state == "Susceptible"]
+ans[, all(counts %in% c(1, 2, 199))]
path: inst/tinytest/test-agents-and-dist-virus-tool.R
user: login: apulsipher
body: ```suggestion
expect_true(ans[, all(counts %in% c(1, 2, 199))])
```
created_at: 2025-05-05T15:03:10Z
author_association: MEMBER
diff_hunk: @@ -1,35 +1,44 @@
 #' Measles model with quarantine
 #'
-#' The `ModelMeaslesQuarantine` function implements a connected version
-#' of a Measles model with quarantine.
+#' Implements a Susceptible-Exposed-Infectious-Hospitalized-Recovered (SEIHR)
+#' model for Measles within a school. The model includes isolation of
+#' detected cases and optional quarantine of unvaccinated individuals.
 #'
 #' @param n Number of agents in the model.
 #' @param prevalence Initial number of agents with the virus.
-#' @param contact_rate Average number of contacts per step.
+#' @param contact_rate Average number of contacts per step. Default is set to
+#' match the basic reproductive number (R0) of 15 (see details).
 #' @param transmission_rate Probability of transmission.
 #' @param vax_efficacy Probability of vaccine efficacy.
 #' @param vax_improved_recovery Increase in recovery rate due to vaccination.
 #' @param incubation_period Average number of incubation days.
 #' @param prodromal_period Average number of prodromal days.
 #' @param rash_period Average number of rash days.
-#' @param days_undetected Average number of days undetected.
+#' @param days_undetected Average number of days undetected. Detected cases
+#' are moved to isolation and trigger the quarantine process.
 #' @param hospitalization_rate Probability of hospitalization.
 #' @param hospitalization_period Average number of days in hospital.
 #' @param prop_vaccinated Proportion of the population vaccinated.
 #' @param quarantine_period Total duration of quarantine.
 #' @param quarantine_willingness Probability of accepting quarantine (
 #' see details).
-#' @param isolation_period Average number of days in isolation.
+#' @param isolation_period Total duration of isolation.
path: R/ModelMeaslesQuarantine.R
user: login: apulsipher
body: ```suggestion
#' @param isolation_period Number of days an agent is in isolation.
```
created_at: 2025-05-05T17:49:18Z
author_association: MEMBER
diff_hunk: @@ -4,13 +4,13 @@
 
 * In the `ModelMeaslesQuarantine` model, isolated individuals now have a different parameter for the number of days in isolation. Previously, the same parameter was used for both isolated and quarantined individuals. The new parameter is `isolation_period`.
 
-* The `ModelMeaslesQuarantine` model changed a couple of parameter names to improve consistency.
+* The `ModelMeaslesQuarantine` model changed a couple of parameter names to improve consistency. We also improved the documentation of the model.
 
 * The mixing models were using the wrong sampling scheme for the contact event between agents. The new implementation allows using the regular basic reproductive number calculation for its calibration.
 
 ## Other Changes
 
-* Added more tests to the `ModelMeaslesQuarantine` model.
+* Added more tests to the `ModelMeaslesQuarantine` model, as well as to the `set_distribution*`, `distribute_*`, and `get_agents_states` functions.
path: NEWS.md
user: login: apulsipher
body: I think this suggestion is overkill, but what do you think @gvegayon ?
created_at: 2025-05-05T17:53:27Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-05-01T16:16:49Z
body: ## [Codecov](https://app.codecov.io/gh/UofUEpiBio/epiworldR/pull/105?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=UofUEpiBio) Report
Attention: Patch coverage is `0%` with `6 lines` in your changes missing coverage. Please review.
> Project coverage is 53.58%. Comparing base [(`afb2662`)](https://app.codecov.io/gh/UofUEpiBio/epiworldR/commit/afb2662f56e5836809474e1e34ce9bed5b8e523c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=UofUEpiBio) to head [(`30e336b`)](https://app.codecov.io/gh/UofUEpiBio/epiworldR/commit/30e336be14462618d7587c8c76e42568cd09aedd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=UofUEpiBio).

| [Files with missing lines](https://app.codecov.io/gh/UofUEpiBio/epiworldR/pull/105?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=UofUEpiBio) | Patch % | Lines |
|---|---|---|
| [R/make\_saver.R](https://app.codecov.io/gh/UofUEpiBio/epiworldR/pull/105?src=pr&el=tree&filepath=R%2Fmake_saver.R&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=UofUEpiBio#diff-Ui9tYWtlX3NhdmVyLlI=) | 0.00% | [4 Missing :warning: ](https://app.codecov.io/gh/UofUEpiBio/epiworldR/pull/105?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=UofUEpiBio) |
| [R/model-methods.R](https://app.codecov.io/gh/UofUEpiBio/epiworldR/pull/105?src=pr&el=tree&filepath=R%2Fmodel-methods.R&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=UofUEpiBio#diff-Ui9tb2RlbC1tZXRob2RzLlI=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/UofUEpiBio/epiworldR/pull/105?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=UofUEpiBio) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #105      +/-   ##
==========================================
+ Coverage   49.44%   53.58%   +4.14%     
==========================================
  Files          40       40              
  Lines        2340     2342       +2     
==========================================
+ Hits         1157     1255      +98     
+ Misses       1183     1087      -96     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/UofUEpiBio/epiworldR/pull/105?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=UofUEpiBio).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=UofUEpiBio).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: gvegayon
created_at: 2025-05-01T16:23:50Z
body: Ready for review, @apulsipher! I added a few new tests to improve coverage. Those were relevant for the work I'm doing with the new mixing model.
author_association: MEMBER
user: login: apulsipher
created_at: 2025-05-05T19:03:53Z
body: @gvegayon I'm hitting a problems with the final test in [`test-agents-and-dist-virus-tool.R`](https://github.com/UofUEpiBio/epiworldR/blob/gvegayon-104-doc-measles/inst/tinytest/test-agents-and-dist-virus-tool.R). During our meeting today we set the last line to:
```
expect_true(ans[, all(counts %in% c(1, 2, 199))])
```
But this test fails, because there's a 0 in `ans`. It would succeed if we wrote it as 

```
expect_true(ans[, all(counts %in% c(0, 1, 199))])
```
Should I make that change?

Interestingly, the same test (`expect_true(ans[, all(counts %in% c(1, 2, 199))])`) succeeds in the mixing model version. I've updated the tests so you can take a look.
author_association: MEMBER