Package 'admixr2'

Title: Aggregate Data Modelling
Description: Fit pharmacokinetic/pharmacodynamic (PK/PD) models to aggregate-level data (mean vector and covariance matrix per study) rather than individual-level data, for meta-analysis across studies. Integrates with the 'nlmixr2'/'rxode2' ecosystem via four estimation methods: a First-Order ('FO') analytical estimator, a Monte Carlo (MC) estimator, a Gauss-Hermite quadrature ('GH') estimator, and an Iterative Reweighting Monte Carlo ('IRMC') estimator. Methods are based on Välitalo (2021) <doi:10.1007/s10928-021-09760-1>; software described in van de Beek et al. (2025) <doi:10.1007/s10928-025-10011-w>.
Authors: H. van de Beek [aut, cre], P.A.J. Välitalo [aut], L.B. Zwep [aut], J.G.C. van Hasselt [aut]
Maintainer: H. van de Beek <[email protected]>
License: GPL (>= 3)
Version: 0.4.1
Built: 2026-09-22 10:18:10 UTC
Source: https://github.com/leidenpharmacology/admixr2

Help Index


Control settings for the FO (First-Order) estimator

Description

Creates a control object for nlmixr2(est = "adfo"). The FO estimator linearises model predictions at η=0\eta = 0: it is faster than the MC estimator but less accurate for models with large IIV or strongly non-linear individual predictions.

Usage

adfoControl(
  studies = list(),
  grad = c("analytical", "none", "fd"),
  algorithm = NULL,
  maxeval = 500L,
  ftol_rel = .Machine$double.eps^(1/2),
  print = 10L,
  seed = 12345L,
  cores = rxode2::rxCores(),
  nDisplayProgress = .Machine$integer.max,
  grad_h = 1e-04,
  grad_bounds = 5,
  cov_h = 0.001,
  cov_h_outer = .Machine$double.eps^(1/5),
  covMethod = c("r,s", "r", "none"),
  n_restarts = 1L,
  restart_sd = 0.5,
  workers = 1L,
  rxControl = NULL,
  calcTables = FALSE,
  compress = TRUE,
  ci = 0.95,
  sigdig = NULL,
  sigdigTable = NULL,
  addProp = c("combined2", "combined1"),
  optExpression = TRUE,
  sumProd = FALSE,
  literalFix = TRUE,
  returnAdmr = FALSE,
  resid_nodes = 81L,
  xtol_rel = .Machine$double.eps^(1/2),
  ...
)

Arguments

studies

Named list of study specifications (same format as admControl(): E, V, n, times, ev, optional method; or an observations list for multi-compartment fits – see admControl()).

grad

Gradient mode. "analytical" (default) uses the closed-form FO gradient with LBFGS; "none" uses derivative-free BOBYQA; "fd" uses central finite differences of the full NLL. Forward differencing was removed in 0.4.1 – it was 10^2 to 10^4 times less accurate than a central difference at every site measured, and the one solve per parameter it saved did not pay for a gradient the optimizer struggles to descend.

The default was "none" up to 0.4.0, because the structural thetas were finite-differenced through the whole NLL and the resulting gradient was too noisy for a quasi-Newton step to pay off. They are now differentiated analytically from a second-order sensitivity model (relative error ~1e-7 against a central difference, where the finite-difference pass reached 1e-2), so LBFGS on the exact gradient is the better default. A model that cannot build that sensitivity model falls back to the finite-difference gradient automatically, and grad = "none" remains available.

algorithm

nloptr algorithm, or NULL (default) to pick the default that matches grad: "NLOPT_LD_LBFGS" with a gradient, "NLOPT_LN_BOBYQA" when grad = "none". Any algorithm reported by nloptr::nloptr.print.options() is accepted. An explicit algorithm is reconciled with grad: when grad = "none" a gradient-based algorithm (⁠NLOPT_LD_*⁠ / ⁠NLOPT_GD_*⁠) falls back to "NLOPT_LN_BOBYQA"; when a gradient is requested a derivative-free algorithm (⁠NLOPT_LN_*⁠ / ⁠NLOPT_GN_*⁠) turns the gradient off. Both emit a message.

maxeval

Maximum function evaluations (default 500).

ftol_rel

Relative tolerance (default sqrt(.Machine$double.eps)).

print

Print-frequency for live progress (0 = silent).

seed

Random seed (used for restarts).

cores

OpenMP threads for rxSolve(). Defaults to rxode2::rxCores(). When workers > 1 it is a total budget, split across the workers.

nDisplayProgress

Passed to rxSolve(): show the solver's text progress bar only once a single solve exceeds this many subjects. The default (.Machine$integer.max) keeps it off for clean script/vignette output; lower it (e.g. 1000L) to see progress during long fits.

grad_h

Finite-difference step for unpaired struct theta gradient and FD Jacobian.

grad_bounds

Box-constraint half-width when using gradients: the fit is confined to ⁠p0 +/- grad_bounds⁠ on the optimizer scale, which for a log-scale parameter is a factor of exp(grad_bounds) (~148 at the default 5). This bound is admixr2's, not the model's – an unbounded parameter has no other – and nloptr reports normal convergence at a box corner, so a warning is emitted if an estimate finishes on it.

cov_h

Inner FD step for the gradient-based Hessian (only used when covMethod = "r" and grad != "none"). Default 1e-3.

cov_h_outer

Outer step scale for NLL-FD Hessian.

covMethod

"r,s" (the DEFAULT) computes the sandwich ⁠H^-1 J H^-1⁠; "r" the numerical Hessian alone, ⁠2H^-1⁠; "none" skips the covariance. A study generated from a published model defaults to "none" and refuses an explicit covariance method because it has no sampling law. All three span the structural, residual-error and omega parameters. Omega is included because excluding it also biases the STRUCTURAL standard errors downward – a theta carrying an eta is correlated with that eta's variance. If the weakly-identified omega Cholesky makes the Hessian non-positive definite, the structural + residual sub-block is reported with a warning.

"r,s" adds a sandwich correction, ⁠H^-1 J H^-1⁠, on the same Hessian. For FO this does more than correct kurtosis: ⁠V = J Omega J' + Sigma⁠ is the covariance of an exactly normal individual law, so the reported standard errors otherwise answer to the linearisation rather than to the model. The correction scores the FO fit against the model's true nonlinear law, built post-fit on a quadrature ensemble, and so absorbs part of the linearisation error as well. Point estimates are untouched. Transform-both-sides endpoints. adfo composes the residual by a second-order expansion about the linearised moments, because FO carries no node ensemble to compose over – that is what the method is. adgh and admc compose exactly at their nodes/draws, so an adfo fit of a boxCox, yeoJohnson, logitNorm or probitNorm endpoint differs from theirs by the expansion's truncation: roughly 0.3% in V at moderate between-subject variability, rising to ~3% for a tightly-bounded logit/probit at high variability. That is a property of the estimator, not a discrepancy.

It is the default because it is the conservative choice, not the aggressive one. Under correct specification ⁠J = 2H⁠ and the sandwich returns what "r" returns, so defaulting to it costs nothing when the normal-theory assumption holds and corrects the standard errors when it does not. Anything it cannot build degrades to "r" and reports "r", so no fit loses its covariance by asking. Pass covMethod = "r" for the pre-0.4.1 behaviour.

Applies to every residual family whose conditional law is independent across timepoints, which is all of them except ar(): the conditionally-normal set (add, prop, pow, combined1, combined2), the closed-form distributional ones (lnorm, pois, binom, nbinomMu, beta, and t() with nu > 4), and the transform-both-sides ones (boxCox, yeoJohnson, logitNorm, probitNorm), whose third and fourth conditional moments come off the same quadrature that already gives their mean and variance. Refused, and degraded to "r": ar(), because it correlates the residual ACROSS timepoints and the cross terms the expansion drops are then real; t() with nu <= 4, whose kurtosis does not exist; and ordinal() and same-subject joint studies, which stack several outputs into one covariance the per-output node ensemble does not describe. These four are refusals by construction rather than failures, so the fit reports the reason as a message and falls back to "r"; a sandwich that was attempted and could not be built still warns.

"r,s" is more sensitive to an ill-conditioned Hessian than "r" is. "r" reports ⁠2H^-1⁠ and inverts H once; the sandwich reports ⁠H^-1 J H^-1⁠ and inverts it twice, so in a direction the data barely identifies any gap between J and ⁠2H⁠ is amplified quadratically. A residual SD contributing 0.01 variance against 1.7 from between-subject variability is such a direction: measured on one 1-cmt fixture at cond(H) = 3.5e5, the reported residual SE moved by a factor of 0.11 and two omega entries by 0.59 and 1.55, while the same model and design on a study the residual IS identified in (cond(H) = 247) reproduced "r" to four decimals on every parameter. Neither number is a correction there – both methods are reporting an unidentified direction, and "r,s" is louder about it. admixr2 says so: when the Hessian's reciprocal condition number falls below eps^(1/4) – the point at which squaring the conditioning reaches the bound a single inversion is already called singular at – the fit records a note naming the parameter that loads most heavily on the offending direction. It arrives on fit$runInfo and is listed by print(fit), which is where nlmixr2est routes an estimator's warnings. The sandwich is still reported, because the well-determined parameters of the same fit are unaffected; check the named parameter's relative standard error before reading its "r,s" value as a finding.

All three blocks are reported on the scale the ESTIMATES are printed on, as nlmixr2est does: structural thetas on the log/optimizer scale, residual error as an SD, and omega as the variance/covariance entries (named ⁠om.<eta>⁠ and ⁠cov.<eta_i>.<eta_j>⁠). The omega block is rotated by the full Jacobian of Omega with respect to the log-Cholesky, which is not diagonal once omega is correlated.

An adfo standard error describes scatter, not accuracy. FO linearises the model at eta = 0, and on a non-additive residual (or a saturating endpoint, or a large omega) the resulting point estimates carry a bias of several standard errors – measured 5-20 SE, giving 0% coverage for a nominal 95% interval even where the SE itself matches the sampling SD. Use adgh or admc when the uncertainty matters.

n_restarts

Number of optimizer restarts (1 = no multi-start).

restart_sd

Standard deviation for random perturbations of initial struct thetas at each restart (> 1).

workers

Number of parallel workers (mirai daemons) for multi-restart (default 1 = sequential). Requires the mirai package.

rxControl

rxode2::rxControl() object. Created automatically when NULL.

calcTables, compress, ci, sigdigTable, optExpression, sumProd, literalFix

Passed to nlmixr2est::foceiControl() for the table/output machinery.

sigdig

Significant digits asked of the ODE solver, or NULL (the default) to leave rxode2's own solver tolerances alone. When set, it is passed to rxode2::rxSolve()'s own sigdig argument for every solve the estimator issues – rxode2 owns the mapping to atol/rtol and has changed it between releases, which is why the digits, not the tolerances, are what travels – and to nlmixr2est::foceiControl() for the post-fit tables.

It is a speed lever, and an opt-in one because it is not free. The estimators finite-difference the solve with steps of the same order: grad_h (1e-4), cov_h (1e-3) and cov_h_outer (~2.5e-3), while sigdig = 4 maps to a relative tolerance of ~1e-4 on current rxode2. Differencing a solution whose own noise is 1e-4 with a 1e-4 step returns noise, and it surfaces as a moved objective and an indefinite covariance Hessian (every SE reported NA) rather than as an error. Most worthwhile where the gradient is fully analytic and nothing differences the solve – adfoControl(grad = "analytical") measured ~4.8x faster at sigdig = 4 with standard errors unchanged to 4 significant figures. Elsewhere, compare the objective and the standard errors against NULL before relying on it. Table formatting is unaffected either way: sigdigTable defaults to 4 regardless.

addProp

How combined additive+proportional error is parameterised in the nlmixr2 output tables: "combined2" (default, variance form) or "combined1" (SD form). Has no effect on admixr2's own estimation.

returnAdmr

If TRUE, return a plain list instead of the full nlmixr2 fit object.

resid_nodes

Gauss-Hermite nodes used to integrate the RESIDUAL for a transform-both-sides endpoint (boxCox, yeoJohnson, logitNorm, probitNorm), where y = g(h(f) + sigma*eps) has no closed-form mean and variance. Ignored by every other error model, which has closed forms. Default 81. Measured worst-case relative error against an independent quadrature, over all four transforms and residual SD of 0.5, 1, 2 and 3: n = 15 gives 5.7e-2, 31 gives 4.5e-3, 81 gives 5.0e-5. The error is dominated by large residual SD; at SD <= 1, n = 31 already gives 1e-7 or better.

This is an ACCURACY dial, not a speed one. The quadrature is linear in resid_nodes in isolation (~50 us at 15, 300 us at 81 for an 8-row study) but negligible beside the ODE solve: a full NLL evaluation measured 0.750 s per 60 evaluations at BOTH 31 and 81 nodes. Raise it if you have a saturating endpoint with a large residual SD; there is little to gain by lowering it.

xtol_rel

Relative parameter tolerance (default sqrt(.Machine$double.eps)).

...

Unused arguments (trigger an error).

Value

An adfoControl object (a named list).

Installing memuse

rxode2::rxSolve() estimates free RAM on every call. When the memuse package is not installed its fallback ends up shelling out to vm_stat, a macOS-only command, so on Windows and Linux every solve spawns a process that can only fail. Because the FO estimator issues many small solves, this overhead is measurable (roughly 17% of an FO gradient). Installing memuse makes the fallback unreachable:

install.packages("memuse")

See Also

admControl(), adirmcControl()

Examples

# Inspect defaults
ctl <- adfoControl()
ctl$grad
ctl$maxeval

# Analytical gradient, more evaluations
ctl2 <- adfoControl(grad = "analytical", maxeval = 1000L)


library(rxode2)
library(nlmixr2)

data("examplomycin")
obs    <- examplomycin[examplomycin$EVID == 0, ]
obs    <- obs[order(obs$ID, obs$TIME), ]
times  <- sort(unique(obs$TIME))
ids    <- unique(obs$ID)
dv_mat <- do.call(rbind, lapply(ids, function(i) {
  sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)]
}))
E <- colMeans(dv_mat)
V <- cov.wt(dv_mat, method = "ML")$cov

