Skip to content

Commit 28b6cec

Browse files
authored
Merge pull request #447 from UCD-SERG/sim-pop-multi-no-skip
try to not skip
2 parents 12abd38 + 5c2dd46 commit 28b6cec

7 files changed

Lines changed: 15250 additions & 15210 deletions

File tree

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Type: Package
22
Package: serocalculator
33
Title: Estimating Infection Rates from Serological Data
4-
Version: 1.4.0.9015
4+
Version: 1.4.0.9016
55
Authors@R: c(
66
person("Kristina", "Lai", , "kwlai@ucdavis.edu", role = c("aut", "cre")),
77
person("Chris", "Orwa", role = "aut"),

NEWS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616

1717
## Bug fixes
1818

19+
* `sim_pop_data()` and `sim_pop_data_multi()` now produce identical results
20+
across operating systems. Simulated inter-infection times are now rounded to
21+
whole days, so the number of random draws consumed no longer depends on
22+
platform-specific floating-point results of `log()` (which previously
23+
shifted the random-number stream out of sync and made simulated values, and
24+
their snapshots, differ between macOS, Windows, and Linux). Simulated
25+
values change slightly as a result of this fix. (#447)
1926
* Removed lingering terminology discrepancies
2027
* `load_noise_params()` and `load_sr_params()` now fail gracefully with informative messages when internet resources are unavailable, complying with CRAN policy (#505)
2128
* Added Version Crosswalk article to pkgdown website to help users migrate code from v1.3.0 to v1.4.0

R/quantize_t_next.R

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#' Quantize an inter-infection time to whole days
2+
#'
3+
#' Rounds a continuous inter-infection time (in days) to the nearest whole
4+
#' day, with a floor of one day. The antibody-kinetics simulation in
5+
#' `simresp.tinf()` already operates on a daily grid, but the number of
6+
#' `while()`-loop iterations (and hence the number of random draws consumed)
7+
#' otherwise depends on the platform-specific floating-point result of
8+
#' `log()`. Quantizing to whole days keeps random-number consumption
9+
#' identical across operating systems, so simulated data and its snapshots
10+
#' are reproducible on macOS, Windows, and Linux.
11+
#'
12+
#' The floor of one day prevents a zero-length step, which would otherwise
13+
#' stall the `while()` loop in `simresp.tinf()`.
14+
#'
15+
#' @param t_next a positive [numeric] scalar inter-infection time, in days
16+
#' @returns a whole-number-valued [numeric] scalar, at least 1
17+
#' @noRd
18+
quantize_t_next <- function(t_next) {
19+
max(1, round(t_next))
20+
}

R/simresp.tinf.R

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ simresp.tinf <- function(# nolint: object_name_linter
4242
mcsize <- dim(predpar)[3]
4343
nmc <- n_mcmc_samples
4444

45-
day2yr <- 365.25
45+
days_per_year <- 365.25
4646

4747
if (n_mcmc_samples == 0) {
4848
nmc <- sample.int(n = mcsize, size = 1)
@@ -59,8 +59,19 @@ simresp.tinf <- function(# nolint: object_name_linter
5959
t_inf <- c()
6060

6161
t_next <- -log(runif(1, 0, 1)) / lambda # time to first infection...
62-
63-
age <- if_else(!is.na(age_fixed), age_fixed, t_next / day2yr)
62+
# Quantize inter-infection times to whole days. The simulation already
63+
# operates on a daily grid (`seq(0, t_next, by = 1)`, and ages are sampled
64+
# over integer days), but the `while()` loop boundary and the per-episode
65+
# grid lengths otherwise depend on the platform-specific floating-point
66+
# result of `log()`. Those ULP-level differences flip the number of loop
67+
# iterations on some operating systems, which changes how many random
68+
# draws are consumed and desynchronizes the (otherwise platform-independent)
69+
# L'Ecuyer-CMRG stream -- making snapshot output irreproducible across
70+
# macOS/Windows/Linux. Rounding to whole days keeps RNG consumption
71+
# identical across platforms. See `quantize_t_next()`.
72+
t_next <- quantize_t_next(t_next)
73+
74+
age <- if_else(!is.na(age_fixed), age_fixed, t_next / days_per_year)
6475

6576
mcpar <- ldpar(
6677
age = age,
@@ -116,7 +127,7 @@ simresp.tinf <- function(# nolint: object_name_linter
116127

117128
if (!renew_params) {
118129
par_now <- ldpar(
119-
if (!is.na(age_fixed)) age_fixed else t0 / day2yr,
130+
if (!is.na(age_fixed)) age_fixed else t0 / days_per_year,
120131
antigen_isos,
121132
nmc,
122133
predpar = predpar, ...
@@ -129,6 +140,10 @@ simresp.tinf <- function(# nolint: object_name_linter
129140
# note: the renew_params == TRUE case is handled many lines below
130141

131142
t_next <- -log(runif(1, 0, 1)) / lambda
143+
# Quantize to whole days for cross-platform reproducibility (see the
144+
# comment on the first-infection draw above). `t_end` and `t0` are whole
145+
# days, so the boundary clamp below stays integer-valued too.
146+
t_next <- quantize_t_next(t_next)
132147
if (t0 <= t_end && t0 + t_next > t_end) {
133148
t_next <- t_end - t0
134149
}
@@ -150,7 +165,7 @@ simresp.tinf <- function(# nolint: object_name_linter
150165
b <- rbind(b, b_now)
151166
y_mat <- rbind(y_mat, y_now)
152167

153-
y_end <- y_mat %>% tail(1)
168+
y_end <- y_mat |> tail(1)
154169

155170
if (renew_params) {
156171
if (n_mcmc_samples == 0) {
@@ -163,7 +178,7 @@ simresp.tinf <- function(# nolint: object_name_linter
163178
age <- if_else(
164179
!is.na(age_fixed),
165180
age_fixed,
166-
(t0 + t_next) / day2yr
181+
(t0 + t_next) / days_per_year
167182
)
168183

169184
par_now <- ldpar(

0 commit comments

Comments
 (0)