nlmixr2est tells the rxode2 event
bus when a user-facing piece of work finishes: a fit, a simulation,
or an update to an existing fit. This vignette is for people who want to
listen to those events, for example to keep a log of
every fit in a project. The main consumer is the nlmixr2log package; if
you simply want a full log, use that package instead of writing your own
listener.
The event bus in brief
The bus lives in rxode2 (see
?rxode2::rxEventListen for the full API). A listener is a
function function(event, ...): it receives the event name
and the payload fields as named arguments. It is registered with
rxode2::rxEventListen(id, fun) and removed with
rxode2::rxEventUnlisten(id).
Events are delivered only from the outermost
operation. Every emitting function enters a shared operation scope, and
an event is delivered only when no scope is active. So the many
rxSolve() calls, refits and covariance steps done inside
nlmixr2() are silent, and a bootstrap or a covariate search
that runs many fits reports only its own result (from the package that
runs it). Worker processes started inside a scope stay silent too.
A listener that errors gives a warning; it never stops the fit or the other listeners. Events triggered while a listener runs are dropped.
nlmixr2est checks whether the installed rxode2 has the
bus; without it every hook is a no-op.
Events emitted by nlmixr2est
| Function | Event | Notes |
|---|---|---|
nlmixr2() that returns a fit |
fitComplete |
any est, including
nlmixr2(fit, est = ...)
|
nlmixr2() with est = "rxSolve",
"simulate" or "predict"
|
solveComplete |
also reached by simulate(fit)
(kind = "rxSolve") and predict(fit)
|
vpcSim() |
solveComplete |
kind = "vpcSim"; the simulations inside are silent |
augPred() on a fit |
solveComplete |
kind = "augPred" |
addCwres() |
fitUpdate |
what = "cwres" |
addNpde() |
fitUpdate |
what = "npde" |
addTable() |
fitUpdate |
what = "table" |
setOfv() |
fitUpdate |
what = "ofv" |
reading a deferred objective (fit$objf,
fit$AIC, …) |
fitUpdate |
what = "ofv"; only the first read, when it computes the
value |
predict(fit, level = "individual") calls
rxode2::rxSolve() directly, so its
solveComplete comes from rxode2 (with
kind = "rxSolve" and the fit as object).
fitComplete
| Field | Content |
|---|---|
fit |
the new fit (an nlmixr2FitCore object, with its final
timing) |
object |
what was passed to nlmixr2(): a model function, a
rxUi, or a previous fit |
call |
the nlmixr2() call, normalized (see below) |
objName |
the variable name of object when it was a plain name
(e.g. "one.cmt"), otherwise NULL; the pipe
placeholders . and .x give
NULL
|
source |
always "fit" for nlmixr2est |
solveComplete
| Field | Content |
|---|---|
result |
the simulation or prediction (an rxSolve object for
nlmixr2(); the returned value for vpcSim() and
augPred()) |
object |
the fit (or model) it was made from, so the result can be linked to its fit |
call |
the call, normalized |
kind |
the est used by nlmixr2()
("rxSolve", "simulate" or
"predict"), or "vpcSim" or
"augPred"
|
fitUpdate
| Field | Content |
|---|---|
fit |
the updated fit |
original |
the fit that was passed in |
name |
the variable name of the user’s fit when the update is in
place, otherwise NULL
|
what |
"cwres", "npde", "table" or
"ofv"
|
inPlace |
TRUE when the user’s object now holds the result |
inPlace follows what actually happened to the user’s
object:
-
addCwres()andaddNpde()(with the defaultupdateObject = TRUE) rebind the variable that was passed in.inPlaceisTRUEonly when that rebinding happened, which needs a plain variable name, bound in the calling environment to the same fit.addCwres(fits[[1]]),addCwres(env$fit)orupdateObject = FALSEgiveinPlace = FALSEandname = NULL. -
addTable()works on a copy by default (updateObject = FALSE), givinginPlace = FALSE.addTable(updateObject = TRUE)modifies the fit’s shared environment, soinPlaceisTRUEwhenever the result shares the original’s environment, even foraddTable(fits[[1]], updateObject = TRUE)(wherenameis thenNULLbecause there is no plain name). -
setOfv()always changes the fit in place:inPlace = TRUE. - Reading a deferred objective computes it inside the fit:
inPlace = TRUE,name = NULL, andoriginalis the fit itself.
The call field
rxode2 normalizes every call field before
delivery so a recorded call never embeds a large object: the function
name is restored (e.g. nlmixr2 or vpcSim), and
arguments that are values rather than code (for example a data frame
inlined by do.call()) become `<value>`
(or a single `<...>` marker when there are many).
What is not emitted
-
Nested work. The fits, solves and covariance steps
inside
nlmixr2(); the internal FOCEi refit ofaddCwres(); the simulations insidevpcSim(); and every internal re-estimation done while a prior is applied. -
No-op updates. An update whose result is identical
to the fit passed in emits nothing:
addCwres()on a fit that already has CWRES,addNpde()on a fit that already has NPDE, oraddTable(updateObject = TRUE)that recomputes the same table. A second read of an objective that is already computed emits nothing either. (setOfv()is the exception: it always emits, since it changes the fit in place and returns the same object.) -
Results that are not fits or solves.
nlmixr2(model)without data (which returns the model) andnlmixr2()with no arguments emit nothing. - Errors. A function that stops with an error emits nothing.
-
Accessors. Asking a fit for data that needs CWRES
(through
nmObjGetData()) adds them silently: it is not a user update. - A fit passed inline, as in
nlmixr2(nlmixr2(one.cmt, data, "focei"), est = "posthoc"), is evaluated before the outer call starts, so it is reported on its own, followed by the outer fit.
Listening
A small recording listener keeps the event names and a few payload fields:
library(nlmixr2est)
#> Loading required package: nlmixr2data
events <- list()
rxode2::rxEventListen("demo", function(event, ...) {
p <- list(...)
fields <- p[c("kind", "what", "objName", "name", "inPlace")]
fields <- fields[!vapply(fields, is.null, logical(1))]
events[[length(events) + 1L]] <<- paste0(
event, "(",
paste(names(fields), vapply(fields, deparse, character(1)),
sep = " = ", collapse = ", "),
")"
)
})
show <- function() {
if (length(events) == 0L) cat("(no events)\n") else writeLines(unlist(events))
events <<- list()
}A small model fit with posthoc:
one.cmt <- 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)
})
}
fit <- suppressMessages(
nlmixr2(one.cmt, nlmixr2data::theo_sd, est = "posthoc")
)
show()
#> fitComplete(objName = "one.cmt")Only one fitComplete is delivered, although the fit
solved the model many times. Simulations and predictions from the fit
each give one solveComplete:
s <- suppressMessages(simulate(fit))
p <- suppressMessages(predict(fit, nlmixr2data::theo_sd))
v <- suppressMessages(vpcSim(fit, n = 3))
show()
#> solveComplete(kind = "rxSolve")
#> solveComplete(kind = "predict")
#> solveComplete(kind = "vpcSim")Updates report whether the user’s object now holds the result.
addTable() works on a copy by default, while
updateObject = TRUE changes the fit’s shared environment.
The second call below recomputes the table the fit already has, so its
result is identical to its input and nothing is emitted; the third adds
NPDE in place:
f2 <- suppressMessages(addTable(fit))
fit <- suppressMessages(addTable(fit, updateObject = TRUE))
fit <- suppressMessages(addTable(fit, updateObject = TRUE,
table = tableControl(npde = TRUE)))
show()
#> fitUpdate(what = "table", inPlace = FALSE)
#> fitUpdate(what = "table", name = "fit", inPlace = TRUE)A saem fit defers its objective function. The first read
computes it inside the fit and emits a fitUpdate; later
reads do not:
fit2 <- suppressMessages(nlmixr2(one.cmt, nlmixr2data::theo_sd, est = "saem",
control = saemControl(print = 0)))
show()
#> fitComplete(objName = "one.cmt")
o1 <- suppressMessages(fit2$objf)
o2 <- fit2$objf
show()
#> fitUpdate(what = "ofv", inPlace = TRUE)addCwres() rebinds the variable it was given. Calling it
again has nothing to do, so the second call emits nothing:
fit2 <- suppressMessages(addCwres(fit2))
show()
#> fitUpdate(what = "cwres", name = "fit2", inPlace = TRUE)
fit2 <- suppressMessages(addCwres(fit2))
show()
#> (no events)Remove the listener when you are done:
rxode2::rxEventUnlisten("demo")Full logging
nlmixr2log listens to these events (and to those of the other nlmixr2 packages) and keeps a project log of fits, simulations, updates and saved results, so you rarely need to write a listener yourself.
For developers: nlmixrUpdateObject()
nlmixrUpdateObject(fit, objName, envir, origFitEnv)
rebinds the variable objName in envir to the
updated fit when that variable still holds the original
fit. It now returns TRUE (invisibly) when it rebound the
variable and FALSE otherwise, for instance when
objName is not a single name (as for
addCwres(fits[[1]]), where it used to error) or the
variable is not bound to the original fit. A package that writes its own
updating function can use this value to set inPlace on the
fitUpdate event it emits.