pk_model <- function() {
  ini({
    tcl <- log(5); tv <- log(30)
    prop.sd <- c(0, 0.2)
    eta.cl ~ 0.09; eta.v ~ 0.04
  })
  model({
    cl <- exp(tcl + eta.cl)
    v  <- exp(tv  + eta.v)
    d/dt(central) <- -(cl/v) * central
    cp <- central / v
    cp ~ prop(prop.sd)
  })
}

fit <- nlmixr2(
  pk_model, admData(), est = "adfo",
  control = adfoControl(
    studies = list(study1 = list(E = E, V = V, n = length(ids),
                                 times = times, ev = et(amt = 100))),
    maxeval = 100L
  )
)
print(fit)

Control settings for the Gauss-Hermite (GH) quadrature estimator

Description

Creates a control object for nlmixr2(est = "adgh"). The GH estimator integrates model predictions against the random-effects prior η∼N(0,Ω)\eta \sim N(0, \Omega) using a deterministic tensor-product Gauss-Hermite quadrature grid. It is unbiased at any IIV magnitude (unlike FO), noise-free (unlike MC), and much faster than MC for models with up to ~4 etas.

Usage

adghControl(
  studies = list(),
  n_nodes = 5L,
  grad = c("analytical", "fd", "none"),
  algorithm = NULL,
  maxeval = 500L,
  ftol_rel = .Machine$double.eps^(1/2),
  print = 10L,
  seed = 12345L,
  cores = rxode2::rxCores(),
  nDisplayProgress = .Machine$integer.max,
  grad_h = 1e-04,
  grad_bounds = 5,
  cov_h = 0.001,
  cov_h_outer = .Machine$double.eps^(1/4),
  covMethod = c("r,s", "r", "none"),
  n_restarts = 1L,
  restart_sd = 0.5,
  workers = 1L,
  rxControl = NULL,
  calcTables = FALSE,
  compress = TRUE,
  ci = 0.95,
  sigdig = NULL,
  sigdigTable = NULL,
  addProp = c("combined2", "combined1"),
  optExpression = TRUE,
  sumProd = FALSE,
  literalFix = TRUE,
  returnAdmr = FALSE,
  resid_nodes = 81L,
  cov_nodes = 7L,
  cov_integration = c("on", "sparse", "off"),
  cov_sparse_level = 3L,
  xtol_rel = .Machine$double.eps^(1/2),
  ...
)

Arguments

studies

Named list of study specifications (same format as admControl(): E, V, n, times, ev, optional method; or an observations list for multi-compartment fits – see admControl()).

n_nodes

Number of quadrature nodes per eta dimension (default 5). For a transform-both-sides endpoint (boxCox, yeoJohnson, logitNorm, probitNorm) this also controls the accuracy of the RESIDUAL composition: those endpoints have a conditional mean that is nonlinear in the structural prediction, so the residual is composed at each node and aggregated rather than expanded about the ensemble mean. Before that, n_nodes had no effect at all on a TBS fit's accuracy – the expansion's error was a floor no node count removed. Total nodes = n_nodes^n_eta. n_nodes = 5 achieves near-exact covariance moments for IIV SD up to ~0.5; n_nodes = 7 extends coverage to SD ~0.7. For models with >= 5 etas the node count grows steeply; consider reducing n_nodes or using a different estimator.

grad

Gradient mode. "analytical" (default) uses closed-form contractions through the sensitivity equations – cheapest and exact. "fd" uses central finite differences (forward differencing was removed in 0.4.1; see adfoControl()). "none" uses derivative-free BOBYQA.

algorithm

nloptr algorithm, or NULL (default) to pick the default that matches grad: "NLOPT_LD_LBFGS" with a gradient, "NLOPT_LN_BOBYQA" when grad = "none". Any algorithm reported by nloptr::nloptr.print.options() is accepted. An explicit algorithm is reconciled with grad: when grad = "none" a gradient-based algorithm (⁠NLOPT_LD_*⁠ / ⁠NLOPT_GD_*⁠) falls back to "NLOPT_LN_BOBYQA"; when a gradient is requested a derivative-free algorithm (⁠NLOPT_LN_*⁠ / ⁠NLOPT_GN_*⁠) turns the gradient off. Both emit a message.

maxeval

Maximum function evaluations (default 500).

ftol_rel

Relative tolerance (default sqrt(.Machine$double.eps)).

print

Print-frequency for live progress (0 = silent).

seed

Random seed (used for restarts).

cores

OpenMP threads for rxSolve(). Defaults to rxode2::rxCores(). When workers > 1 it is a total budget, split across the workers.

nDisplayProgress

Passed to rxSolve(): show the solver's text progress bar only once a single solve exceeds this many subjects. The default (.Machine$integer.max) keeps it off for clean script/vignette output; lower it (e.g. 1000L) to see progress during long fits.

grad_h

Finite-difference step for unpaired struct theta gradient and FD Jacobian fallback.

grad_bounds

Box-constraint half-width when using gradients: the fit is confined to ⁠p0 +/- grad_bounds⁠ on the optimizer scale, which for a log-scale parameter is a factor of exp(grad_bounds) (~148 at the default 5). This bound is admixr2's, not the model's – an unbounded parameter has no other – and nloptr reports normal convergence at a box corner, so a warning is emitted if an estimate finishes on it.

cov_h

Inner FD step for the gradient-based Hessian (only used when covMethod = "r" and grad != "none").

cov_h_outer

Outer step scale for numerical Hessian. Default eps^(1/4) (tighter than admc's eps^(1/5) because the GH surface is noise-free).

covMethod

"r,s" (the DEFAULT) computes the sandwich ⁠H^-1 J H^-1⁠; "r" the numerical Hessian alone, ⁠2H^-1⁠; "none" skips the covariance. A study generated from a published model defaults to "none" and refuses an explicit covariance method because it has no sampling law. All three span the structural, residual-error and omega parameters. Omega is included because excluding it also biases the STRUCTURAL standard errors downward – a theta carrying an eta is correlated with that eta's variance. If the weakly-identified omega Cholesky makes the Hessian non-positive definite, the structural + residual sub-block is reported with a warning.

"r,s" adds a sandwich correction, ⁠H^-1 J H^-1⁠, on the same Hessian. The aggregate objective scores the reported mean and covariance as though the subjects behind them were multivariate normal; they are not, because the model is nonlinear in the random effects, so the sampling law of ⁠(E, V)⁠ is not the one the objective assumes. "r,s" scores that law from the model instead. Point estimates are untouched – only the reported uncertainty changes – and under correct specification it reduces to "r" exactly. It is the default because it is the conservative choice, not the aggressive one. Under correct specification ⁠J = 2H⁠ and the sandwich returns what "r" returns, so defaulting to it costs nothing when the normal-theory assumption holds and corrects the standard errors when it does not. Anything it cannot build degrades to "r" and reports "r", so no fit loses its covariance by asking. Pass covMethod = "r" for the pre-0.4.1 behaviour.

Applies to every residual family whose conditional law is independent across timepoints, which is all of them except ar(): the conditionally-normal set (add, prop, pow, combined1, combined2), the closed-form distributional ones (lnorm, pois, binom, nbinomMu, beta, and t() with nu > 4), and the transform-both-sides ones (boxCox, yeoJohnson, logitNorm, probitNorm), whose third and fourth conditional moments come off the same quadrature that already gives their mean and variance. Refused, and degraded to "r": ar(), because it correlates the residual ACROSS timepoints and the cross terms the expansion drops are then real; t() with nu <= 4, whose kurtosis does not exist; and ordinal() and same-subject joint studies, which stack several outputs into one covariance the per-output node ensemble does not describe. These four are refusals by construction rather than failures, so the fit reports the reason as a message and falls back to "r"; a sandwich that was attempted and could not be built still warns.

"r,s" is more sensitive to an ill-conditioned Hessian than "r" is. "r" reports ⁠2H^-1⁠ and inverts H once; the sandwich reports ⁠H^-1 J H^-1⁠ and inverts it twice, so in a direction the data barely identifies any gap between J and ⁠2H⁠ is amplified quadratically. A residual SD contributing 0.01 variance against 1.7 from between-subject variability is such a direction: measured on one 1-cmt fixture at cond(H) = 3.5e5, the reported residual SE moved by a factor of 0.11 and two omega entries by 0.59 and 1.55, while the same model and design on a study the residual IS identified in (cond(H) = 247) reproduced "r" to four decimals on every parameter. Neither number is a correction there – both methods are reporting an unidentified direction, and "r,s" is louder about it. admixr2 says so: when the Hessian's reciprocal condition number falls below eps^(1/4) – the point at which squaring the conditioning reaches the bound a single inversion is already called singular at – the fit records a note naming the parameter that loads most heavily on the offending direction. It arrives on fit$runInfo and is listed by print(fit), which is where nlmixr2est routes an estimator's warnings. The sandwich is still reported, because the well-determined parameters of the same fit are unaffected; check the named parameter's relative standard error before reading its "r,s" value as a finding.

All three blocks are reported on the scale the ESTIMATES are printed on, as nlmixr2est does: structural thetas on the log/optimizer scale, residual error as an SD, and omega as the variance/covariance entries (named ⁠om.<eta>⁠ and ⁠cov.<eta_i>.<eta_j>⁠). The omega block is rotated by the full Jacobian of Omega with respect to the log-Cholesky, which is not diagonal once omega is correlated.

n_restarts

Number of optimizer restarts (1 = no multi-start).

restart_sd

SD of random perturbations of initial struct thetas at each restart.

workers

Number of parallel workers (mirai daemons) for multi-restart (default 1 = sequential). Requires the mirai package.

rxControl

rxode2::rxControl() object. Created automatically when NULL.

calcTables, compress, ci, sigdigTable, optExpression, sumProd, literalFix

Passed to nlmixr2est::foceiControl() for the table/output machinery.

sigdig

Significant digits asked of the ODE solver, or NULL (the default) to leave rxode2's own solver tolerances alone. When set, it is passed to rxode2::rxSolve()'s own sigdig argument for every solve the estimator issues – rxode2 owns the mapping to atol/rtol and has changed it between releases, which is why the digits, not the tolerances, are what travels – and to nlmixr2est::foceiControl() for the post-fit tables.

It is a speed lever, and an opt-in one because it is not free. The estimators finite-difference the solve with steps of the same order: grad_h (1e-4), cov_h (1e-3) and cov_h_outer (~2.5e-3), while sigdig = 4 maps to a relative tolerance of ~1e-4 on current rxode2. Differencing a solution whose own noise is 1e-4 with a 1e-4 step returns noise, and it surfaces as a moved objective and an indefinite covariance Hessian (every SE reported NA) rather than as an error. Most worthwhile where the gradient is fully analytic and nothing differences the solve – adfoControl(grad = "analytical") measured ~4.8x faster at sigdig = 4 with standard errors unchanged to 4 significant figures. Elsewhere, compare the objective and the standard errors against NULL before relying on it. Table formatting is unaffected either way: sigdigTable defaults to 4 regardless.

addProp

How combined additive+proportional error is parameterised in the nlmixr2 output tables: "combined2" (default) or "combined1".

returnAdmr

If TRUE, return a plain list instead of the full nlmixr2 fit object.

resid_nodes

Gauss-Hermite nodes used to integrate the RESIDUAL for a transform-both-sides endpoint (boxCox, yeoJohnson, logitNorm, probitNorm), where y = g(h(f) + sigma*eps) has no closed-form mean and variance. Ignored by every other error model, which has closed forms. Default 81. Measured worst-case relative error against an independent quadrature, over all four transforms and residual SD of 0.5, 1, 2 and 3: n = 15 gives 5.7e-2, 31 gives 4.5e-3, 81 gives 5.0e-5. The error is dominated by large residual SD; at SD <= 1, n = 31 already gives 1e-7 or better.

This is an ACCURACY dial, not a speed one. The quadrature is linear in resid_nodes in isolation (~50 us at 15, 300 us at 81 for an 8-row study) but negligible beside the ODE solve: a full NLL evaluation measured 0.750 s per 60 evaluations at BOTH 31 and 81 nodes. Raise it if you have a saturating endpoint with a large residual SD; there is little to gain by lowering it.

cov_nodes

Gauss-Hermite nodes per covariate used to integrate the COVARIATE distribution when a study declares cov_dist (default 7). This is a separate dial from n_nodes, which refines the random-effect dimensions only: raising n_nodes alone leaves the covariate integration exactly where it was. Measured on a two-compartment model with an allometric weight effect and a lognormal weight distribution, 7 nodes place the marginal moments within 2e-06 (mean) and 2e-05 (covariance) of an exact reference, and the remaining error is the ODE solver's rather than the quadrature's. A wider or more skewed covariate distribution, or a more strongly non-linear covariate effect, warrants more. Measured against an exact reference on a two-compartment model with an allometric weight effect and a lognormal weight distribution: 3 nodes give 7.3e-04 / 8.2e-03 (mean / covariance), 5 give 2.8e-05 / 3.7e-04, 7 give 2.2e-06 / 2.4e-05, and 9 onwards sit at ~1.2e-06 / ~1.0e-06, which is the ODE solver's accuracy rather than the quadrature's. The default is set past that knee, and raising it further buys nothing: against a per-subject reference the accuracy is identical at 5, 9 and 15 nodes. Ignored when cov_integration = "sparse", which sets its own resolution through cov_sparse_level.

It is a nodes-per-DIRECTION budget rather than a literal node count. Where the covariates reach the model through fewer scalars than there are covariates, admixr2 integrates over those directions instead of over a product grid, and each direction is given cov_nodes * p / r nodes rounded up – MORE than cov_nodes, because a direction that absorbs several covariate axes carries their combined spread and needs proportionally more resolution to resolve it. Three covariates reaching the model as a single scalar therefore get 21 nodes on one direction at the default, not 7, and still cost 21 design points against the product grid's 343. The same budget sizes the directions of a joint random-effect/covariate design where one is used.

cov_integration

How a study's covariate distribution is integrated. Three states.

"on" (default) integrates on a product Gauss–Hermite grid of cov_nodes points per covariate and reduces it wherever the model permits, choosing per study without being asked. Where the random effects and the covariates span fewer directions than they have members – an allometric weight effect on the same parameter as its random effect is one direction, not two – the integral is taken over those directions instead, which for p covariates costs design points in the rank rather than cov_nodes^p. A study that does not qualify is integrated on the full grid. There is nothing to tune: a reduction is admitted only after it reproduces the design it stands in for, so it cannot trade accuracy for speed behind your back. Measured across four model shapes it is 2.5x to 17x cheaper AND 100x to 170000x more accurate than the unreduced grid.

"off" disables every reduction and integrates on the full product grid. Slower, and useful mainly as a reference when a result is in question.

"sparse" replaces the product rule with a Smolyak sparse grid of cov_sparse_level, which for p covariates costs far fewer than cov_nodes^p points and is the speed lever for models with several covariates.

Both are Gauss–Hermite rules; they differ in which product terms are kept. Measured against an exact reference (lognormal margins, allometric plus a saturable term), relative error on the mean and the covariance:

rule p = 3, rho = 0.85 p = 4, rho = 0.85
sparse, level 2 6 pts, 7.8e-04 / 4.7e-02 9 pts, 9.7e-04 / 6.1e-02
product, 3 nodes 27 pts, 4.0e-05 / 1.5e-02 81 pts, 1.5e-04 / 3.2e-02
sparse, level 3 31 pts, 1.6e-06 / 5.0e-04 49 pts, 3.5e-06 / 9.5e-04

At four covariates level 3 is both cheaper than the 3-node product grid and roughly 40x more accurate, and the advantage grows with p. Level 2 is the axial rule — at one covariate it is exactly cov_nodes = 3 — and it is offered for continuity rather than recommended.

DEPENDENT covariates (cor, rho, Sigma) are handled by rotating onto the eigenvectors of the latent correlation, and correlation does not cost the sparse rule accuracy: at p = 2 its mean error is 6.6e-07 at rho = 0 and 4.8e-08 at rho = 0.85. (Level 2 behaves the other way, losing an order of magnitude to correlation, which is one reason the default is 3.) An opaque joint sampler is refused, because the rotation needs a correlation the closure does not report — declare the dependence with cor and admixr2 builds the sampler itself.

The cost of a sparse rule is SIGNED weights: they sum to 1 exactly, but the sum of their absolute values is 2.5 at level 3 for three covariates and 4.1 for four, so the answer is a difference of terms several times its own size and solver noise is amplified accordingly. A sandwich covariance whose weight matrix comes out indefinite as a result is refused rather than reported.

cov_sparse_level

Smolyak level for cov_integration = "sparse" (default 3, minimum 2). Level 2 is the axial rule, level 3 adds the five-point axes and the pairwise crosses, and each further level refines again at a growing weight-magnitude cost. See cov_integration for the measured accuracy and point counts.

xtol_rel

Relative parameter tolerance (default sqrt(.Machine$double.eps)).

...

Unused arguments (trigger an error).

Value

An adghControl object (a named list).

See Also

admControl(), adfoControl(), adirmcControl()

Examples

ctl <- adghControl()
ctl$n_nodes
ctl$grad

# More nodes for large IIV, analytical gradient
ctl2 <- adghControl(n_nodes = 7L, grad = "analytical", maxeval = 300L)


library(rxode2)
library(nlmixr2)

data("examplomycin")
obs    <- examplomycin[examplomycin$EVID == 0, ]
obs    <- obs[order(obs$ID, obs$TIME), ]
times  <- sort(unique(obs$TIME))
ids    <- unique(obs$ID)
dv_mat <- do.call(rbind, lapply(ids, function(i) {
  sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)]
}))
E <- colMeans(dv_mat)
V <- cov.wt(dv_mat, method = "ML")$cov

