---
title: "nlmixr2est events for loggers"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{nlmixr2est events for loggers}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
has_bus <- exists("rxEventListen", asNamespace("rxode2"))
knitr::opts_chunk$set(eval = has_bus)
```

```{r, include = FALSE}
rxode2::rxSetProgressBar(0)
```

```{r, echo = FALSE, eval = !has_bus, results = "asis"}
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 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()` 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`.
* Reading a deferred objective computes it inside the fit: `inPlace = TRUE`,
  `name = NULL`, and `original` is 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 of `addCwres()`; the simulations inside `vpcSim()`;
  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, 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.)
* **Results that are not fits or solves.**  `nlmixr2(model)` without data
  (which returns the model) and `nlmixr2()` 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:

```{r}
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`:

```{r, results = "hide"}
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")
)
```

```{r}
show()
```

Only one `fitComplete` is delivered, although the fit solved the model many
times.  Simulations and predictions from the fit each give one
`solveComplete`:

```{r}
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:

```{r}
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:

```{r}
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:

```{r}
fit2 <- suppressMessages(addCwres(fit2))
show()
fit2 <- suppressMessages(addCwres(fit2))
show()
```

Remove the listener when you are done:

```{r}
rxode2::rxEventUnlisten("demo")
```

## Full logging

'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.

## 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.
