
Contributing a long-running example (precompute + cache)
Source:vignettes/precompute-articles.Rmd
precompute-articles.RmdThis article is for contributors. It explains how to add a pkgdown article whose model fits are too slow to run on every website build — a full FOCEi or SAEM estimation, a PBPK model, a delay differential equation — without timing the build out.
The mechanics are a one-time setup chunk plus a single line per fit.
The problem
The articles under vignettes/ are
.Rbuildignored, so they are built only for the
website (pkgdown), never during
R CMD check or on CRAN. That is deliberate: it lets an
article run a real estimation.
But a real estimation is slow. When several articles each re-fit a SAEM or FOCEi model on every site build, the pkgdown runner times out (it exits 143 after its wall-clock limit). So we cache: fit once, commit the result, and load it on every later build.
We do this with the := operator from
the nlmixr2save
package, which saves a fit in a portable format (readable independent of
the nlmixr2/rxode2 version) and reloads it instead of refitting.
The setup chunk
Each article that fits a model starts with:
library(nlmixr2save)
options(nlmixr2save.dir = system.file("cache", package = "nlmixr2"), # where the cache files live
nlmixr2save.prefix = "modelPiping-", # a per-article file prefix
nlmixr2save.check = FALSE) # trust the committed cache-
nlmixr2save.dir = system.file("cache", package = "nlmixr2")points at the cache underinst/cache/(which installs tocache/at the package root).inst/cache/is.Rbuildignored for normalR CMD build/ CRAN tarballs, but the pkgdown workflow removes that line before building the package, sosystem.file()finds it during the site build.precompute.Rpkgload::load_all()s the source package sosystem.file()resolves to the source tree’sinst/cache/while the cache is populated. -
nlmixr2save.prefixnamespaces this article’s files. Use the article’s base name plus a dash ("modelPiping-","wbc-","delays-", …) so files do not collide between articles. -
nlmixr2save.check = FALSEtells:=to trust the committed cache: if the cache file exists it is loaded as-is; it is regenerated only when it is missing (or you clear it). This keeps a committed cache stable across nlmixr2/rxode2 updates — the fits are re-run only when you actually want them re-run.
Caching a fit with :=
Replace a normal assignment fit <- nlmixr2(...) with
fit := nlmixr2(...):
fit := nlmixr2(one.compartment, theo_sd, est = "focei",
control = list(print = 0),
table = list(cwres = TRUE, npde = TRUE))The first time this runs it fits the model and writes
inst/cache/modelPiping-fit.zip; every later build loads
that file and assigns fit without refitting. The variable
name (fit) plus the prefix is the cache file name, so
each := target must be unique within an
article.
A simulation or other non-fit value works the same way and is stored
as an .rds (a fit becomes a portable
.zip):
vpc := vpcSim(fit, n = 300) # inst/cache/modelPiping-vpc.rdsOne model transformation per fit
:= caches a single
nlmixr2()/nlmixr() call. When an example
builds its model by piping, do the (cheap) model transformation first
and give the (slow) fit its own := line:
# build the model -- cheap, not cached
noEta.mod <- fit |> model(ka <- exp(tka))
# fit it -- cached
noEta := nlmixr2(noEta.mod, theo_sd, est = "focei",
control = list(print = 0))Piping a fit through
ini()/model()/update() returns a
model (an rxUi), which carries no data, so pass the data
explicitly to nlmixr2().
Registering, populating, and committing the cache
1. Register the article in precompute.R
— add its file name to the vignettes vector so the
cache-populating script renders it:
vignettes <- c(
"addingCovariances.Rmd",
"delays.Rmd",
"mymodel.Rmd", # <- add here
"modelPiping.Rmd",
# ...
)2. Populate the cache. From the
vignettes/ directory:
precompute.R renders each registered article in its
own fresh R subprocess (building many rxode2 models in
one long-lived session eventually fails) that first
pkgload::load_all()s the source package (so
system.file() resolves to inst/cache/) and
then runs the article’s real code — so the := lines do the
fitting and write the cache files there.
3. Commit the cache alongside the
.Rmd:
4. Add the article to the pkgdown navbar in
_pkgdown.yml.
Refreshing the cache
To force the fits to re-run — after changing a model, data, or
package — clear the cache and re-render.
nlmixr2saveInvalidate() removes every cache entry under the
active prefix/dir:
options(nlmixr2save.dir = system.file("cache", package = "nlmixr2"),
nlmixr2save.prefix = "mymodel-")
nlmixr2saveInvalidate() # clears the mymodel-* cache entriesor clear everything and rebuild all articles:
When a fit needs settings that only matter for the demo
A cached fit is computed once, so it is fine — and often clearer — to
use control settings that make the demonstration fast and
robust rather than production-grade. The DDE article, for example, skips
the standard-error step (covMethod = "") and
common-subexpression optimization (optExpression = FALSE)
because the point is parameter recovery, not precision. Put such choices
in the .Rmd (the source of truth) and note why in
the prose.
Summary
- Articles in
vignettes/build for the website only; they may run real fits. - Set
nlmixr2save.dir/.prefix/.checkonce, then cache every fit withname := nlmixr2(...)(a fit →.zip, a simulation →.rds). - Do model piping in a separate, cheap step; give each fit its own
unique
:=. - Register the article in
precompute.R, run it, and commitinst/cache/<prefix>*. -
precompute.R --clean(ornlmixr2saveInvalidate()) refreshes the cache when a model changes.