pk_model <- function() {
  ini({
    tcl <- log(5); tv <- log(30)
    prop.sd <- c(0, 0.2)
    eta.cl ~ 0.09; eta.v ~ 0.04
  })
  model({
    cl <- exp(tcl + eta.cl)
    v  <- exp(tv  + eta.v)
    d/dt(central) <- -(cl/v) * central
    cp <- central / v
    cp ~ prop(prop.sd)
  })
}

fit <- nlmixr2(
  pk_model, admData(), est = "adgh",
  control = adghControl(
    studies = list(study1 = list(E = E, V = V, n = length(ids),
                                 times = times, ev = et(amt = 100)))
  )
)

Control settings for the IRMC estimator

Description

Constructs a control object for est = "adirmc", the Iterative Reweighting Monte Carlo estimator.

Usage

adirmcControl(
  studies = list(),
  n_sim = 2500L,
  outer_iter = 50L,
  sampling = c("sobol", "halton", "torus", "lhs", "rnorm"),
  algorithm = NULL,
  maxeval = 5000L,
  ftol_rel = .Machine$double.eps,
  print = 1L,
  omega_expansion = 1,
  seed = 12345L,
  cores = rxode2::rxCores(),
  nDisplayProgress = .Machine$integer.max,
  grad = c("analytical", "none", "fd"),
  kappa_method = c("exact", "linearized", "linearized_gh"),
  kappa_n_nodes = 5L,
  grad_h = 1e-06,
  cov_h = 0.001,
  cov_h_outer = .Machine$double.eps^(1/5),
  phases = c(2, 1, 0.5, 0.01),
  convcrit = 1e-05,
  max_worse = 5L,
  covMethod = c("r,s", "r", "none"),
  cov_n_sim = 10000L,
  n_restarts = 1L,
  restart_sd = 0.2,
  workers = 1L,
  rxControl = NULL,
  calcTables = FALSE,
  compress = TRUE,
  ci = 0.95,
  sigdig = NULL,
  sigdigTable = NULL,
  addProp = c("combined2", "combined1"),
  optExpression = TRUE,
  sumProd = FALSE,
  literalFix = TRUE,
  returnAdmr = FALSE,
  resid_nodes = 81L,
  xtol_rel = .Machine$double.eps^(1/2),
  ...
)

Arguments

studies

Named list of study specifications. Each element is a list with:

  • E – observed mean vector

  • V – observed covariance matrix or variance vector (auto-detected)

  • n – sample size

  • times – numeric vector of observation times

  • ev – rxode2::et() dosing event table

  • method – "cov" or "var" (optional; auto-detected from V)

  • v_denom – "ml" (default) or "unbiased", declaring which denominator the supplied V uses. The likelihood is the exact one for n iid draws only under the ML (n) covariance, which is what cov.wt(method = "ML") and datagen() produce. A published SD is the unbiased (n - 1) SD, so a digitised figure gives V = SD^2 on the n - 1 scale: declare v_denom = "unbiased" and admixr2 converts it. Declared per study, since a meta-analysis routinely mixes a digitised source with a model-derived one and the two need not share a denominator. At n = 60 the factor is 1.7%; it matters more the smaller n is, and more again for any method that scores the reported covariance against its own sampling law.

Multi-compartment (multiple observed outputs). To fit several observed compartments simultaneously (e.g. plasma and brain/CSF), give the study an observations list instead of top-level E/V/times. Each entry is one observed output with its own output (the model prediction variable, e.g. "cp" or "cCSF"), times, E, V and – for independent fits – ev and n. Pass the endpoint names to admData(), e.g. admData(c("cp", "cCSF")), so nlmixr2 recognises every endpoint. There are two modes:

  • Independent – each observed output has its own n/ev (separate experiments / subjects, e.g. a plasma study and a brain study combined for meta-analysis). The outputs are independent likelihood blocks and the aggregate ⁠-2LL⁠ is their sum.

  • Joint (same subjects) – the outputs are measured on the SAME subjects. Give the study a shared n and ev, and a joint covariance either as a study-level full matrix V (blocks in observations order) or as per-output marginal V plus a cross list of cross-covariance blocks keyed "outA:outB" (each length(times_A) x length(times_B); omitted pairs are zero). The compartments are then scored by a single MVN over the stacked vector with shared random effects. est = "adirmc" does not support multiple observed outputs; use "admc", "adfo" or "adgh".

Long format (one row per endpoint/time). As an alternative to the observations list, a study may carry a data frame that keys each observed summary by endpoint, the way nlmixr2 keys observations by DVID/CMT. The frame needs an endpoint column (DVID, CMT or output), a time column (TIME), a mean column (E) and – unless a joint V is given – a variance column (V) or an SD column (SD). It is normalised into exactly the same units as the observations form, so the two are interchangeable:

# independent blocks: per-row variances; optional per-endpoint `n` column
# and per-endpoint `ev` (a list of event tables keyed by endpoint)
list(n = 60L, ev = ev,
     data = data.frame(DVID = c("cp", "cp", "cCSF"), TIME = c(1, 2, 2),
                       E = c(9.1, 7.4, 2.2), V = c(1.2, 0.9, 0.1)))

# joint (same subjects): ONE stacked covariance whose rows/cols align with
# the rows of `data` -- no `cross` blocks to assemble by hand
list(n = 60L, ev = ev, data = data.frame(DVID = ..., TIME = ..., E = ...),
     V = V_joint)

A study-level V (or an explicit joint = TRUE) marks the endpoints as same-subject; without one, each endpoint is an independent likelihood block. Endpoints are stacked in the order they first appear in data.

n_sim

Number of Monte Carlo samples per NLL evaluation.

outer_iter

Maximum inner optimiser iterations per phase.

sampling

Sampling method for eta draws: "sobol" (Sobol, default), "halton" (Halton), "torus" (Kronecker/torus), "lhs" (Latin hypercube), or "rnorm" (iid normal).

algorithm

nloptr algorithm string, or NULL (default) to pick the default that matches grad: "NLOPT_LD_LBFGS" with a gradient, "NLOPT_LN_BOBYQA" when grad = "none". Any algorithm reported by nloptr::nloptr.print.options() is accepted (e.g. "NLOPT_LD_MMA", "NLOPT_LN_NELDERMEAD"). An explicit algorithm is reconciled with grad: when grad = "none" a gradient-based algorithm (⁠NLOPT_LD_*⁠ / ⁠NLOPT_GD_*⁠) falls back to "NLOPT_LN_BOBYQA"; when a gradient is requested a derivative-free algorithm (⁠NLOPT_LN_*⁠ / ⁠NLOPT_GN_*⁠) turns the gradient off. Both emit a message.

maxeval

Maximum number of optimizer function evaluations.

ftol_rel

Relative function-value tolerance for convergence.

print

Print progress every this many evaluations (0 = silent).

omega_expansion

Inflate proposal Omega by this factor (>= 1).

seed

Random seed for reproducibility.

cores

Number of OpenMP threads for rxSolve(). Defaults to rxode2::rxCores(). rxSolve() parallelises over subjects, so this is the main speed lever for the MC estimators; when workers > 1 it is a total budget, split across the workers.

nDisplayProgress

Passed to rxSolve(): the solver shows its text progress bar only once a single solve exceeds this many subjects. The default (.Machine$integer.max) keeps the bar off, which is what you want for scripts, vignettes and logs; lower it (e.g. 1000L) to see solver progress during long interactive fits.

grad

Gradient mode for the inner optimiser: "analytical" (default, closed-form weight-path gradient), "none" (derivative-free BOBYQA), or "fd" (central finite differences). Note: "sens" is not available for the IRMC estimator.

kappa_method

Kappa correction method for models with non-mu-referenced struct thetas: "exact" (default, re-evaluates population prediction f(theta, 0) via rxSolve at each inner step), "linearized" (precomputes J = df/d(theta) once per outer iteration using f(theta, 0) as baseline — zero rxSolve per inner step), or "linearized_gh" (same linear approximation but baseline and Jacobian use Gauss-Hermite quadrature E_GH[f(theta, eta)] instead of f(theta, 0) — more accurate baseline at any IIV magnitude, still zero rxSolve per inner step).

kappa_n_nodes

Number of GH nodes per eta dimension for kappa_method = "linearized_gh" (default 5). Total quadrature points = kappa_n_nodes^n_eta. Ignored for other kappa methods.

grad_h

Step size for the inner optimiser's finite-difference gradient (grad = "fd"). Defaults to 1e-6, not the 1e-4 the other three controls use: the IRMC inner NLL is deterministic given fixed proposals, so there is no Monte Carlo noise to step over and the truncation-versus-noise balance that sets 1e-4 elsewhere does not apply. The inner step was a hard-coded 1e-6 until it was made to honour grad_h; inheriting the coarser default would have changed the gradient the loop was tuned for.

cov_h

Inner FD step for the gradient-based Hessian (only used when covMethod = "r" and grad != "none"). Each gradient evaluation has MC noise of order sigma / cov_h; the Hessian divides that noise by the outer step, giving total noise ⁠sigma / (cov_h * cov_h_outer * |p|)⁠. cov_h = 1e-3 balances truncation error and noise amplification. Increase to 1e-2 if the Hessian is non-positive definite.

cov_h_outer

Outer step scale for the numerical Hessian. The actual step for parameter p is ⁠max(|p|, 0.1) * cov_h_outer⁠. Applied to both the gradient-FD Hessian (grad != "none") and the NLL-FD Hessian (grad = "none"). Default eps^(1/5) (~2.5e-3) is larger than the textbook eps^(1/4) to account for MC noise in NLL and gradient evaluations; empirically it matches the analytical (sensitivity-equation) Hessian ground truth. Increase (e.g. to 5e-3 or 1e-2) if the Hessian is non-positive definite.

phases

Numeric vector of box-constraint half-widths, one per phase. Phases progressively tighten the search region.

convcrit

Convergence criterion: phase ends when ⁠|approx - exact| < convcrit⁠.

max_worse

Stop a phase after this many consecutive worsening iterations.

covMethod

"r,s" (the DEFAULT) computes the sandwich ⁠H^-1 J H^-1⁠; "r" the numerical Hessian alone, ⁠2H^-1⁠; "none" skips the covariance. A study generated from a published model defaults to "none" and refuses an explicit covariance method because it has no sampling law. All three span the structural, residual-error and omega parameters, and are reported on the scale the estimates are printed on. admControl() documents what the sandwich is, why it is the conservative default, and why it is more sensitive than "r" to an ill-conditioned Hessian; the same implementation runs here. What does NOT carry over is the family coverage, because adirmc itself is narrower: the estimator accepts only add, prop, pow, combined1, combined2 and lnorm residuals, and "r,s" applies to all six. The count, beta, transform-both-sides, ordinal and ar() endpoints admControl() lists are refused by est = "adirmc" itself, before any covariance is reached – they are not models whose sandwich degrades to "r" here, they are models this estimator does not fit.

