knitr::opts_chunk$set(collapse = TRUE, comment = "#>") has_bus <- exists("rxEventListen", asNamespace("rxode2")) knitr::opts_chunk$set(eval = has_bus)
rxode2::rxSetProgressBar(0)
cat("> **Note:** the installed `rxode2` has no event bus, so the code in", "this vignette is shown but not run. With such an `rxode2`, nlmixr2est", "emits nothing and behaves exactly as before.\n")
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 (in development); once available, use it for a full log
instead of writing your own listener.
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.
| 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() and addNpde() (with the default updateObject = TRUE) rebind
the variable that was passed in. inPlace is TRUE only 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) or
updateObject = FALSE give inPlace = FALSE and name = NULL.addTable() works on a copy by default (updateObject = FALSE), giving
inPlace = FALSE. addTable(updateObject = TRUE) modifies the fit's
shared environment, so inPlace is TRUE whenever the result shares the
original's environment, even for addTable(fits[[1]], updateObject = TRUE)
(where name is then NULL because there is no plain name).setOfv() always changes the fit in place: inPlace = TRUE.inPlace = TRUE,
name = NULL, and original is the fit itself.call fieldrxode2 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).
nlmixr2();
the internal FOCEi refit of addCwres(); the simulations inside vpcSim();
and every internal re-estimation done while a prior is applied.addCwres() on a fit that already has CWRES, addNpde()
on a fit that already has NPDE, or addTable(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.)nlmixr2(model) without data
(which returns the model) and nlmixr2() with no arguments emit nothing.nmObjGetData()) adds them silently: it is not a user update.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.A small recording listener keeps the event names and a few payload fields:
library(nlmixr2est) 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()
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()
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()
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() o1 <- suppressMessages(fit2$objf) o2 <- fit2$objf show()
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() fit2 <- suppressMessages(addCwres(fit2)) show()
Remove the listener when you are done:
rxode2::rxEventUnlisten("demo")
'nlmixr2log' (in development) 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.
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.
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.