nlmixr2save tells loggers what was assigned:
when fit := nlmixr2(...) finishes, a logger learns that the
result was named fit, whether it came from the cache, and
the hash it was stored under. It does this by reporting to the
rxode2 event bus.
The event bus in one paragraph
rxode2 provides a small publish/subscribe mechanism:
packages call rxode2::rxEventEmit(event, ...) when a
top-level operation finishes, and any function registered with
rxode2::rxEventListen(id, fun) is called as
fun(event, ...) with the payload fields as named arguments.
Events are only delivered from the outermost operation (scope depth 0),
so the fits inside a bootstrap or the solves inside a fit are silent.
See ?rxode2::rxEventListen for the full API
(rxEventScope(), rxEventDepth(),
rxEventSeq(), …).
Other packages emit the events that describe the work
(fitComplete from nlmixr2est::nlmixr2(),
solveComplete from rxode2::rxSolve(), …).
nlmixr2save emits the events that describe the
assignment around that work. Every emitter checks that the bus
exists first, so with an older rxode2 nothing is emitted
and nothing breaks.
Events emitted by nlmixr2save
| Function | Events |
|---|---|
x := value |
assignStart, then assign or
assignAbort
|
saveFit(fit) |
assign (only outside :=, only for a
fit) |
assignStart
Emitted by := before anything is evaluated or loaded, so
a logger can attribute every event that follows (a
fitComplete, a solveComplete) to the name
being assigned.
| Field | Meaning |
|---|---|
name |
The name on the left of := (NULL if it is
not a symbol) |
assign
Emitted by := when it returns successfully, whether the
value was computed or restored from the cache.
| Field | Meaning |
|---|---|
name |
The name assigned (same as in assignStart) |
value |
The value assigned (a fit, a simulation, a data frame, …) |
call |
The right-hand side of :=, normalized by
rxode2 so it never embeds large values |
envir |
The environment the name was assigned in |
cached |
TRUE when the value was restored from disk instead of
computed |
sha |
The hash the value is cached under (NULL in
trusted-cache mode,
options(nlmixr2save.check = FALSE)) |
random |
TRUE when the right-hand side is a random/simulation
function (rxSolve(), vpcSim(), …) |
seed |
For a random result, the random seed captured before it ran;
NULL otherwise |
since |
rxode2::rxEventSeq() just after
assignStart; events delivered after it belong to this
assignment |
since lets a logger collect the events produced by the
right-hand side: every event whose sequence number is greater than
since and that was delivered before this
assign belongs to it.
assignAbort
Emitted by := when the right-hand side (or the saving)
errors. The only field is name. A logger can drop whatever
it collected since the matching assignStart.
assign from saveFit()
A direct saveFit() of an nlmixr2 fit
reports an assign after the fit is written, with the same
fields:
-
nameis thefileargument when given, otherwise the name of the variable passed asfit(saveFit(fit)reports"fit"); -
cached = FALSE,random = FALSE, andsha,seedandsinceareNULL; - there is no
assignStartand noassignAbort: asaveFit()that errors emits nothing.
saveFit() vs :=. When
:= saves a fit it calls saveFit() internally;
that inner saveFit() is silent and only the :=
reports. So every saved fit is reported exactly once: by :=
when it was assigned with :=, otherwise by
saveFit(). saveFit() of anything that is not a
fit (for example a plain list) emits nothing.
What is not emitted
-
Hash evaluations. To build its cache key,
:=evaluates the arguments of the right-hand side (or, fornlmixr2(), the estimation properties). That evaluation runs inside an event scope, so ina := augPred(nlmixr2(model, data, est = "posthoc"))the fit computed only to hash the call is silent; only the fit that produces the value reports afitComplete. -
Cache hits produce no work events. When the value
is restored from disk, the right-hand side is not run, so the sequence
is just
assignStartandassignwithcached = TRUE. -
Anything inside another operation’s scope. A
:=that runs inside a fit, a solve or any other scoped operation emits nothing, like every other event.
Nested :=
A := evaluated while another := is running
its right-hand side (for example inside a function called on the right)
reports normally, and its assign arrives before the outer
one:
assignStart(outer) -> assignStart(inner) -> assign(inner) -> assign(outer)
The outer := saves and restores its own state around the
inner one, so the outer assign still carries its own
sha, and an inner saveFit() inside either one
stays silent.
Listening
A listener is any function(event, ...). This one just
records what it is told:
library(nlmixr2save)
library(nlmixr2est)
events <- new.env()
events$log <- list()
rxode2::rxEventListen("demo", function(event, ...) {
p <- list(...)
events$log[[length(events$log) + 1L]] <- list(event = event, payload = p)
})
seen <- function() {
vapply(events$log, function(e) {
if (is.null(e$payload$name)) e$event else paste0(e$event, "(", e$payload$name, ")")
}, character(1))
}A small model to fit:
one <- function() {
ini({
tka <- log(1.57); tcl <- log(2.72); tv <- log(31.5)
eta.ka ~ 0.6; eta.cl ~ 0.3; eta.v ~ 0.1
add.sd <- 0.7
})
model({
ka <- exp(tka + eta.ka)
cl <- exp(tcl + eta.cl)
v <- exp(tv + eta.v)
linCmt() ~ add(add.sd)
})
}The first := runs the fit. When the installed
nlmixr2est has the event hooks, its
fitComplete arrives between the two
nlmixr2save events (assignStart(fit),
fitComplete, assign(fit)); with an
nlmixr2est without them only the two
nlmixr2save events are seen:
seen()
#> [1] "assignStart(fit)" "fitComplete" "assign(fit)"
a <- events$log[[length(events$log)]]$payload
a$cached
#> [1] FALSE
a$sha
#> [1] "7627ce8c37bb6ce794f615d561390c0dc1bbc31d"
as.call(as.list(a$call)) # the right-hand side of `:=`
#> nlmixr2(one, nlmixr2data::theo_sd, est = "posthoc", control = list(print = 0))Running it again restores the fit from fit.zip; nothing
is fit, so only the assignment is reported:
events$log <- list()
fit := nlmixr2(one, nlmixr2data::theo_sd, est = "posthoc",
control = list(print = 0))
seen()
#> [1] "assignStart(fit)" "assign(fit)"
events$log[[2]]$payload$cached
#> [1] TRUEA simulation is flagged as random and carries the seed it started from:
events$log <- list()
ev <- rxode2::et(amt = 320) |> rxode2::et(0:24)
set.seed(42)
sim := rxode2::rxSolve(fit, ev, nSub = 2)
seen()
#> [1] "assignStart(sim)" "solveComplete" "assign(sim)"
a <- events$log[[length(events$log)]]$payload
a$random
#> [1] TRUE
!is.null(a$seed)
#> [1] TRUE
a$since
#> [1] 6An error on the right-hand side reports assignAbort:
events$log <- list()
try(bad := nlmixr2(one, nlmixr2data::theo_sd, est = "notAnEst"), silent = TRUE)
seen()
#> [1] "assignStart(bad)" "assignAbort(bad)"A direct saveFit() reports one assign named
after the file:
And a nested :=:
events$log <- list()
inner <- function() {
y := data.frame(b = 2)
data.frame(c = 3)
}
outer := inner()
seen()
#> [1] "assignStart(outer)" "assignStart(y)" "assign(y)"
#> [4] "assign(outer)"Remove the listener when done:
rxode2::rxEventUnlisten("demo")Stable fit keys
:= caches a result built on a fit
(sim := rxSolve(fit, ev), a := augPred(fit))
under a hash of its arguments. Serializing a fit to hash it is not
stable: a fit reloaded from its zip in a new session differs byte-wise
from the fresh one (loader fields, delayed bindings, tables that went
through a csv file). So := hashes a fit on its right-hand
side with nlmixr2saveFitKey() instead, and that result is a
cache hit in a new session too. Both keys are lists meant to be hashed,
and neither forces nor writes anything in the fit.
-
nlmixr2saveFitIdentity(fit)says which fit it is: the model (normalized model, error lines and initial estimates table), the estimation method, the fixed effects, theomegamatrix and the data. It is the same for a fit, itssaveFit()/loadFit()reload, and the fit afteraddCwres(). -
nlmixr2saveFitKey(fit)adds everything a downstream result could depend on: the column names, the objective function rows and values, the covariance and its method, the table values, and (withsha = TRUE, the default) the hash:=stored on the fit.
Numbers are rounded to 10 to 12 significant digits so a csv round
trip does not change the keys. What invalidates a := cache
built on a fit is anything that changes
nlmixr2saveFitKey():
| Change to the fit | Identity | Key (invalidates :=) |
|---|---|---|
saveFit() then loadFit(), a new R
session |
same | same |
addCwres(), addNpde(),
addTable() (new columns) |
same | changes |
setOfv() (new objective function row) |
same | changes |
a new covariance (preconditionFit(), …) |
same | changes |
| different estimates, data, model or method | changes | changes |
fit2 <- loadFit("fitCopy")
identical(digest::digest(nlmixr2saveFitIdentity(fit)),
digest::digest(nlmixr2saveFitIdentity(fit2)))
#> [1] TRUE
identical(digest::digest(nlmixr2saveFitKey(fit, sha = FALSE)),
digest::digest(nlmixr2saveFitKey(fit2, sha = FALSE)))
#> [1] TRUELoggers can use nlmixr2saveFitIdentity() to recognize
the same fit across fitComplete, fitUpdate and
assign events, even after it was saved and reloaded.
Full logging
To record all of these events (with the fits, simulations and results
they describe) to a log, use the nlmixr2log package,
which listens to the bus and turns
assignStart/assign into named entries.