cov_n_sim

Number of MC samples for the covariance (Hessian) step. More samples reduce MC noise in NLL evaluations. The NLL-based Hessian (grad = "none") uses a central second difference of the NLL with the same Sobol sequence (CRN) at every perturbed point, so noise largely cancels and cov_n_sim = 10000 (default) is sufficient for most models.

n_restarts

Number of optimization restarts. Runs in parallel when workers > 1.

restart_sd

Standard deviation of structural theta perturbations for restart initialisation.

workers

Number of parallel workers for multi-restart. 1 (default) runs restarts sequentially. Values ⁠> 1⁠ run the restarts on a pool of background R processes (mirai daemons), which behaves the same way on every platform. Requires the mirai package. Workers are stopped automatically after the restart phase so all cores are available for the Hessian step; if a fit is interrupted, admStopWorkers() cleans up any survivors.

rxControl

rxode2::rxControl() object. Created automatically when NULL.

calcTables, compress, ci, sigdigTable, optExpression, sumProd, literalFix

Passed to nlmixr2est::foceiControl() for the table/output machinery.

sigdig

Significant digits asked of the ODE solver, or NULL (the default) to leave rxode2's own solver tolerances alone. When set, it is passed to rxode2::rxSolve()'s own sigdig argument for every solve the estimator issues – rxode2 owns the mapping to atol/rtol and has changed it between releases, which is why the digits, not the tolerances, are what travels – and to nlmixr2est::foceiControl() for the post-fit tables.

It is a speed lever, and an opt-in one because it is not free. The estimators finite-difference the solve with steps of the same order: grad_h (1e-4), cov_h (1e-3) and cov_h_outer (~2.5e-3), while sigdig = 4 maps to a relative tolerance of ~1e-4 on current rxode2. Differencing a solution whose own noise is 1e-4 with a 1e-4 step returns noise, and it surfaces as a moved objective and an indefinite covariance Hessian (every SE reported NA) rather than as an error. Most worthwhile where the gradient is fully analytic and nothing differences the solve – adfoControl(grad = "analytical") measured ~4.8x faster at sigdig = 4 with standard errors unchanged to 4 significant figures. Elsewhere, compare the objective and the standard errors against NULL before relying on it. Table formatting is unaffected either way: sigdigTable defaults to 4 regardless.

addProp

How combined additive+proportional error is parameterised in the nlmixr2 output tables: "combined2" (default, variance form) or "combined1" (SD form). Has no effect on admixr2's own estimation; passed to nlmixr2est::foceiControl() for the table/output machinery only.

returnAdmr

If TRUE, return a plain list instead of a full nlmixr2 fit object (useful for debugging).

resid_nodes

Gauss-Hermite nodes used to integrate the RESIDUAL for a transform-both-sides endpoint (boxCox, yeoJohnson, logitNorm, probitNorm), where y = g(h(f) + sigma*eps) has no closed-form mean and variance. Ignored by every other error model, which has closed forms. Default 81. Measured worst-case relative error against an independent quadrature, over all four transforms and residual SD of 0.5, 1, 2 and 3: n = 15 gives 5.7e-2, 31 gives 4.5e-3, 81 gives 5.0e-5. The error is dominated by large residual SD; at SD <= 1, n = 31 already gives 1e-7 or better.

This is an ACCURACY dial, not a speed one. The quadrature is linear in resid_nodes in isolation (~50 us at 15, 300 us at 81 for an 8-row study) but negligible beside the ODE solve: a full NLL evaluation measured 0.750 s per 60 evaluations at BOTH 31 and 81 nodes. Raise it if you have a saturating endpoint with a large residual SD; there is little to gain by lowering it.

xtol_rel

Relative parameter tolerance for convergence (default sqrt(.Machine$double.eps)).

...

Additional arguments (none allowed; triggers an error).

Details

Multi-compartment fits (a study observations list with several observed outputs) are not supported by adirmc; use est = "admc", "adfo", or "adgh" for those. Single-output studies are fit as usual.

Value

An object of class adirmcControl.

Examples

# Inspect defaults
ctl <- adirmcControl()
ctl$phases
ctl$omega_expansion

# Tighter phases, more restarts
ctl2 <- adirmcControl(
  n_sim           = 1000L,
  omega_expansion = 1.5,
  phases          = c(2, 1, 0.5, 0.01),
  n_restarts      = 3L
)


library(rxode2)
library(nlmixr2)

data("examplomycin")
obs   <- examplomycin[examplomycin$EVID == 0, ]
obs   <- obs[order(obs$ID, obs$TIME), ]
times <- sort(unique(obs$TIME))
ids   <- unique(obs$ID)
dv_mat <- do.call(rbind, lapply(ids, function(i) {
  sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)]
}))
E <- colMeans(dv_mat)
V <- diag(diag(cov.wt(dv_mat, method = "ML")$cov))

pk_model <- function() {
  ini({
    tcl <- log(5);  tv1 <- log(12); tv2 <- log(25)
    tq  <- log(12); tka <- log(1.2)
    prop.sd <- c(0, 0.2)
    eta.cl ~ 0.09; eta.v1 ~ 0.09; eta.v2 ~ 0.09
    eta.q  ~ 0.09; eta.ka ~ 0.09
  })
  model({
    cl <- exp(tcl + eta.cl); v1 <- exp(tv1 + eta.v1)
    v2 <- exp(tv2 + eta.v2); q  <- exp(tq  + eta.q)
    ka <- exp(tka + eta.ka)
    d/dt(depot)      <- -ka * depot
    d/dt(central)    <- ka * depot - (cl/v1 + q/v1) * central + (q/v2) * peripheral
    d/dt(peripheral) <- (q/v1) * central - (q/v2) * peripheral
    cp <- central / v1
    cp ~ prop(prop.sd)
  })
}

fit <- nlmixr2(
  pk_model, admData(), est = "adirmc",
  control = adirmcControl(
    studies = list(study1 = list(E = E, V = V, n = length(ids),
                                 times = times, ev = et(amt = 100))),
    n_sim   = 500L
  )
)
print(fit)

Control settings for the ADM estimator

Description

Constructs a control object for est = "admc", the Monte Carlo aggregate data modelling estimator.

Usage

admControl(
  studies = list(),
  n_sim = 5000L,
  sampling = c("sobol", "halton", "torus", "lhs", "rnorm"),
  algorithm = NULL,
  maxeval = 500L,
  ftol_rel = .Machine$double.eps^2,
  print = 10L,
  seed = 12345L,
  cores = rxode2::rxCores(),
  nDisplayProgress = .Machine$integer.max,
  grad = c("sens", "fd", "none"),
  grad_h = 1e-04,
  cov_h = 0.001,
  cov_h_outer = .Machine$double.eps^(1/5),
  grad_bounds = 5,
  covMethod = c("r,s", "r", "none"),
  cov_n_sim = 10000L,
  n_restarts = 1L,
  restart_sd = 0.5,
  workers = 1L,
  rxControl = NULL,
  calcTables = FALSE,
  compress = TRUE,
  ci = 0.95,
  sigdig = NULL,
  sigdigTable = NULL,
  addProp = c("combined2", "combined1"),
  optExpression = TRUE,
  sumProd = FALSE,
  literalFix = TRUE,
  returnAdmr = FALSE,
  resid_nodes = 81L,
  xtol_rel = .Machine$double.eps^(1/2),
  ...
)

Arguments

studies

Named list of study specifications. Each element is a list with:

  • E – observed mean vector

  • V – observed covariance matrix or variance vector (auto-detected)

  • n – sample size

  • times – numeric vector of observation times

  • ev – rxode2::et() dosing event table

  • method – "cov" or "var" (optional; auto-detected from V)

  • v_denom – "ml" (default) or "unbiased", declaring which denominator the supplied V uses. The likelihood is the exact one for n iid draws only under the ML (n) covariance, which is what cov.wt(method = "ML") and datagen() produce. A published SD is the unbiased (n - 1) SD, so a digitised figure gives V = SD^2 on the n - 1 scale: declare v_denom = "unbiased" and admixr2 converts it. Declared per study, since a meta-analysis routinely mixes a digitised source with a model-derived one and the two need not share a denominator. At n = 60 the factor is 1.7%; it matters more the smaller n is, and more again for any method that scores the reported covariance against its own sampling law.

Multi-compartment (multiple observed outputs). To fit several observed compartments simultaneously (e.g. plasma and brain/CSF), give the study an observations list instead of top-level E/V/times. Each entry is one observed output with its own output (the model prediction variable, e.g. "cp" or "cCSF"), times, E, V and – for independent fits – ev and n. Pass the endpoint names to admData(), e.g. admData(c("cp", "cCSF")), so nlmixr2 recognises every endpoint. There are two modes:

  • Independent – each observed output has its own n/ev (separate experiments / subjects, e.g. a plasma study and a brain study combined for meta-analysis). The outputs are independent likelihood blocks and the aggregate ⁠-2LL⁠ is their sum.

  • Joint (same subjects) – the outputs are measured on the SAME subjects. Give the study a shared n and ev, and a joint covariance either as a study-level full matrix V (blocks in observations order) or as per-output marginal V plus a cross list of cross-covariance blocks keyed "outA:outB" (each length(times_A) x length(times_B); omitted pairs are zero). The compartments are then scored by a single MVN over the stacked vector with shared random effects. est = "adirmc" does not support multiple observed outputs; use "admc", "adfo" or "adgh".

Long format (one row per endpoint/time). As an alternative to the observations list, a study may carry a data frame that keys each observed summary by endpoint, the way nlmixr2 keys observations by DVID/CMT. The frame needs an endpoint column (DVID, CMT or output), a time column (TIME), a mean column (E) and – unless a joint V is given – a variance column (V) or an SD column (SD). It is normalised into exactly the same units as the observations form, so the two are interchangeable:

# independent blocks: per-row variances; optional per-endpoint `n` column
# and per-endpoint `ev` (a list of event tables keyed by endpoint)
list(n = 60L, ev = ev,
     data = data.frame(DVID = c("cp", "cp", "cCSF"), TIME = c(1, 2, 2),
                       E = c(9.1, 7.4, 2.2), V = c(1.2, 0.9, 0.1)))

# joint (same subjects): ONE stacked covariance whose rows/cols align with
# the rows of `data` -- no `cross` blocks to assemble by hand
list(n = 60L, ev = ev, data = data.frame(DVID = ..., TIME = ..., E = ...),
     V = V_joint)

A study-level V (or an explicit joint = TRUE) marks the endpoints as same-subject; without one, each endpoint is an independent likelihood block. Endpoints are stacked in the order they first appear in data.

n_sim

Number of Monte Carlo samples per NLL evaluation.

sampling

Sampling method for eta draws: "sobol" (Sobol, default), "halton" (Halton), "torus" (Kronecker/torus), "lhs" (Latin hypercube), or "rnorm" (iid normal).

algorithm

nloptr algorithm string, or NULL (default) to pick the default that matches grad: "NLOPT_LD_LBFGS" with a gradient, "NLOPT_LN_BOBYQA" when grad = "none". Any algorithm reported by nloptr::nloptr.print.options() is accepted (e.g. "NLOPT_LD_MMA", "NLOPT_LN_NELDERMEAD"). An explicit algorithm is reconciled with grad: when grad = "none" a gradient-based algorithm (⁠NLOPT_LD_*⁠ / ⁠NLOPT_GD_*⁠) falls back to "NLOPT_LN_BOBYQA"; when a gradient is requested a derivative-free algorithm (⁠NLOPT_LN_*⁠ / ⁠NLOPT_GN_*⁠) turns the gradient off. Both emit a message.

maxeval

Maximum number of optimizer function evaluations.

ftol_rel

Relative function-value tolerance for convergence.

print

Print progress every this many evaluations (0 = silent).

seed

Random seed for reproducibility.

cores

Number of OpenMP threads for rxSolve(). Defaults to rxode2::rxCores(). rxSolve() parallelises over subjects, so this is the main speed lever for the MC estimators; when workers > 1 it is a total budget, split across the workers.

nDisplayProgress

Passed to rxSolve(): the solver shows its text progress bar only once a single solve exceeds this many subjects. The default (.Machine$integer.max) keeps the bar off, which is what you want for scripts, vignettes and logs; lower it (e.g. 1000L) to see solver progress during long interactive fits.

grad

Gradient mode: "sens" (sensitivity equations, default), "fd" (central finite differences; forward was removed in 0.4.1), or "none" (derivative-free). A warning is issued when "sens" is requested but the sensitivity model is unavailable; the estimator then falls back to central finite differences.

grad_h

Step size for finite-difference gradient evaluation during optimization (used by grad = "fd"). This is the FALLBACK step: the step is normally measured per parameter by the Shi (2021) procedure, and grad_h is what a parameter falls back to when that measurement cannot be made (a direction the objective is flat in, or a failed noise estimate).

cov_h

Inner FD step for the gradient-based Hessian (only used when covMethod = "r" and grad != "none"). Each gradient evaluation has MC noise of order sigma / cov_h; the Hessian divides that noise by the outer step, giving total noise ⁠sigma / (cov_h * cov_h_outer * |p|)⁠. cov_h = 1e-3 balances truncation error and noise amplification. Increase to 1e-2 if the Hessian is non-positive definite.

cov_h_outer

Outer step scale for the numerical Hessian. The actual step for parameter p is ⁠max(|p|, 0.1) * cov_h_outer⁠. Applied to both the gradient-FD Hessian (grad != "none") and the NLL-FD Hessian (grad = "none"). Default eps^(1/5) (~2.5e-3) is larger than the textbook eps^(1/4) to account for MC noise in NLL and gradient evaluations; empirically it matches the analytical (sensitivity-equation) Hessian ground truth. Increase (e.g. to 5e-3 or 1e-2) if the Hessian is non-positive definite.

grad_bounds

Box-constraint half-width when using gradients: the fit is confined to ⁠p0 +/- grad_bounds⁠ on the optimizer scale, which for a log-scale parameter is a factor of exp(grad_bounds) (~148 at the default 5). This bound is admixr2's, not the model's – an unbounded parameter has no other – and nloptr reports normal convergence at a box corner, so a warning is emitted if an estimate finishes on it.

