---
title: "Extending nlmixr2est: custom estimation methods and behavior hooks"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Extending nlmixr2est: custom estimation methods and behavior hooks}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

`nlmixr2est` provides the estimation routines for the `nlmixr2` ecosystem.  This
vignette is for **package developers** who want to plug their own behavior into
that machinery.  There are three extension points, from highest-level to lowest:

1. **Add a new estimation method** (`est = "myMethod"`) via S3 dispatch.
2. **Intercept an estimation** so a *standard* method transparently does something
   extra, without a new `est` name.
3. **Contribute to the objective function** from compiled code (C
   function-pointer linkage), e.g. to add likelihood terms during a FOCEi/SAEM fit.

The lowest-level solve hooks (injecting parameters or derivative forcing into the
`rxode2` solve itself) live in `rxode2`; see its *"Solve-time hooks for package
developers"* article.

## 1. Adding a new estimation method

`nlmixr2()` dispatches on the `est` string through the `nlmixr2Est()` S3 generic:
the estimation *environment* is given the class `c(est, "nlmixr2Est")`, so
`est = "myMethod"` calls `nlmixr2Est.myMethod()`.  A method needs three pieces:

* **A control constructor** `myMethodControl(...)` returning a classed list.
* **`getValidNlmixrCtl.myMethod(control)`** -- validate/coerce the user's control
  (called before dispatch).
* **`nlmixr2Est.myMethod(env, ...)`** -- do the fit and return an `nlmixr2FitData`.

By the time your method runs, the estimation environment `env` carries everything
you need:

| `env` slot          | contents                                             |
|---------------------|------------------------------------------------------|
| `env$ui`            | the model (an `rxUi`); `env$ui$iniDf` has the estimates |
| `env$data`          | the (validated) data.frame                           |
| `env$control`       | your validated control                               |
| `env$table`         | the `tableControl()` for output tables               |
| `class(env)[1]`     | the est name (`"myMethod"`)                           |
| `env$.nlmixr2Dots`  | any extra named `nlmixr2()` arguments (see section 2) |

The existing methods are the best templates -- a population optimizer such as
`bobyqa` (`R/bobyqa.R`) is the simplest, and `saem`/`focei` show the full pattern.
Skeleton:

```{r, eval = FALSE}
myMethodControl <- function(maxeval = 1000L, ...) {
  .ctl <- list(maxeval = as.integer(maxeval), ...)
  class(.ctl) <- "myMethodControl"
  .ctl
}

#' @exportS3Method nlmixr2est::getValidNlmixrCtl
getValidNlmixrCtl.myMethod <- function(control) {
  .ctl <- control[[1]]
  if (is.null(.ctl)) .ctl <- myMethodControl()
  if (!inherits(.ctl, "myMethodControl")) {
    stop("est = 'myMethod' needs control = myMethodControl(...)", call. = FALSE)
  }
  .ctl
}

#' @exportS3Method nlmixr2est::nlmixr2Est
nlmixr2Est.myMethod <- function(env, ...) {
  .ui   <- env$ui
  .data <- env$data
  .ctl  <- env$control
  ## ... run the fit, build the result ...
  ## return an nlmixr2FitData (see nlmixr2CreateOutputFromUi / the existing methods)
}
```

Optional method attributes (mirroring the built-ins) advertise capabilities to the
dispatcher, e.g. `attr(nlmixr2Est.myMethod, "covPresent") <- TRUE`.

`nlmixr2AllEst()` lists every registered method, so once your package is loaded
`est = "myMethod"` is available like any built-in.

## 2. Intercepting an estimation

Sometimes you do not want a *new* `est`; you want a model fit with a **standard**
estimator to transparently do something extra.  For example, `nlmixr2nn` lets a
model that contains a neural-network term be fit with `est = "focei"` and trains the
network as a side effect -- with no `est = "nn"`.

Register an **interceptor** -- a `function(env)` consulted at the top of
`nlmixr2Est()`, *before* the standard method dispatches:

```{r, eval = FALSE}
nlmixr2est::registerEstInterceptor("myPkg", function(env) {
  if (!shouldClaim(env$ui)) return(NULL)         # decline -> normal fit proceeds
  ## claim it: do the work and return a finalized nlmixr2FitData
  runMyThing(env)
})
```

* Returning **`NULL`** *declines* -- the next interceptor, then the ordinary
  method, runs.  Returning a fit **claims** the estimation.
* Interceptors are **re-entrancy guarded**: while a claimed interceptor runs,
  interceptors are suppressed, so it may call `nlmixr2()` internally (e.g. with the
  requested est) and get the ordinary method.
* Register in your package's `.onLoad()` and `removeEstInterceptor("myPkg")` in
  `.onUnload()`.

### Passing options to your interceptor

Any *extra named argument* to `nlmixr2()` is stashed on `env$.nlmixr2Dots`, so an
interceptor can be configured without wrapping the standard control.  `nlmixr2nn`
uses this so `nn = nnControl(...)` rides alongside the ordinary `est`/`control`:

```{r, eval = FALSE}
nlmixr2(model, data, "focei", foceiControl(), nn = nnControl(...))
#                                             ^ arrives as env$.nlmixr2Dots$nn
```

## 3. Contributing to the objective (compiled linkage)

The deepest hook lets a compiled plugin add terms to the FOCEi/SAEM objective
during the fit -- per observation, inside the parallel subject loop, with **no R
callback** (R is not thread-safe).  This is how `nlmixr2nn` feeds a network's
objective cotangents into a running FOCEi fit.

`nlmixr2est` exposes a registry of C callables via a function-pointer table
(`nlmixr2est::.nlmixr2estLikContribPtrs()`), consumed downstream through the header
`inst/include/nlmixr2estLikContrib.h`.  A contributor supplies plain-C entry points
(a per-subject begin/obs/end bundle and/or an EM per-subject log-likelihood) that
read the per-observation prediction `f`, residual variance `R`, and `dv`, and add
into the accumulators (`llik`, `dLL/deta`, ...).  Registration is via
`nlmixrRegisterLikContrib()` / `nlmixrRegisterEmLik()`; the registry is *cycled in
series*, so several packages can contribute at once.

Because the registry holds raw function pointers, the lifecycle matters in both
directions: install the pointer table on **every** `.onLoad()` (a reloaded
`nlmixr2est` hands back new addresses), and unregister every bundle in your
`.onUnload()` -- `nlmixr2est` cannot tell that your DLL has gone away, so a
bundle left behind is called on the next objective evaluation.

This is a C-linkage API rather than an R one -- see `inst/include/nlmixr2estLikContrib.h`
for the exact struct and the `nlmixr2nn` package (`src/*Contrib.c`) for a worked
consumer.  The mechanism follows `rxode2`'s function-pointer-table convention
described in the `rxode2` hooks article.

## See also

* `rxode2`: *"Solve-time hooks for package developers"* -- the par-loader hook,
  `rxForcedPars()`, and the dydt forcing hook used to change what the ODE solve
  sees.
* `nlmixr2AllEst()` -- the current dispatch table of estimation methods.
