Introduction
rxode2 provides a small event bus: packages
emit an event when a user-facing operation finishes, and any
package can listen for it with
rxode2::rxEventListen(). The main consumer is nlmixr2log, which
records every fit, simulation and result as a run, but a listener can be
any function. See ?rxode2::rxEventListen for the bus
itself.
The key rule of the bus is that events are only delivered from the
outermost operation. Every emitting function enters a
shared operation scope, and an event is dropped when it is emitted
inside one. That matters a great deal for nlmixr2extra: its
drivers (bootstrapFit(), multistart(),
covarSearchAuto(), …) run tens or hundreds of
nlmixr2() fits and rxSolve() calls internally.
Each driver enters the scope, so its internal fits and solves are
silent, and on the way out it emits one event that
describes the whole call. A logger then stores one entry per driver call
instead of one run per internal fit.
Everything here is a no-op when the installed rxode2 has
no event bus, and nothing is emitted when no listener is registered.
Which event a driver emits
On exit, a driver looks at what it returned and picks one event:
- the input fit itself, changed in place, gives
fitUpdate– a new version of that fit’s run; - a different fit gives
fitCompletewithsourceset to the driver’s kind – a new run linked to the input fit (object); - anything else (a data frame, a list of results, a plot) gives one
fitResultcarrying a fit-free summary of the result.
When a result holds a best fit (multistart()’s
$best), that fit is also announced with
fitComplete, linked to the input fit, so it can be kept as
its own run.
| Driver | Event(s) |
what / source /
kind
|
|---|---|---|
bootstrapFit() (default
returnType = "model") |
fitUpdate |
bootstrap |
bootstrapFit(returnType = "fitList"/"modelList") |
fitResult |
bootstrap |
preconditionFit() |
fitUpdate (always) |
covariance |
multistart(fit) |
fitResult, then fitComplete
for $best
|
multistart |
multistart(model, data) |
fitComplete for $best, then
fitResult
|
multistart |
covarSearchAuto() |
fitResult |
covarSearch |
resSearch() |
fitResult |
resSearch |
iivSearch() |
fitResult |
iivSearch |
rerunTopN() |
nothing (refits are silent) | – |
linearize() |
fitComplete |
linearize |
profile() (fit method) |
fitResult |
profile |
profileFixed() |
fitResult |
profile |
profileLlp() |
fitResult |
profile |
lassoCoefficients() |
fitResult |
lasso |
adaptivelassoCoefficients() |
fitResult |
lasso |
adjustedlassoCoefficients() |
fitResult |
lasso |
regularmodel() |
fitComplete |
lasso |
bootplot() |
fitResult (the plot) |
bootplot |
The kind is stored in what for fitUpdate,
in source for fitComplete and in
kind for fitResult.
Payloads
Listeners are called as fun(event, ...): the event name,
then the payload fields as named arguments.
fitUpdate
| Field | Value |
|---|---|
fit |
the input fit, as changed by the driver |
original |
the same fit (it was changed in place) |
name |
NULL |
what |
the driver’s kind, e.g. "bootstrap" or
"covariance"
|
inPlace |
TRUE |
call |
the driver call |
fitComplete
| Field | Value |
|---|---|
fit |
the new fit (linearize(),
regularmodel(), or a result’s best fit) |
object |
the input fit it came from; NULL when the
driver started from a model |
call |
the driver call |
objName |
NULL |
source |
the driver’s kind, e.g. "linearize"
|
fitResult
| Field | Value |
|---|---|
fit |
the input fit (or the best fit when the driver started from a model) |
result |
a fit-free summary of what the driver returned (see below) |
kind |
the driver’s kind, e.g. "profile"
|
call |
the driver call |
The result is built so that a logger can store it
cheaply:
- every fit inside the result is replaced by
list(OBJF = <objective function>, parFixedDf = <parameter table>), read from the fit without computing anything; - data frames and
ggplotobjects are kept as they are; - functions and environments are dropped;
- lists are summarized up to four levels deep, and the original class
of a classed list is kept in the
"nlmixr2extraClass"attribute.
The call of every event is normalized by
rxode2: its head is the driver’s name, and large values
inlined in it are replaced by markers, so a recorded call never embeds a
data set or a fit.
What is not emitted
-
Internal work. The fits, refits and solves a driver
runs are inside its scope and emit nothing – not the bootstrap fits, not
the multistart candidates, not the profile steps, not the covariate
search candidates. This includes
bootstrapFit()when it is called bybootplot(). -
Nested drivers. A driver called from inside another
driver (or from inside any other emitting operation, such as
nlmixr2()) is itself silent; only the outermost call reports. - Errors. When a driver fails, it leaves the scope and emits nothing.
-
Non-fit inputs. A driver whose input is not a fit
emits only when its result holds a best fit (as
multistart(model, data)does). SorerunTopN(), whose input is aniivSearch()result, emits nothing; its refits are simply kept silent. -
A best fit that is the input. When a result’s best
fit is the input fit itself, only the
fitResultis emitted. - Unsummarizable results. If summarizing the result fails, the driver leaves the scope without an event rather than breaking the call.
Listening
A listener is a function function(event, ...). This one
records the event name and the driver’s kind:
library(nlmixr2extra)
events <- list()
rxode2::rxEventListen("demo", function(event, ...) {
p <- list(...)
kind <- if (!is.null(p$what)) p$what else if (!is.null(p$source)) p$source else p$kind
events[[length(events) + 1L]] <<- data.frame(event = event, kind = kind)
})A fit to drive. It is run inside rxode2::rxEventScope()
here so that only the drivers below are recorded (otherwise
nlmixr2() would report this fit with its own
fitComplete, when nlmixr2est has the event
hooks):
one.compartment <- 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 <- rxode2::rxEventScope(
nlmixr2(one.compartment, nlmixr2data::theo_sd, est = "focei",
control = list(print = 0))
)
#> [====|====|====|====|====|====|====|====|====|====] 0:00:00
#> [====|====|====|====|====|====|====|====|====|====] 0:00:00
#> [====|====|====|====|====|====|====|====|====|====] 0:00:00
#> calculating covariance matrix
#> [====|====|====|====|====|====|====|====|====|====] 0:00:00
#> doneNow call a few drivers. Each runs many fits internally, but reports
once (or twice, for multistart()’s best start):
preconditionFit(fit)
lin <- linearize(fit)
prof <- profile(fit, which = data.frame(tka = log(c(1.4, 1.6))), method = "fixed")
ms <- multistart(fit, control = list(n = 2, spread = 0.3, screen = "none",
cacheDir = NA, refitBest = FALSE))
do.call(rbind, events)
#> event kind
#> 1 fitUpdate covariance
#> 2 fitComplete linearize
#> 3 fitResult profile
#> 4 fitResult multistart
#> 5 fitComplete multistartThe heavier drivers report the same way; for example, this records a
single fitUpdate (kind bootstrap) for the 100
bootstrap fits:
bootstrapFit(fit, nboot = 100)When you are done, remove the listener:
rxode2::rxEventUnlisten("demo")Full logging
For a complete log of fits, simulations and driver results – with each driver call stored as one entry linked to the fit it started from – use nlmixr2log, which listens to these events for you.