covMethod

"r,s" (the DEFAULT) computes the sandwich ⁠H^-1 J H^-1⁠; "r" the numerical Hessian alone, ⁠2H^-1⁠; "none" skips the covariance. A study generated from a published model defaults to "none" and refuses an explicit covariance method because it has no sampling law. All three span the structural, residual-error and omega parameters. Omega is included because excluding it also biases the STRUCTURAL standard errors downward – a theta carrying an eta is correlated with that eta's variance. If the weakly-identified omega Cholesky makes the Hessian non-positive definite, the structural + residual sub-block is reported with a warning.

"r,s" adds a sandwich correction, ⁠H^-1 J H^-1⁠, on the same Hessian. The aggregate objective scores the reported mean and covariance as though the subjects behind them were multivariate normal; they are not, because the model is nonlinear in the random effects. "r,s" scores that law from the model instead, on a quadrature ensemble rather than on this fit's own MC draws, so the reported uncertainty carries no sampling noise of its own. Point estimates are untouched, and under correct specification it reduces to "r" exactly. It is the default because it is the conservative choice, not the aggressive one. Under correct specification ⁠J = 2H⁠ and the sandwich returns what "r" returns, so defaulting to it costs nothing when the normal-theory assumption holds and corrects the standard errors when it does not. Anything it cannot build degrades to "r" and reports "r", so no fit loses its covariance by asking. Pass covMethod = "r" for the pre-0.4.1 behaviour.

Applies to every residual family whose conditional law is independent across timepoints, which is all of them except ar(): the conditionally-normal set (add, prop, pow, combined1, combined2), the closed-form distributional ones (lnorm, pois, binom, nbinomMu, beta, and t() with nu > 4), and the transform-both-sides ones (boxCox, yeoJohnson, logitNorm, probitNorm), whose third and fourth conditional moments come off the same quadrature that already gives their mean and variance. Refused, and degraded to "r": ar(), because it correlates the residual ACROSS timepoints and the cross terms the expansion drops are then real; t() with nu <= 4, whose kurtosis does not exist; and ordinal() and same-subject joint studies, which stack several outputs into one covariance the per-output node ensemble does not describe. These four are refusals by construction rather than failures, so the fit reports the reason as a message and falls back to "r"; a sandwich that was attempted and could not be built still warns.

"r,s" is more sensitive to an ill-conditioned Hessian than "r" is. "r" reports ⁠2H^-1⁠ and inverts H once; the sandwich reports ⁠H^-1 J H^-1⁠ and inverts it twice, so in a direction the data barely identifies any gap between J and ⁠2H⁠ is amplified quadratically. A residual SD contributing 0.01 variance against 1.7 from between-subject variability is such a direction: measured on one 1-cmt fixture at cond(H) = 3.5e5, the reported residual SE moved by a factor of 0.11 and two omega entries by 0.59 and 1.55, while the same model and design on a study the residual IS identified in (cond(H) = 247) reproduced "r" to four decimals on every parameter. Neither number is a correction there – both methods are reporting an unidentified direction, and "r,s" is louder about it. admixr2 says so: when the Hessian's reciprocal condition number falls below eps^(1/4) – the point at which squaring the conditioning reaches the bound a single inversion is already called singular at – the fit records a note naming the parameter that loads most heavily on the offending direction. It arrives on fit$runInfo and is listed by print(fit), which is where nlmixr2est routes an estimator's warnings. The sandwich is still reported, because the well-determined parameters of the same fit are unaffected; check the named parameter's relative standard error before reading its "r,s" value as a finding.

All three blocks are reported on the scale the ESTIMATES are printed on, as nlmixr2est does: structural thetas on the log/optimizer scale, residual error as an SD, and omega as the variance/covariance entries (named ⁠om.<eta>⁠ and ⁠cov.<eta_i>.<eta_j>⁠). The omega block is rotated by the full Jacobian of Omega with respect to the log-Cholesky, which is not diagonal once omega is correlated.

cov_n_sim

Number of MC samples for the covariance (Hessian) step. More samples reduce MC noise in NLL evaluations. The NLL-based Hessian (grad = "none") uses a central second difference of the NLL with the same Sobol sequence (CRN) at every perturbed point, so noise largely cancels and cov_n_sim = 10000 (default) is sufficient for most models.

n_restarts

Number of optimization restarts. Runs in parallel when workers > 1.

restart_sd

Standard deviation of structural theta perturbations for restart initialisation.

workers

Number of parallel workers for multi-restart. 1 (default) runs restarts sequentially. Values ⁠> 1⁠ run the restarts on a pool of background R processes (mirai daemons), which behaves the same way on every platform. Requires the mirai package. Workers are stopped automatically after the restart phase so all cores are available for the Hessian step; if a fit is interrupted, admStopWorkers() cleans up any survivors.

rxControl

rxode2::rxControl() object. Created automatically when NULL.

calcTables, compress, ci, sigdigTable, optExpression, sumProd, literalFix

Passed to nlmixr2est::foceiControl() for the table/output machinery.

sigdig

Significant digits asked of the ODE solver, or NULL (the default) to leave rxode2's own solver tolerances alone. When set, it is passed to rxode2::rxSolve()'s own sigdig argument for every solve the estimator issues – rxode2 owns the mapping to atol/rtol and has changed it between releases, which is why the digits, not the tolerances, are what travels – and to nlmixr2est::foceiControl() for the post-fit tables.

It is a speed lever, and an opt-in one because it is not free. The estimators finite-difference the solve with steps of the same order: grad_h (1e-4), cov_h (1e-3) and cov_h_outer (~2.5e-3), while sigdig = 4 maps to a relative tolerance of ~1e-4 on current rxode2. Differencing a solution whose own noise is 1e-4 with a 1e-4 step returns noise, and it surfaces as a moved objective and an indefinite covariance Hessian (every SE reported NA) rather than as an error. Most worthwhile where the gradient is fully analytic and nothing differences the solve – adfoControl(grad = "analytical") measured ~4.8x faster at sigdig = 4 with standard errors unchanged to 4 significant figures. Elsewhere, compare the objective and the standard errors against NULL before relying on it. Table formatting is unaffected either way: sigdigTable defaults to 4 regardless.

addProp

How combined additive+proportional error is parameterised in the nlmixr2 output tables: "combined2" (default, variance form) or "combined1" (SD form). Has no effect on admixr2's own estimation; passed to nlmixr2est::foceiControl() for the table/output machinery only.

returnAdmr

If TRUE, return a plain list instead of a full nlmixr2 fit object (useful for debugging).

resid_nodes

Gauss-Hermite nodes used to integrate the RESIDUAL for a transform-both-sides endpoint (boxCox, yeoJohnson, logitNorm, probitNorm), where y = g(h(f) + sigma*eps) has no closed-form mean and variance. Ignored by every other error model, which has closed forms. Default 81. Measured worst-case relative error against an independent quadrature, over all four transforms and residual SD of 0.5, 1, 2 and 3: n = 15 gives 5.7e-2, 31 gives 4.5e-3, 81 gives 5.0e-5. The error is dominated by large residual SD; at SD <= 1, n = 31 already gives 1e-7 or better.

This is an ACCURACY dial, not a speed one. The quadrature is linear in resid_nodes in isolation (~50 us at 15, 300 us at 81 for an 8-row study) but negligible beside the ODE solve: a full NLL evaluation measured 0.750 s per 60 evaluations at BOTH 31 and 81 nodes. Raise it if you have a saturating endpoint with a large residual SD; there is little to gain by lowering it.

xtol_rel

Relative parameter tolerance for convergence (default sqrt(.Machine$double.eps)).

...

Additional arguments (none allowed; triggers an error).

Value

An object of class admControl.

Examples

# Minimal control object -- inspect defaults
ctl <- admControl()
ctl$n_sim
ctl$algorithm

# Override key settings without fitting
ctl2 <- admControl(
  n_sim    = 2000L,
  maxeval  = 300L,
  grad     = "fd",
  seed     = 42L
)


library(rxode2)
library(nlmixr2)

data("examplomycin")
obs   <- examplomycin[examplomycin$EVID == 0, ]
obs   <- obs[order(obs$ID, obs$TIME), ]
times <- sort(unique(obs$TIME))
ids   <- unique(obs$ID)
dv_mat <- do.call(rbind, lapply(ids, function(i) {
  sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)]
}))
E <- colMeans(dv_mat)
V <- cov.wt(dv_mat, method = "ML")$cov

pk_model <- function() {
  ini({
    tcl <- log(5);  tv1 <- log(12); tv2 <- log(25)
    tq  <- log(12); tka <- log(1.2)
    prop.sd <- c(0, 0.2)
    eta.cl ~ 0.09; eta.v1 ~ 0.09; eta.v2 ~ 0.09
    eta.q  ~ 0.09; eta.ka ~ 0.09
  })
  model({
    cl <- exp(tcl + eta.cl); v1 <- exp(tv1 + eta.v1)
    v2 <- exp(tv2 + eta.v2); q  <- exp(tq  + eta.q)
    ka <- exp(tka + eta.ka)
    d/dt(depot)      <- -ka * depot
    d/dt(central)    <- ka * depot - (cl/v1 + q/v1) * central + (q/v2) * peripheral
    d/dt(peripheral) <- (q/v1) * central - (q/v2) * peripheral
    cp <- central / v1
    cp ~ prop(prop.sd)
  })
}

fit <- nlmixr2(
  pk_model, admData(), est = "admc",
  control = admControl(
    studies  = list(study1 = list(E = E, V = V, n = length(ids),
                                  times = times, ev = et(amt = 100))),
    n_sim    = 1000L,
    maxeval  = 200L
  )
)
print(fit)

Dummy data frame for nlmixr2 dispatch

Description

Returns a minimal NONMEM-style data frame that satisfies nlmixr2's data argument requirement. The single observation row carries a non-NA placeholder DV (1); the dose row keeps DV = NA. The placeholder is purely for dispatch and output construction and never enters the reported objective – each estimator overwrites fit$env$objective (and OBJF/logLik/AIC/BIC) with its own aggregate -2LL. A non-NA observation is required because nlmixr2's post-fit output construction (nlmixr2CreateOutputFromUi) solves the model over this frame, and rxode2's event-table translation rejects a dataset with no non-NA observation rows ("no rows in event table or input data"), the same reason the multi-endpoint frame below uses a placeholder DV.

Usage

admData(outputs = NULL)

Arguments

outputs

Optional character vector of observed output (endpoint) names for a multi-compartment model with several prediction lines (e.g. c("cp", "cCSF")). One observation row is emitted per endpoint, keyed by name in the DVID column (nlmixr2's endpoint identifier; CMT is NA on those rows), so nlmixr2's data translation recognises every endpoint. These rows carry a non-NA placeholder DV (1) because nlmixr2's multi-endpoint translator rejects an all-NA-DV dataset; the placeholder is purely for dispatch and never enters the reported objective (each estimator overwrites it with its own aggregate -2LL). When NULL (default) the single-endpoint dummy frame is returned unchanged.

Value

A data frame with columns ID, TIME, DV, AMT, EVID, CMT (single-endpoint), plus a DVID endpoint column when outputs is given.

Examples

admData()
admData(c("cp", "cCSF"))

Observed and predicted aggregate moments, tidied

Description

The numbers every diagnostic panel is drawn from, as one row per study and observation time. plot() draws a fixed set of panels; this returns the moments behind them so you can draw your own.

Usage

admMoments(fit, n_sim = NULL, seed = 1L, by = c("source", "stratum"))

Arguments

fit

An admFit object.

n_sim, seed

Simulation size and seed for the predicted moments. Defaults to the fit's own.

by

"source" to collapse a conditional source back together, "stratum" to keep its strata apart.

Value

A data frame with one row per study and observation time.

What the columns mean

obs_sd and pred_sd are the SD of one observation across subjects – not between-subject variability. They carry BSV, residual error, and, wherever a study marginalises a covariate, the spread that covariate induces. struct_sd is the same quantity before residual error is composed on, so pred_sd - struct_sd is what sigma contributes: a predicted spread that misses the observed one can then be attributed. struct_sd is NA for a transforming error model, which moves the mean as well and leaves the pre-sigma variance on a different scale.

z is the mean standardised residual, (obs - pred) / sqrt(pred_var / n).

Sources and strata

stratify cuts a source into one study per covariate level, because a covariate a study marginalises over is not identified against a random effect on the same parameter. Those strata are the unit of the likelihood, not a unit a reader recognises, so by = "source" (the default) puts them back together: the mean is the n-weighted mean, and the variance is the law of total variance – within plus between, the between term being the covariate effect conditioning created.

by = "stratum" returns them separately, which is what you want when the between-level contrast is the thing you are looking at.

Examples

## Not run: 
m <- admMoments(fit)
library(ggplot2)
ggplot(m, aes(pred_sd, obs_sd, colour = study)) +
  geom_abline(slope = 1, intercept = 0) +
  geom_point()

## End(Not run)

Describe the population a study enrolled

Description

Written the way a baseline demographics table reads. Each covariate takes whichever summary the paper printed — mean/sd, median/iqr, a cv as a percent, or a proportion for a binary one — and correlations are given for the PAIRS that were reported, everything else being independent.

Usage

admPopulation(..., cor = NULL, dist = c("lnorm", "normal"), data = NULL)

Arguments

...

Named covariates. A continuous one takes a named vector, e.g. WT = c(mean = 75, sd = 16) or CRCL = c(median = 92, iqr = c(62, 118)). A binary one takes a single named proportion, e.g. SEX = c(male = 0.55), which becomes levels 0/1 with that probability on 1.

cor

Correlations between covariate PAIRS, named A.B, e.g. cor = c(WT.CRCL = 0.45). Pairs not named are independent, so a partial table needs no identity padding. A full matrix is accepted too.

dist

"lnorm" (default) or "normal", for the continuous margins. Lognormal is the usual choice for a positive covariate — a normal margin wide enough to matter puts mass at or below zero, which is NaN inside any power or log term.

data

A data frame of individual covariates to derive the table FROM, instead of typing it out — a digitised baseline listing, or the cohort itself in a simulation study. Each numeric column becomes a margin (a 0/1 column becomes a proportion, everything else a continuous margin matching that column's mean and SD), and every continuous PAIR gets its correlation — taken on the LATENT scale, so on the logs for a lognormal margin, which is the conversion easiest to get wrong by hand. Anything named in ... or cor overrides what the data would have given, so a column you would rather state yourself simply gets stated.

Value

A covariate specification, as covDist() returns.

See Also

admStudy(), which takes one; covDraw() to inspect it.


Stop parallel workers

Description

Stops any worker processes (mirai daemons) started by a parallel-restart fit (admControl(workers = N)). Workers are stopped automatically after the restart phase completes, so this function is only needed if a fit was interrupted before cleanup could run.

Usage

admStopWorkers()

Value

NULL, invisibly.

Examples

# Safe to call at any time; no-op if no workers are running
admStopWorkers()

Collect the studies a meta-analysis draws on

Description

Takes admStudy() objects and names them. Names come from the argument names where given, so admStudies(smith2019, jones2021) labels them by the objects they were built as.

Usage

admStudies(...)

Arguments

...

admStudy() objects, optionally named.

Details

Nothing is generated here either. The studies are materialised once, inside the fit, so the whole specification stays cheap to build and inspect.

Value

An admStudies object, to pass as studies to any of admControl(), adghControl(), adfoControl() or adirmcControl().

See Also

admStudy(), admPopulation().


Write down a published study

Description

One study, transcribed from one paper. It holds what the paper reported and nothing else, is checked as you build it, and prints so the transcription can be read back against the source before anything is fitted.

Usage

admStudy(
  model = NULL,
  est = NULL,
  E = NULL,
  V = NULL,
  sd = NULL,
  sem = NULL,
  n = NULL,
  times = NULL,
  dose = NULL,
  ev = NULL,
  population = NULL,
  at = NULL,
  by = NULL,
  strata_nodes = NULL,
  range = NULL,
  label = NULL,
  v_denom = NULL
)

Arguments

model

The published model, as an nlmixr2-style function (or a parsed rxUi). Omit for a digitised study.

est

Named vector of the paper's parameter estimates, on the scale the model's ini() is written on — so tcl = log(5.2) for a clearance reported as 5.2. Omitted parameters keep the model's own ini() value.

E, V, sd, sem

Digitised aggregate data: the reported mean profile E, with its spread as sd (per timepoint), sem (converted using n), or a full covariance V.

n

Number of subjects the study reports on. Taken from population when that is the cohort's data frame.

times

Observation times.

dose

Dose amount, as shorthand for a single-dose ev.

ev

A dosing event table from rxode2::et(), for anything dose cannot express.

population

The population the study enrolled — see admPopulation(). Covariates the model reads are integrated over it. A data frame of enrolled subjects is accepted directly and read into a table, taking n from its row count. EVERY column that is not a reserved data column (ID, TIME, DV, AMT, ...) becomes a covariate, including ones this model does not read — a covariate another source fitted is still evidence here, and each one adds a quadrature axis, so drop the columns you do not want integrated over.

at

Named list pinning a covariate at a value, for a study reported in one subgroup, e.g. at = list(SEX = 1). A pinned covariate must be omitted from population.

by

Covariate the paper reports results SEPARATELY by, e.g. by = "SEX". Expands a published model into one study per level; digitised subgroup profiles must be supplied as separate studies.

strata_nodes

Quadrature resolution for a conditional CONTINUOUS covariate, i.e. how many nodes it is cut into. Default 9. Raise it for precision, at a multiplicative cost: a source conditional on two continuous covariates becomes strata_nodes^2 studies, and each is solved. Discrete covariates are unaffected — their levels are exact and cost one stratum each. Note that the objective VALUE depends on this, so two fits are only comparable at the same resolution; admixr2 records it per study and anova() checks it.

Where the nodes are cut on the span of the source and analysis models rather than the product grid, two error sources remain and neither is reduced by raising strata_nodes. A range puts the nodes on a fine cloud, which carries a quadrature error of its own, and the recombination that thins that cloud preserves a set of polynomial moments rather than the integrand the objective contains. strata_nodes refines the moment matching against the same cloud. admixr2 sizes and certifies the cloud at admission — against a coarser cloud, and against nonlinear probes the recombination did not match by construction — and keeps the product grid when it cannot. Untruncated margins reproduce the product objective to ~1e-08; a truncated one to ~3e-05.

range

Optional named list giving the covariate span the source ENROLLED, e.g. range = list(WT = c(52, 118)). The declared distribution is truncated to it, whether that covariate ends up conditional (strata are then cut from the truncated margin) or marginal (the truncated margin is integrated over). Needed when that distribution is wider than the enrolment — a ⁠mean +/- SD⁠ transcribed from a baseline table has tails where nobody was — and not wanted when population is the patients themselves, since the margins are then fitted to exactly who was enrolled and truncating would score the paper against a sub-population of its own. An unnamed value is allowed only when the source itself is conditional on exactly one covariate — which covariate an unnamed range belongs to is a question about the source, so the answer does not change with the model being fitted to it.

label

Optional display name; otherwise taken from the argument name in admStudies().

v_denom

Which denominator the supplied spread uses: "unbiased" (n - 1) or "ml" (n). Usually leave it unset — the currency you wrote the study in already says which it is, and admStudy() resolves it and shows the answer when the study is printed.

sd/sem default to "unbiased", because a published spread is the n - 1 one; V defaults to "ml", because handing over a covariance matrix is the deliberate act of someone who computed it, and the likelihood wants the ML denominator. Set it explicitly when your source breaks that pattern — a sd you computed yourself with the ML denominator, say. A published model is always "ml": its moments are generated at that denominator, so "unbiased" is refused rather than applied to a V that is already right.

Details

A study contributes in one of two currencies, and this takes either:

  • a published model — model, with the paper's parameter table as est;

  • digitised aggregate data — E with sd (or sem, or V).

A study generated from a model is not a sample: its mean and covariance are exact functions of that model's parameters, and n sets its RELATIVE WEIGHT against the other studies rather than its precision. No standard error is reported for a fit that includes one — see admStudies().

Nothing is solved here. The study is generated when it reaches the fit, so building one is cheap and a mistake surfaces on print() rather than after a long run.

Value

An admStudy object.

Conditional and marginal covariates are derived, not declared

Whether a covariate is conditional or marginal for a source is a property of that source's own model, so admixr2 works it out and there is nothing to set.

A covariate is conditional when the source's model ESTIMATED its coefficient. That paper reports a contrast along it, so the source is cut into nodes and the contrast is carried into the fit. A covariate the model merely READS is not conditional — weight at a fixed allometric exponent carries no fitted effect to recover, and conditioning on it would buy nodes and no evidence.

A covariate is marginal when the model did not estimate it. There is no contrast in that paper to condition on, and splitting it on a covariate its model never saw would manufacture evidence, so admixr2 integrates over the population instead. That covers both the covariate the model never mentions and the one it reads at an asserted coefficient; print() names them separately, because only the second is easy to mistake for conditional.

Which covariates a source is conditional on comes from that source. Which of them admixr2 has to cut into nodes is narrowed to the ones the analysis model reads, and that narrowing is exact: if the model's prediction does not move across a source's nodes, the mixture those nodes collapse to is a sufficient statistic for it, so the objective is unchanged to eight decimal places while the study count falls. Measured on a source conditional on two covariates with an analysis model reading one, collapsing the unread one moved the objective by 0.00004 and collapsing the read one by 73.6.

This matters and is why it is not left to the caller: a covariate every source marginalises is not identified against a random effect on the same parameter. Measured over 720 replicates, coverage was 0.933 with one source conditional and 0.925 with all of them conditional, against 0.817 with none.

A source given as digitised E/V has no model to derive from and is marginal over whatever it declares. Use one admStudy(..., at = ...) per reported subgroup to enter a paper that published by subgroup.

stratify was removed; covStrata() still takes it for working with a covariate distribution directly. strata_nodes and range remain: the first is a precision setting and the second is a fact about the source, neither of which is a statement about which covariates are conditional.

See Also

admStudies() to collect several, admPopulation() for the baseline table.


Compare nested admixr2 fits by a likelihood-ratio test

Description

The ordinary LRT: the objective difference against a chi-squared reference with Df equal to the number of parameters the larger model adds.

Usage

## S3 method for class 'admFit'
anova(object, ...)

Arguments

object

An admFit.

...

Further admFits to compare it with.

Details

Both fits must come from the same estimator and, for the quadrature estimators, the same node count (n_nodes) or, for the Monte Carlo ones (admc, adirmc), the same sample size (n_sim). Each scores its own approximation to the likelihood, so objectives from different ones are not comparable and the comparison is refused rather than reported.

The stratum resolution is checked per covariate, and only where the two fits overlap. A covariate a model does not read cannot put the two on different scales: if its prediction does not move across a source's nodes, the mixture those nodes collapse to is a sufficient statistic for it, so its objective is the same at either resolution. That is what makes the nested pair of a covariate test comparable — the null model drops the term, so its sources are not cut along it. Two fits that both read a covariate and cut it differently, including one cutting it and the other integrating over it whole, are refused.

Testing a variance AT ZERO puts the null on the boundary of the parameter space, where the exact reference is a chi-bar-squared mixture rather than a chi-squared. The p-value reported there is CONSERVATIVE – too large – so a significant result stays significant, but treat a borderline one with care.

Value

A data frame of class anova.admFit, smallest model first.


Describe the covariate distribution a study's subjects span

Description

Builds the cov_dist a study carries, from what a publication actually reports: a mean and a standard deviation per covariate, and optionally a correlation between them. Validation happens here, where an error can name the covariate, rather than at the first objective evaluation of a fit.

Usage

covDist(..., cor = NULL, joint = NULL, dist = c("normal", "lnorm"))

Arguments

...

One argument per covariate, named as the model reads it. Each is one of:

c(mean = , sd = )

Mean and SD on the covariate's own scale — what a baseline table reports. Normal by default; dist = "lnorm" moment-matches a lognormal margin instead, which is what you want for a covariate that must stay positive (see dist).

c(mu = , sd = ) / c(meanlog = , sdlog = )

A normal or lognormal margin given directly on its own scale.

c(label = prob, ...)

A categorical covariate: names are the level labels, values their proportions. Levels are coded ⁠0, 1, ...⁠ in the order given.

a list(...)

The canonical form, for anything else — including list(quantile = f) for an arbitrary margin.

Alternatively a single data.frame with covariate, mean and sd columns (and optionally dist): a baseline-characteristics table transcribed as-is.

cor

Correlation between the covariates: a scalar for two of them, or a correlation matrix (named, in any order). Realised through a Gaussian copula on the declared margins.

joint

Not accepted yet, and an error if given. It is the place an arbitrary sampler will attach — a function receiving the matrix of uniforms admixr2 supplies and returning one named column per covariate, on each covariate's own scale, which is the shape a vine copula produces — but the contract around it is not settled, so the argument refuses rather than half-works. Use cor for dependence: it builds a Gaussian copula over the declared margins.

dist

Default margin for the c(mean = , sd = ) form: "normal" (default) or "lnorm". A per-covariate dist wins over it.

Choose "lnorm" when the model needs the covariate to stay positive. A normal margin is unbounded below and the quadrature reaches 3.75 standard deviations, so any covariate with a coefficient of variation above about 0.27 (the guard is mu - 3.75 * sd <= 0) gets a node at or below zero, where an allometric or log term is NaN. covDist() warns when that would happen.

Value

A validated cov_dist, ready to pass to a study. Printing it shows each covariate's realised mean, SD and type.

Covariates the model does not read

A distribution may name covariates the analysis model never uses. The prediction cannot depend on them, so integrating over them changes nothing and they are left off the design, with a message naming them. This is what lets a NESTED pair of models share one set of studies: the null model of a covariate test simply omits the term, while the population it was fitted to still describes everyone who was enrolled. Marginalising a covariate out of a correlated specification is exact — every surviving margin keeps the distribution it was given.

A covariate the model does read must still be described, by a cov_dist entry or a fixed cov value, so a mistyped name is reported as the covariate it left undescribed rather than passing unnoticed.

See Also

covDraw() to see what a specification describes, covStrata() to cut it into strata.

Examples

# what a baseline-characteristics table reports
cd <- covDist(WT = c(mean = 72, sd = 16), CRCL = c(mean = 90, sd = 25),
              cor = 0.6)
cd

# a categorical covariate: labels are the levels, values their proportions
covDist(SEX = c(female = 0.55, male = 0.45))

# or transcribe the table itself
covDist(data.frame(covariate = c("WT", "CRCL"),
                   mean = c(72, 90), sd = c(16, 25)))

Draw covariate values from a cov_dist specification

Description

Simulates the covariate values admixr2 itself would use for n subjects, from the same specification a study carries in cov_dist. It is the way to see what a specification actually describes — to check that a reported mean and standard deviation were transcribed correctly, that a correlation points the way round you meant, or that a copula sampler returns what you think it does — before a fit depends on it.

Usage

covDraw(cov_dist, n = 1000L, n_eta = 0L)

Arguments

cov_dist

A covariate specification, as given to a study. Each element names a covariate and describes its distribution, either as mean and sd on the covariate's own scale — NORMAL by default, so pass dist = "lnorm" for the positive margin an allometric term needs (only covDist() defaults to lognormal) — or as a quantile function, or as values (with optional probs) for a discrete covariate. A cor entry — a scalar for two covariates, or a correlation matrix — links them through a Gaussian copula. A joint function takes the matrix of uniforms and returns one column per covariate, which is how an arbitrary copula, an R-vine included, is supplied.

n

Number of subjects to draw.

n_eta

Number of random effects in the model the specification belongs to. The draws are deterministic and come from Sobol dimensions after the random effects', so passing the model's own n_eta reproduces exactly the values a fit would use. The default of 0 is right for inspecting a specification on its own.

Value

A numeric matrix with n rows and one named column per covariate.

Examples

# a baseline-characteristics table, transcribed directly
X <- covDraw(list(WT   = list(mean = 72, sd = 16),
                  CRCL = list(mean = 90, sd = 25),
                  cor  = 0.6), n = 500)
colMeans(X)
stats::cor(log(X[, "WT"]), log(X[, "CRCL"]))

Cut a covariate distribution into strata

Description

Shows the strata datagen() builds for a study declaring stratify — the covariate value each stratum is pinned at, its effective sample size, and the distribution the remaining covariates follow within it. It is the way to see what a stratification actually describes before a fit depends on it.

Usage

covStrata(
  cov_dist,
  stratify,
  n_nodes = .ADM_STRATA_NODES,
  n = 1,
  n_pool = 32768L,
  cov_range = NULL
)

Arguments

cov_dist

A covariate specification, as given to a study — see covDraw() for the full grammar.

stratify

Character vector naming the covariates to stratify on. The rest are left in each stratum's own cov_dist, to be marginalised over their distribution conditional on that stratum.

n_nodes

Strata per stratified covariate (default 9). A discrete covariate ignores it and is cut at its levels, exactly, with the level probabilities as weights.

For a continuous one it sets how finely the covariate is resolved, and the rule depends on what is known about the joint distribution. Where the latent structure is known — an explicit cor, or no declared dependence, which IS independence — each stratum is a Gauss-Hermite node, held at a POINT, and the remaining covariates follow their exact conditional law within it. An opaque joint sampler cannot be conditioned, and there the covariate is cut into that many equiprobable BINS instead, each described by the pool members that landed in it.

This is a convergence parameter, not a modelling choice. The well-defined object is the limit as the count grows; any finite value approximates it, and under misspecification the answer can jump between basins rather than drift. Raise it until the estimates stop moving rather than picking a value you like. Cost is n_nodes^p studies for p stratified covariates, so this is cheap in one covariate and expensive in three.

How much the objective depends on it turns on which rule applies. Where the latent structure is known — an explicit cor, or no declared dependence, which is independence — conditioning uses a Gauss-Hermite grid and the objective is stable: 0.03 units across counts of 5 to 100, so objective, AIC, BIC and likelihood ratios are comparable across resolutions.

A declared discrete covariate no longer forces the slow rule: as long as it is latently independent of the others it enumerates exactly at its levels, which took a study declaring one sex covariate from 515 units of movement, still rising at a count of 50, to 0.009.

An opaque joint sampler cannot be conditioned and falls back to equiprobable bins, where it is not stable: 51 units at a count of 100 and still moving, with the estimate itself wandering (0.681 at 5, 9, 15 and 100; 0.697 at 50). There, raise the count until the estimates settle, and note that anova() refuses to compare two fits built at different ones.

n

Total sample size to divide among the strata. The default of 1 returns the raw stratum weights.

n_pool

Minimum size of the deterministic pool the strata are cut from. Each stratum's covariate distribution is that pool restricted to its own bin, so this sets how finely the within-stratum distribution is resolved — and, because no stratum can produce a covariate value beyond the pool's extremes, how far into the tails it reaches. The pool is grown automatically with the number of strata, and a stratum left with too few draws is an error rather than a silently coarse answer; n_pool_cell in the result reports what each one got.

It applies only where a pool is used at all: an opaque joint sampler, or a stratified covariate latently correlated with a marginalised one. Where the conditional law is closed form the stratum carries the DECLARED specs themselves, no sample stands in for them, and n_pool_cell is NA.

cov_range

Optional named list giving the range each stratified covariate was actually ENROLLED over, e.g. list(WT = c(52, 118)) — publications routinely report a min-max or a median with an IQR.

Without it the strata are cut over the whole declared distribution, and a source is credited with evidence at covariate values it never sampled. The overstatement is exactly var(declared) / var(enrolled): 3.4x for a source spanning ±1 SD, 12.4x at ±0.5 SD. Estimates stay correct — what inflates is confidence, and it only shows once a second source disagrees. Omitting it warns for that reason.

More strata resolve the covariate range more finely but do not buy accuracy, and each one costs a solve: on a matched one-covariate fit the coefficient came back at 0.7000 / 0.7002 / 0.7005 / 0.7005 for 3 / 4 / 10 / 16 strata against a true 0.700, while the fit took 7.2 / 4.1 / 8.0 / 11.5 seconds. Raise it when the covariate effect is strongly nonlinear over the range; the only hard limit is that a stratum must keep enough pooled draws to describe itself, which admixr2 checks.

Details

Stratify on the covariates a source's own published model conditions on, and leave the rest to be marginalised. A source that never fitted a covariate has no contrast in it to report, and evaluating it at nodes that vary that covariate manufactures a null one — measured, with no sampling noise, as an attenuation from 0.450 to 0.211 in the fitted coefficient.

Value

A list with one element per stratum, each containing cov (the value each stratified covariate is held at — a quadrature node, a level, or a bin mean), cov_dist (the distribution of ALL covariates within that stratum, carrying the full joint dependence), n and weight.

See Also

covDraw() to inspect a covariate specification, datagen() to generate the strata as studies.

Examples

# a source that fitted weight but not renal function, the two correlated
st <- covStrata(list(WT   = list(mean = 72, sd = 15),
                     CRCL = list(mean = 90, sd = 22), cor = 0.7),
                stratify = "WT", n_nodes = 4L, n = 300)
vapply(st, function(s) s$cov$WT, numeric(1))     # where each stratum sits
vapply(st, function(s) s$n, numeric(1))          # and how big it is

# within a stratum, CRCL follows its CONDITIONAL distribution: a heavier
# stratum has a higher creatinine clearance, which is the whole point
vapply(st, function(s) mean(covDraw(s$cov_dist, n = 2000L)[, "CRCL"]),
       numeric(1))

Generate aggregate study data from (possibly different) pharmacometric models

Description

Generates population mean vectors (E) and covariance matrices (V) for each study by integrating over the IIV distribution – either by Monte Carlo (the default) or by a deterministic First-Order expansion (method = "fo", see datagenControl()). Each study may specify its own PK/PD model (as would be the case when digitising data from several published studies, each fit with a different structural model). True parameter values are taken from the ini() block of each study's model. Each element of the returned list is ready to supply directly to admControl(studies = ...).

Usage

datagen(studies, model = NULL, control = datagenControl())

Arguments

studies

A named list of study specifications. Each element is a list with:

model

An nlmixr2-style model function with ini() and model() blocks. Serves as the data-generating model for this study. May differ between studies. Can be omitted if a top-level default is supplied via the model argument.

times

Numeric vector of observation times.

ev

A dosing event table created with rxode2::et().

n

(Optional) integer sample size; stored as metadata and used when supplying the result to admControl().

cov_dist

(Optional) the covariate distribution this study's subjects span — see covDraw() for the grammar. The generated E/V are MARGINAL over it, which is what a publication reports. Needs datagenControl(method = "mc") or "gh"; "fo" integrates over the random effects only and is refused. Prefer "gh", which adds no Monte Carlo noise to data that is meant to BE the reference.

stratify

(Optional) TRUE to stratify on every covariate this study's OWN data-generating model ESTIMATED a coefficient for, marginalising the rest — the split is read from the model, so it cannot disagree with it. Two sources sharing one cov_dist therefore stratify differently, each according to what it fitted. A covariate the model merely READS — weight at a fixed allometric exponent — is refused: there is no fitted effect to recover there. A character vector names the covariates explicitly instead, and is taken at its word. NOTHING IS DERIVED WITHOUT THIS: a study that asks for no stratification is generated MARGINAL over whatever it declared, which is what a publication reports. The study is expanded into one ordinary study per covariate stratum, named ⁠<study>_s1⁠, ⁠<study>_s2⁠, ..., each pinned at its own covariate value, carrying its own effective size n_k (the quadrature weight times n, summing to n), and marginalising the remaining covariates over their distribution CONDITIONAL on that stratum. Use it when the source reports — or, being a published model, can report — summaries by covariate subgroup. Stratify only on what the source actually fitted: a source with no term in a covariate has no contrast in it to give, and nodes that vary it manufacture a null one. See covStrata().

strata_nodes

(Optional) strata per stratified covariate (default 9); a discrete covariate is cut at its levels instead. Each stratum costs a solve and more of them do not buy accuracy: a matched one-covariate fit recovers 0.7000 / 0.7002 / 0.7005 at 3 / 4 / 10 strata against a true 0.700. See covStrata().

observations

(Optional) a named list to generate data for several observed outputs (multi-compartment). Each entry gives one output's output (model prediction variable, e.g. "cp"), times, and optionally ev/n (inherited from the study otherwise). When present, the study result carries a matching observations list of per-output E/V, ready to pass straight to admControl(studies = ...).

model

Optional default model function used for any study that does not supply its own model element. At least one of model or each study's model must be non-NULL.

control

A datagenControl() object.

Details

With control = datagenControl(method = "mc") (the default) population moments are computed via the same Monte Carlo engine as est = "admc":

Et=fˉs(θ^s,ηi,t)E_t = \bar{f}_s(\hat\theta_s, \eta_i, t)

Vts=Cov^η[fs,t,fs,s′]+ΣsV_{ts} = \widehat{\mathrm{Cov}}_\eta[f_{s,t}, f_{s,s'}] + \Sigma_s

where fsf_s and θ^s\hat\theta_s are the model and initial estimates from the ini() block of study ss, the sample covariance uses the ML denominator n_sim, and Σs\Sigma_s is diagonal with entries determined by that study model's residual error type (additive, proportional, or log-normal).

With method = "fo" the moments are instead the deterministic First-Order expansion used by est = "adfo":

E=fs(θ^s,0)E = f_s(\hat\theta_s, 0)

V=JΩsJ⊤+Σs,Jtj=∂fs,t/∂ηj∣η=0V = J \Omega_s J^\top + \Sigma_s, \quad J_{tj} = \partial f_{s,t}/\partial \eta_j |_{\eta = 0}

with the Jacobian JJ obtained from the sensitivity model (or finite differences if that is unavailable). This is the natural choice for design evaluation and optimal design: the moments are fast and reproducible, and because the data-generating and data-analytic models coincide, the FO Hessian of the log-likelihood (the expected information matrix) is evaluated at the true maximum rather than at a point that is not an MLE of the generated data. Note est = "adfo" always adds Σ\Sigma to its predicted covariance, so for a consistent FIM keep the residual error in the generating model; omit it only when residual-free (IIV-only) moments are genuinely what you want.

With method = "gh" the moments are computed by deterministic Gauss-Hermite quadrature over the random-effects prior η∼N(0,Ω)\eta \sim N(0, \Omega):

E=∑qwqf(θ^,ηq),V=∑qwq(fq−E)(fq−E)⊤+ΣE = \sum_q w_q f(\hat\theta, \eta_q), \quad V = \sum_q w_q (f_q - E)(f_q - E)^\top + \Sigma

where (ηq,wq)(\eta_q, w_q) are the Cholesky-scaled tensor-product GH nodes and weights. Unlike FO this is unbiased at any IIV magnitude; unlike MC the result is noise-free and exactly reproducible. Matching the moments of est = "adgh" makes method = "gh" the natural choice for optimal design with that estimator.

Models are compiled and cached on first use (keyed by model expression digest), so repeated calls or multiple studies sharing the same model incur only a single compilation.

Value

A named list with one element per study. Each element contains:

E

Population mean vector at times.

V

Population covariance matrix (length(times) x length(times); ML denominator n_sim for method = "mc", the analytical FO covariance for method = "fo", or the GH weighted covariance for method = "gh"). The diagonal carries the model's residual-error variance; to generate residual-free (IIV-only) moments, omit the error term from the model.

n

Sample size (NA_integer_ if not supplied).

times

Observation times.

ev

Dosing event table.

samples

Raw ⁠n_sim x length(times)⁠ prediction matrix (only when control$return_samples = TRUE).

See Also

datagenControl(), admControl()

Examples

library(rxode2)

pk_model <- function() {
  ini({
    tcl <- log(5); tv <- log(30)
    prop.sd <- c(0, 0.2)
    eta.cl ~ 0.09; eta.v ~ 0.04
  })
  model({
    cl <- exp(tcl + eta.cl)
    v  <- exp(tv  + eta.v)
    d/dt(central) <- -(cl/v) * central
    cp <- central / v
    cp ~ prop(prop.sd)
  })
}

study_data <- datagen(
  studies = list(
    study1 = list(times = c(1, 2, 4, 8, 12, 24),
                  ev = rxode2::et(amt = 100), n = 200L)
  ),
  model   = pk_model,
  control = datagenControl(n_sim = 2000L)
)

# E and V plug directly into admControl(studies = ...)
round(study_data$study1$E, 2)

Control parameters for datagen()

Description

Control parameters for datagen()

Usage

datagenControl(
  method = c("mc", "fo", "gh"),
  n_sim = 5000L,
  n_nodes = 5L,
  sampling = c("sobol", "halton", "torus", "lhs", "rnorm"),
  seed = 12345L,
  cores = 1L,
  return_samples = FALSE,
  resid_nodes = 81L,
  cov_nodes = 7L
)

Arguments

method

Moment approximation used to generate E and V: "mc" (default) draws Monte Carlo samples over the IIV distribution, as in est = "admc"; "fo" uses the deterministic First-Order expansion (mu = f(theta, 0), ⁠V = J Omega J' + Sigma⁠), matching est = "adfo"; "gh" uses deterministic Gauss-Hermite quadrature over the random-effects prior, matching est = "adgh" – unbiased at any IIV magnitude and noise-free. Use "fo" or "gh" for design evaluation where the data-generating and data-analytic models must coincide.

n_sim

Number of Monte Carlo samples used to approximate population moments. Ignored when method = "fo" or "gh".

n_nodes

Number of Gauss-Hermite nodes per eta dimension for method = "gh" (default 5). Total nodes = n_nodes^n_eta. Ignored for "mc" and "fo".

sampling

Quasi-random sampling method: "sobol" (default), "halton", "torus", "lhs", or "rnorm". Ignored when method = "fo" or "gh".

seed

Integer seed. Applied before stochastic methods ("rnorm", "lhs"). Ignored when method = "fo" or "gh".

cores

Number of rxSolve threads.

return_samples

Include the raw ⁠n_sim x length(times)⁠ prediction matrix as ⁠$samples⁠ in each study's output. No effect when method = "fo" or "gh" (those methods draw no samples).

resid_nodes

Gauss-Hermite nodes used to integrate the RESIDUAL for a transform-both-sides endpoint (boxCox, yeoJohnson, logitNorm, probitNorm), where y = g(h(f) + sigma*eps) has no closed-form mean and variance. Ignored by every other error model. Default 81 – the same default the four estimator controls use, so datagen() and the fit it feeds agree unless you deliberately change one of them. See admControl() for the measured convergence.

cov_nodes

Gauss-Hermite nodes per covariate when method = "gh" integrates a study's cov_dist (default 7). Total covariate points are cov_nodes^p for p covariates. Ignored by "mc", which draws a covariate value per simulated subject instead, and by "fo", which cannot integrate a covariate at all.

Value

A list of class "datagenControl".

See Also

datagen()

Examples

ctrl <- datagenControl(n_sim = 2000L)
ctrl$sampling  # "sobol"

# Deterministic FO moments for design evaluation:
datagenControl(method = "fo")$method  # "fo"

# GH quadrature moments (unbiased, noise-free):
datagenControl(method = "gh", n_nodes = 5L)$n_nodes

Examplomycin dataset

Description

A simulated pharmacokinetic dataset for the fictional drug examplomycin, intended as a worked example for aggregate data modelling with admixr2. The dataset contains 500 subjects, each with 9 observation time points, generated from a two-compartment model with first-order absorption.

Usage

examplomycin

Format

A data frame with 5000 rows and 6 columns:

  • ID: Subject identifier (integer, 1–500).

  • TIME: Time after dose (hours).

  • DV: Observed plasma concentration (mg/L).

  • AMT: Dose amount (mg); 100 for dosing records, 0 otherwise.

  • EVID: Event type (101 = dose, 0 = observation).

  • CMT: Compartment (1 = depot, 2 = central).

Details

True population parameters:

Parameter Value
CL (L/hr) 5
V1 (L) 10
V2 (L) 30
Q (L/hr) 10
ka (1/hr) 1
IIV (all, SD on log scale) 0.3
Proportional error (SD) 0.2

Single oral dose of 100 mg; sampling at 0.1, 0.25, 0.5, 1, 2, 3, 5, 8, and 12 hours post-dose.

Source

Generated from a two-compartment PK model using rxode2::rxSolve(). See vignette("admixr2") for a full modelling example.

Examples

data("examplomycin")
head(examplomycin)

# Compute aggregate statistics
obs <- examplomycin[examplomycin$EVID == 0, ]
obs <- obs[order(obs$ID, obs$TIME), ]
times <- sort(unique(obs$TIME))
E <- sapply(times, function(t) mean(obs$DV[obs$TIME == t]))
round(E, 3)

Fit an aggregate data model via First-Order (FO) approximation

Description

Called automatically by nlmixr2(model, admData(), est = "adfo", control = adfoControl(...)). Not typically called directly.

Usage

## S3 method for class 'adfo'
nlmixr2Est(env, ...)

Arguments

env

nlmixr2 environment containing ui and control.

...

Unused.

Value

An admFit nlmixr2 fit object.


Fit an aggregate data model via Gauss-Hermite quadrature

Description

Called automatically by nlmixr2(model, admData(), est = "adgh", control = adghControl(...)). Not typically called directly.

Usage

## S3 method for class 'adgh'
nlmixr2Est(env, ...)

Arguments

env

nlmixr2 environment containing ui and control.

...

Unused.

Value

An admFit nlmixr2 fit object.


Fit an aggregate data model via Iterative Reweighting MC (adirmc estimator)

Description

Called automatically by nlmixr2(model, admData(), est = "adirmc", control = adirmcControl(...)). Not typically called directly.

Usage

## S3 method for class 'adirmc'
nlmixr2Est(env, ...)

Arguments

env

nlmixr2 environment containing ui and control.

...

Unused.

Value

An admFit nlmixr2 fit object.


Fit an aggregate data model via Monte Carlo (admc estimator)

Description

Called automatically by nlmixr2(model, admData(), est = "admc", control = admControl(...)). Not typically called directly.

Usage

## S3 method for class 'admc'
nlmixr2Est(env, ...)

Arguments

env

nlmixr2 environment containing ui and control.

...

Unused.

Value

An admFit nlmixr2 fit object.


Diagnostic plots for an admixr2 fit

Description

Generates up to five diagnostic panels:

Usage

## S3 method for class 'admFit'
plot(
  x,
  which = c("mean", "cov", "covariate", "nll", "par"),
  n_sim = NULL,
  seed = 1L,
  ...
)

Arguments

x

An admFit object returned by nlmixr2() with est = "adfo", est = "admc", est = "adgh", or est = "adirmc".

which

Character vector selecting which panel types to produce. Any subset of c("mean", "cov", "covariate", "nll", "par"). Defaults to all five. Note "cov" (predicted vs observed covariance) and "covariate" (covariate effect) are different panels.

n_sim

Number of MC samples for the final prediction. Defaults to the value used during fitting. Only used when "mean", "cov" or "covariate" is in which.

seed

Random seed for reproducibility.

...

Unused.

Details

  1. "mean" – Observed vs predicted mean per study (2x2 grid). Upper row: observed and predicted mean lines with +/-1 SD ribbon on a shared y scale (black throughout). Lower row: raw residual lollipop with +/-2 SE band and standardised residual z-scores with +/-1.96 reference lines.

  2. "cov" – Observed vs predicted (co)variance heatmaps per study (2x2 grid). Upper row shares a common colour scale (blue-white-red). Lower row uses distinct diverging scales: residual (red-white-green) and standardised residual (gold-white-purple). Significance stars overlaid on the standardised residual panel.

  3. "covariate" – Two BETWEEN-study covariate panels, one facet per covariate any source describes – including one the model does not read, which is exactly the case covariate_resid exists for. covariate_effect sweeps each covariate across the range the sources between them cover and draws every model parameter that reads it, at the fitted thetas, as ONE line per parameter with the other covariates held at the pooled centre; the region no source sampled is shaded as extrapolation, and a discrete covariate is drawn on its levels rather than swept through the values between them. covariate_resid plots each study's mean standardised residual against the covariate value it sits at, with an lm trend on a continuous axis: a slope there is a mis-specified covariate form. Produced only for a fit whose studies declare covariates, so it is silently absent otherwise.

  4. "nll" – NLL trace per restart over optimizer evaluations. Restarts coloured with the Okabe-Ito palette.

  5. "par" – Parameter trace per restart on the natural scale (struct thetas back-transformed, sigma as SD, omega diagonal as variance labelled V(eta.x)). Facets ordered as in the model ini() block. Restarts coloured with the Okabe-Ito palette.

Value

A named list of ggplot2 objects, invisibly. Prints each selected top-level panel. For the "mean" and "cov" panels the returned list also contains each sub-panel individually so a single panel (or a few) can be extracted in code without reprinting the whole grid. Elements can be pulled out by name – plot(fit, which = "mean")$mean_study1_pred or plot(fit, which = "cov")$cov_study1_std_resid – or by position, with the combined 2x2 grid stored first per study (plot(fit, which = "mean")[[1]] is the full grid, ⁠[1]⁠ the length-1 named sub-list). The sub-panel keys are ⁠<type>_<source>_obs⁠, ⁠_pred⁠, ⁠_resid⁠, and ⁠_std_resid⁠; the combined grid stays under ⁠<type>_<source>⁠. ⁠<source>⁠ is the study name, and for a source cut into nodes it is the name of the SOURCE rather than of a node – these panels are about the paper, so the nodes are put back together first and there is no mean_study1_s1. The extra sub-panel keys are not printed on their own.

Why the covariate panels are separate

With aggregate data a covariate effect is identified BETWEEN studies: the renal exponent in vignette("covariates") is recovered from three cohorts sitting at three different creatinine-clearance medians, none of which fitted a renal term at all. The "mean" and "cov" panels are per study and in isolation, so every source can sit beautifully on its own panel while the relation tying them together is wrong. That contrast is what "covariate" draws.

Marginal and conditioned sources

Both panels distinguish how each study enters a covariate, because the two are different statements about the source rather than degrees of the same one. A marginalised covariate – one the estimator integrates over a declared distribution – is drawn as a round point at the study's median with a 10th-90th percentile bar and a 2.5th-97.5th whisker: the source constrains the effect through the spread it induces, and the whole span is what it speaks for. A conditional covariate – cut into nodes, or at or by, so the model is solved at one value – is drawn as a diamond at that value with no spread, because it has none; what that source reports along it is a relationship, drawn as its own regression over the range it covers.

A SOURCE IS ONE MARK, not one per stratum. Conditioning cuts a source into one study per quadrature node, and a node is an internal discretisation of the very distribution the source already stands for: drawn straight it became nine small dots for one paper, each carrying a ninth of its patients, with the outermost node setting the axis. On a LEVEL axis the positions are values the paper actually reported, so a source appears once per level.

Reading covariate_resid

A continuous covariate is read as a trend: the dashed lm across studies, where a slope is a mis-specified covariate form.

A discrete one is read as a contrast instead, and is drawn that way. Its axis is ticked at its levels and nowhere else, no regression is fitted through it – a line across the levels of a factor reports as a slope what is a difference between groups – and the strata stratify cut from one source are joined, because that pairing is the evidence conditioning creates: the same study, one covariate moved. Several sources tilting the same way is the mis-specification. A regression over the pooled cloud of strata cannot show it, since it averages the pairs away.

With few sources, covariates whose study medians happen to move together cannot be told apart here – three cohorts whose weights and renal function both decline will show a slope in both facets whichever one is mis-specified. The panel localises the problem to a set of covariates, not to one; separating them needs a source that breaks the pattern.

Aggregate data slot

Every admixr2 fit also carries the observed and predicted aggregate data in fit$env$aggData, a named list with one entry per study. Each entry holds the observation times, the study n, and two moment sets – obs (from the data) and pred (predicted at the fitted parameters) – each a list with the mean vector E and the (co)variance matrix V:

  fit$env$aggData$study1$obs$E    # observed mean vector
  fit$env$aggData$study1$obs$V    # observed covariance matrix
  fit$env$aggData$study1$pred$E   # predicted mean vector
  fit$env$aggData$study1$pred$V   # predicted covariance matrix

The predicted moments are computed by one MC simulation at the fitted parameters using the fit's own n_sim and a fixed seed. The slot is absent only when the fit cannot be simulated (no simulation model available).

It is per study, which for a source cut into nodes means per node – where the "mean" and "cov" panels are per SOURCE, the nodes put back together by the mixture law. So the two no longer line up entry for entry on such a fit, and the panel keys are ⁠mean_<source>⁠ rather than ⁠mean_<source>_s1⁠. admMoments() returns whichever of the two you want, tidied: by = "source" matches the panels, by = "stratum" matches this slot.

nlmixr2 traceplot()

admixr2 fits also plug into the nlmixr2 traceplot() generic. During fitting the parameter iteration history of the best restart is stored on the fit in the standard parHistData slot (natural scale), so traceplot(fit) produces the familiar per-parameter, free-y facetted trace used elsewhere in the nlmixr2 ecosystem. There is no burn-in marker (admixr2 records optimizer evaluations, not SAEM iterations), and only the best restart is shown – the per-restart overlay and the NLL trace remain available via plot(fit, which = c("par", "nll")). The trace stores only improving evaluations (steps that lowered the best NLL), so the iter axis indexes those improvement steps rather than raw optimizer iterations.

Examples

library(rxode2)
library(nlmixr2)

data("examplomycin")
obs    <- examplomycin[examplomycin$EVID == 0, ]
obs    <- obs[order(obs$ID, obs$TIME), ]
times  <- sort(unique(obs$TIME))
ids    <- unique(obs$ID)
dv_mat <- do.call(rbind, lapply(ids, function(i) {
  sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)]
}))
E <- colMeans(dv_mat)
V <- cov.wt(dv_mat, method = "ML")$cov

pk_model <- function() {
  ini({
    tcl <- log(5); tv <- log(30)
    prop.sd <- c(0, 0.2)
    eta.cl ~ 0.09; eta.v ~ 0.04
  })
  model({
    cl <- exp(tcl + eta.cl)
    v  <- exp(tv  + eta.v)
    d/dt(central) <- -(cl/v) * central
    cp <- central / v
    cp ~ prop(prop.sd)
  })
}

fit <- nlmixr2(
  pk_model, admData(), est = "adfo",
  control = adfoControl(
    studies = list(study1 = list(E = E, V = V, n = length(ids),
                                 times = times, ev = et(amt = 100))),
    maxeval = 100L
  )
)
plot(fit)

Print method for admFit objects

Description

Delegates to print.nlmixr2FitCore for the standard nlmixr2 coloured output. admFit class is kept on the object during the call so that head.admFit intercepts any head(fit) calls that arise in the paged- output path (R Markdown / notebooks), preventing the ⁠[.data.frame(.subset2(env, integer))⁠ crash that occurs when an environment-backed fit is subscripted like a plain list.

Usage

## S3 method for class 'admFit'
print(x, ...)

Arguments

x

An admFit object.

...

Passed to print.nlmixr2FitCore.

Value

x, invisibly.

Examples

library(rxode2)
library(nlmixr2)

data("examplomycin")
obs    <- examplomycin[examplomycin$EVID == 0, ]
obs    <- obs[order(obs$ID, obs$TIME), ]
times  <- sort(unique(obs$TIME))
ids    <- unique(obs$ID)
dv_mat <- do.call(rbind, lapply(ids, function(i) {
  sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)]
}))
E <- colMeans(dv_mat)
V <- cov.wt(dv_mat, method = "ML")$cov

pk_model <- function() {
  ini({
    tcl <- log(5); tv <- log(30)
    prop.sd <- c(0, 0.2)
    eta.cl ~ 0.09; eta.v ~ 0.04
  })
  model({
    cl <- exp(tcl + eta.cl)
    v  <- exp(tv  + eta.v)
    d/dt(central) <- -(cl/v) * central
    cp <- central / v
    cp ~ prop(prop.sd)
  })
}

fit <- nlmixr2(
  pk_model, admData(), est = "adfo",
  control = adfoControl(
    studies = list(study1 = list(E = E, V = V, n = length(ids),
                                 times = times, ev = et(amt = 100))),
    maxeval = 100L
  )
)
print(fit)