R/foceiControl.R

Defines functions rxUiDeparse.foceiControl .rxUiDeparseFoceiControl foceiControl .foceiAssertHessianMethod

Documented in foceiControl

# innerOpt= levels.  "auto" is resolved in C++ (foceiSetup_) once
# needOptimHess is known: n1qn1 for a generalized-likelihood endpoint, trust
# otherwise.  See the innerOpt @param below.
.innerOptFun <- c("n1qn1" = 1L, "BFGS" = 2L, "trust" = 3L, "auto" = 4L)

# hessianMethod= levels.  Anything but "fd" is only consulted under
# innerOpt="trust" (hessianQNEligible, src/inner.cpp).
.hessianMethodIdx <- c("fd" = 1L, "bfgs" = 2L, "sr1" = 3L, "bofill" = 4L)

#' Refuse a hessianMethod no inner optimizer would consult.
#'
#' `innerOpt` must already be resolved: foceiControl() passes `4L` ("auto")
#' through untouched because needOptimHess is not known there yet, and
#' R/focei.R re-checks with the value it resolves to.
#' @noRd
.foceiAssertHessianMethod <- function(hessianMethod, innerOpt, note = "") {
  if (hessianMethod == 1L || innerOpt == 3L || innerOpt == 4L) {
    return(invisible(TRUE))
  }
  stop(
    "hessianMethod = \"",
    names(.hessianMethodIdx)[hessianMethod],
    "\" requires innerOpt = \"trust\"",
    note,
    call. = FALSE
  )
}

.foceiControlInternal <- c(
  "genRxControl",
  "resetEtaSize",
  "foceType",
  "resetThetaSize",
  "resetThetaFinalSize",
  "outerOptFun",
  "outerOptTxt",
  "skipCov",
  "foceiMuRef",
  "foceiMuCovEta",
  "predNeq",
  "nfixed",
  "nomega",
  "neta",
  "ntheta",
  "nF",
  "printTop",
  "needOptimHess",
  "iterPrintControl",
  "est",
  "foceiMuModel",
  "foceiMuGroupTheta",
  "foceiMuGroupEta",
  "foceiMuGroupCovStart",
  "foceiMuGroupCovCount",
  "foceiMuGroupCovTheta",
  "foceiMuGroupCovUserFixed",
  "foceiMuGroupThetaLower",
  "foceiMuGroupThetaUpper",
  "foceiMuGroupCovLower",
  "foceiMuGroupCovUpper",
  "foceiMuGroupCovData",
  "foceiMuGroupTol",
  "foceiMuGroupMaxCycles",
  "foceiMuGroupClampRetries",
  # derived from covMethod ("analytic" vs the finite-difference
  # formulas); kept internal so a built control round-trips.
  "covType",
  # foreign covariance ("sa"/"imp") deferred to a post-fit
  # recompute; internal so a built control round-trips.
  "covMethodDeferred",
  # subject-constant covariates stashed by .foceiFamilyReturn
  # for the analytic covariate-coefficient reuse; internal so
  # a built control round-trips (e.g. posthoc re-validation).
  "foceiConstCovs",
  # TRUE when the outer optimizer was defaulted (not user
  # specified); lets *f wrappers re-default under fast=TRUE
  "outerOptDefault",
  # the built rx_prior_spec_t* external pointer
  # (.nlmixr2BuildPriorSpec(), R/priors.R); not a formal
  # argument, not deparseable/comparable, and rebuilt fresh
  # every fit -- internal so a built control round-trips.
  "priorSpec"
)

#' Control Options for FOCEi
#'
#' @param sigdig Optimization significant digits.  One value drives, with a single
#'   consistent formula, the inner/outer optimizer convergence tolerance
#'   (\code{10^-sigdig}), the boundary check tolerance (\code{5*10^(-sigdig+1)}),
#'   and the ODE solver tolerances: the \code{rtol} exponent IS \code{sigdig} and
#'   \code{atol} sits three orders below, so \code{rtol = 10^-sigdig},
#'   \code{atol = 10^(-sigdig-3)} for every solver (stiff, non-stiff or
#'   auto-switching).  The sensitivity (\code{atolSens}/\code{rtolSens})
#'   tolerances match the main solve (the outer gradient and covariance are built
#'   from them); the steady-state (\code{ssAtol}/\code{ssRtol}) tolerances run one
#'   order looser.
#'   Keying the optimizer to the same \code{10^-sigdig} means it converges to
#'   exactly the precision the solve supports.  At the default \code{sigdig = 3}
#'   this is \code{atol = 1e-6}, \code{rtol = 1e-3}.
#'
#' @param sigdigTable Significant digits in the final output table.
#'   If not specified (`NULL`), it defaults to `sigdig`.
#'
#' @param epsilon Precision of estimate for n1qn1 optimization.
#'
#' @inheritParams iterPrintParams
#'
#' @param scaleTo Scale the initial parameter estimate to this value.
#'     By default this is 1.  When zero or below, no scaling is performed.
#'
#' @param scaleObjective Scale the initial objective function to this
#'     value.  By default this is 0 (meaning do not scale)
#'
#' @param derivEps Forward difference tolerances (relative, absolute); step
#'     size \code{h = abs(x)*derivEps[1] + derivEps[2]}.
#'
#' @param derivMethod Derivative method for the outer problem: "switch",
#'     "central", or "forward". "switch" starts forward and toggles to
#'     central when \code{abs(delta(OFV)) <= derivSwitchTol}.
#'
#' @param derivSwitchTol The tolerance to switch forward to central
#'     differences.
#'
#' @param covDerivMethod indicates the method for calculating the
#'     derivatives while calculating the covariance components
#'     (Hessian and S).
#'
#' @param covMethod Method for calculating the covariance.  \code{"r,s"} (the
#'     default) is the sandwich estimator (see below).  \code{"analytic"}
#'     uses the exact analytic observed-information R-matrix (reported as
#'     \eqn{R^{-1}}) and additionally returns the residual and \code{Omega} standard
#'     errors; it covers FOCEI/FOCE fits with additive, proportional, or combined
#'     error, mu-referenced/covariate/other structural parameters (and
#'     non-mu-referenced etas), and SD-scale inter-occasion variability, and emits a
#'     message and falls back to the finite-difference Hessian for anything out of
#'     scope (FO, \code{nAGQ > 1}, censoring, DV-transformed error, bounded-parameter
#'     transforms, a structural theta shared by two etas, non-SD \code{iovXform}, or a
#'     pure-proportional variance that vanishes at a near-zero prediction).  The
#'     finite-difference methods use R (the Hessian) and S (the sum of individual
#'     gradient cross-products at the empirical Bayes estimates): \code{"r,s"} sandwich
#'     (\code{solve(R)\%*\%S\%*\%solve(R)}), \code{"r"} Hessian-based
#'     (\code{solve(R)}), \code{"s"} cross-product-based (\code{solve(S)}), or
#'     \code{""} to skip the covariance step.  \code{"sa"} (SAEM Louis
#'     stochastic-approximation FIM) and \code{"imp"} (importance-sampling
#'     Monte-Carlo observed information) are also accepted for any method; they
#'     are computed post-fit at the converged estimates by the decoupled
#'     recompute engine.
#'
#' @param covSolveTol absolute/relative ODE tolerance for the covariance solves --
#'     the augmented-sensitivity solves behind \code{covMethod="analytic"} and the
#'     perturbed solves behind the finite-difference methods.  \code{NULL} (default)
#'     derives a tight tolerance from \code{sigdig}; supply a number to override it.
#'
#' @param covFull shape of \code{fit$cov}.  \code{TRUE} (default) installs the
#'     full theta + residual sigma + Omega covariance (assembled analytically for
#'     \code{covMethod="analytic"}, or by central finite differences over the same
#'     parameter set otherwise).  For the finite-difference methods it follows
#'     \code{covMethod}: \code{"r,s"} is the full sandwich
#'     \code{solve(Rfull) \%*\% Sfull \%*\% solve(Rfull)}, \code{"s"} is
#'     \code{solve(Sfull)}, \code{"r"} is \code{solve(Rfull)}.  \code{FALSE}
#'     installs only the structural-theta block (the historical shape).  The
#'     installed shape is named by \code{fit$covMethod} -- \code{"r,s (full)"}
#'     versus \code{"r,s"} -- and the other shape is cached, so
#'     \code{\link{setCov}()} swaps between them without recomputing either.
#'
#' @param fdOutlierZ Cut of the Iglewicz-Hoaglin modified z-score that decides
#'   whether a finite-differenced subject's slope is an outlier against the exact
#'   analytic slopes, and so whether `fdChartrand` refines it.  The conventional
#'   3.5; lower it to make the pass fire more readily (and to exercise it), raise
#'   it to suppress it without turning `fdChartrand` off.
#' @param fdOutlierScale Test the outlier criterion on the **per-observation**
#'   slope (`TRUE`, the default) rather than the raw one.  A per-subject slope
#'   scales with how much data that subject carries, so raw slopes from a
#'   3-observation and a 20-observation subject are not draws from one
#'   distribution -- pooling them makes a legitimately large slope look like an
#'   outlier on unbalanced or sparse data.  Only the test is scaled; the gradient
#'   itself is untouched.  On balanced data this changes nothing, since dividing
#'   every slope by the same count leaves the modified z-score unchanged.
#' @param fdRefine Estimator used to recompute a finite-differenced slope once
#'   the outlier pass fires.  On noisy, hard-to-solve likelihood surfaces the
#'   ordering is roughly `"richardson"` < `"lanczos"` < `"chartrand"`:
#'
#'   * `"richardson"` -- Richardson extrapolation of central differences at
#'     `h, h/v, h/v^2, ...`, cancelling the `h^2, h^4, ...` truncation terms in
#'     turn.  Cheapest, and right when the surface is smooth and only truncation
#'     matters.  It extrapolates toward `h -> 0`, which is *into* the noise, so it
#'     is the wrong instrument when the noise floor is what limits the difference.
#'   * `"lanczos"` -- the Lanczos generalized derivative, a least-squares slope
#'     through `2m+1` points.  Same `O(h^2)` truncation as a central difference but
#'     lower variance, since independent evaluation noise averages down as points
#'     are added rather than being amplified.  The middle rung.
#'   * `"chartrand"` (default) -- total-variation regularized differentiation over
#'     a wide interval, which absorbs curvature through the regularized derivative
#'     itself instead of assuming a stencil.  Most expensive and most robust to a
#'     genuinely rough surface.
#'
#'   All three apply identically: they are gated by the same outlier test, touch
#'   only the finite-differenced subjects, and never recompute a subject whose
#'   analytic gradient is available.
#' @param fdLanczosM Half-width `m` of the `"lanczos"` estimator (`2m` evaluations).
#' @param fdRichardsonR Depth of the `"richardson"` extrapolation table.
#' @param fdRichardsonV Step-shrink ratio `v` of the `"richardson"` estimator.
#' @param fdChartrandAll When the outlier pass fires for a parameter, refine
#'   **every** finite-differenced subject with the Chartrand TV derivative rather
#'   than only the outlying ones.  Subjects whose augmented solve succeeded keep
#'   their exact analytic gradient either way -- only finite differences are ever
#'   recomputed.  Default `FALSE` (refine the outliers only).
#' @param fdOutlierAny Let an outlier among the **exact analytic** slopes fire the
#'   outlier pass as well, not only an outlier among the finite differences.
#'   Still only the finite differences are recomputed.  Default `FALSE`.
#' @param fdIndividualStep For the per-subject finite-difference fallback of the
#'   analytic outer gradient (`fast=TRUE`), search the shi step size separately
#'   for every flagged subject (`TRUE`, the default) rather than once on the
#'   summed objective over them.  The subjects that reach this path are the badly
#'   conditioned ones, so one shared step cannot suit them all; a per-subject
#'   search also supplies the population of converged peers a clamped step is
#'   repaired from.  `FALSE` restores the single shared step, which costs fewer
#'   evaluations.
#' @param fdChartrand Refine finite-difference slopes that the robust outlier
#'     test flags (default \code{TRUE}).  When a subject's per-parameter slope
#'     sits far outside the modified z-score interval of the others, its central
#'     difference is suspect; those slopes -- and only those -- are recomputed
#'     with a total-variation regularized derivative (Chartrand) on a wide
#'     interval.  Set \code{FALSE} to keep the plain central difference.
#'
#'     On by default because the outlier test is itself the gate: a well-behaved
#'     problem flags nothing and pays nothing, so the cost falls only on the
#'     complex fits where a slope really is an outlier -- exactly where you would
#'     want the refinement, and where a user is least likely to know to ask for
#'     it.  \code{fit$env$nFdOutlier} reports flagged parameters and refined
#'     slopes, so you can see whether it engaged for a given fit.
#'
#'     Worth knowing when judging it: the measurements that originally motivated
#'     this refinement were taken while the likelihood and the Shi step selection
#'     were both faulty, so they do not evidence its value on current code, and it
#'     has not been observed to trigger on ordinary fits.
#'
#' @param fast When \code{TRUE}, compute the outer (population) gradient
#'     analytically from Almquist (2015) sensitivity equations instead of by
#'     finite differences, and use the Eq-48 random-effect extrapolation for the
#'     next inner-problem starting values.  Requires an analytic-scope model.
#'     Conditionally Gaussian endpoints route through the general (f,R)
#'     assembler, which covers more than the plain add/prop case -- multiple
#'     endpoints, combined and power error, both-sides transforms and a single
#'     estimated boxCox/yeoJohnson lambda.  A single non-Gaussian
#'     (\code{ll()}/generalized) endpoint instead differentiates the
#'     log-density directly, giving an exact inner Hessian and analytic outer
#'     gradient.  Out of scope are \code{linCmt()}, \code{fo}, IOV, more than
#'     one estimated lambda, a theta mu-referenced by several random effects,
#'     and (for the non-Gaussian path) multiple endpoints, censoring or
#'     \code{nAGQ > 1}; those fall back to the finite-difference gradient with
#'     a message (linCmt() and out-of-scope log-likelihood models downgrade to
#'     \code{fast=FALSE} up front).  When unspecified,
#'     the outer optimizer defaults to \code{"lbfgsb3c"} (vs \code{"bobyqa"} for
#'     \code{fast=FALSE}); pairing \code{fast=TRUE} with a derivative-free
#'     \code{outerOpt} reverts to \code{fast=FALSE}.  The \code{*f} methods (e.g.
#'     \code{foceif}) default this to \code{TRUE}.
#' @param priorMethod Which of the shared prior kernel's three omega
#'     conventions (nlmixr2/rxode2#1270) to evaluate an \code{ini({})}
#'     \code{prior()} under -- \code{"general"} (textbook Bayesian),
#'     \code{"nwpri"} (NONMEM's own \verb{$PRIOR NWPRI}), or \code{"tnpri"}
#'     (the Monolix/NONMEM-own-estimation joint-normal convention on omega).
#'     The default, \code{"auto"}, reads the convention off what the model's
#'     own \code{ini({})} actually wrote (an \code{invWishart()} degrees-of-
#'     freedom prior on an omega block means \code{"nwpri"}; a normal prior
#'     directly on an omega element means \code{"tnpri"}; anything else,
#'     including a \code{dcauchy()} prior, means \code{"general"}) --
#'     deliberately never a fixed default, since the identical
#'     \code{invWishart(nu)} syntax means a different number under
#'     \code{"general"} and \code{"nwpri"}.  Setting this explicitly forces
#'     that convention regardless of what auto-detection would have picked,
#'     and errors before any estimation starts if the model's priors are not
#'     representable under it (e.g. \code{priorMethod="tnpri"} on a model
#'     with an \code{invWishart()} prior).  Ignored when the model has no
#'     prior at all.
#'
#' @param covTryHarder If the R matrix is non-positive definite and
#'     cannot be corrected to be non-positive definite try estimating
#'     the Hessian on the unscaled parameter space.
#'
#' @param foceEbeTol Convergence tolerance on the score of the FOCE
#'     frozen-variance EBE re-solve, which the analytic outer gradient
#'     (\code{fast=TRUE}, \code{interaction=FALSE}) needs because FOCE's mode is
#'     not the inner problem's mode.  \code{NULL} (default) uses \code{1e-9}; the
#'     first iteration uses a looser \code{1e-3} so an already-stationary eta is
#'     returned untouched.  Unlike the solver and optimizer tolerances this is not
#'     derived from \code{sigdig} -- it is a convergence target on an inner Newton
#'     rather than a precision request.  It also bounds the Newton decrement at
#'     which a stalled subject is accepted at the ODE solve's noise floor rather
#'     than declining the gradient.  Set it explicitly to test whether a fit's
#'     finite-difference fallbacks are tolerance-driven.
#'
#' @param hessEps is a double value representing the epsilon for the
#'   Hessian calculation. This is used for the R matrix calculation.
#'
#' @param hessEpsLlik is a double value representing the epsilon for
#'   the Hessian calculation when doing focei generalized
#'   log-likelihood estimation.  This is used for the R matrix
#'   calculation.
#'
#' @param optimHessType Hessian type for numeric-difference individual
#'   Hessians in generalized log-likelihood estimation: "central" (matches
#'   R's `optimHess()`, default) or "forward" (faster).
#'
#' @param optimHessCovType Hessian type for numeric-difference individual
#'   Hessians used for the covariance step/final likelihood: "central"
#'   (more accurate, used here) or "forward".
#'
#' @param hessEtaStepMin Floor on the finite-difference step used for the
#'   individual (eta) Hessian in generalized log-likelihood estimation,
#'   expressed as a fraction of that random effect's own standard deviation
#'   (\code{sqrt(diag(Omega))}).  Default \code{0.05}; \code{0} restores the
#'   plain absolute \code{shi21hMin} floor.
#'
#'   The Shi (2021) step search is told the function's noise floor is
#'   \code{rxControl(atolSens=)}, but the inner gradient it differences comes
#'   out of a sensitivity solve that \code{rtolSens} governs as well, so the
#'   noise is understated and the search shrinks the step until the difference
#'   is taken inside it.  \code{n1qn1} absorbs that -- it uses this Hessian only
#'   as a warm-start seed and then corrects it by its own quasi-Newton updates
#'   as it iterates -- but \code{innerOpt="trust"} re-derives it as its
#'   trust-region model Hessian at every trial point, with nothing to correct
#'   it, and adds its log-determinant to the reported objective.  Flooring the
#'   step relative to the eta scale stops the runaway without paying for a
#'   tighter solve.  It applies to every inner optimizer: the step a finite
#'   difference needs is a property of the problem, not of who consumes the
#'   Hessian.
#'
#' @param censOption Treatment of the second derivative for censored
#'   (M2/M3/M4/BLQ) observations in the FOCEI family.  \code{"gauss"} (the default)
#'   keeps the historic uncensored Gauss-Newton curvature, matching common PMx tools;
#'   \code{"laplace"} uses the exact censored second derivative of the objective (a
#'   proper Laplace inner Hessian and analytic covariance).  Accepted by
#'   \code{saemControl}/\code{nlmControl} for a uniform interface but inert there --
#'   SAEM (stochastic EM) has no Laplace inner Hessian, and NLM uses a
#'   finite-difference Hessian that already reflects censoring exactly.
#'
#' @param shi21maxOuter The maximum number of steps for the
#'   optimization of the forward-difference step size.  When not zero,
#'   use this instead of Gill differences.
#'
#' @param shi21maxInner The maximum number of steps for the
#'   optimization of the individual Hessian matrices in the
#'   generalized likelihood problem. When 0, un-optimized finite differences
#'   are used.
#'
#' @param shi21maxInnerCov The maximum number of steps for the
#'   optimization of the individual Hessian matrices in the
#'   generalized likelihood problem for the covariance step. When 0,
#'   un-optimized finite differences are used.
#'
#' @param shi21maxFD The maximum number of steps for the optimization
#'   of the forward difference step size when using dosing events (lag
#'   time, modeled duration/rate and bioavailability)
#'
#' @param shi21hMax Upper bound on the adaptive shi21 finite-difference
#'   step size for FOCEi gradients (both the inner eta and outer
#'   theta/covariate finite differences).  The step-size search never
#'   probes a parameter by more than this on its estimation scale; a
#'   larger value lets the gradient of a flat, small-magnitude parameter
#'   (e.g. a covariate coefficient near 0) clear the ODE-solver noise
#'   floor, at the cost of risking a degenerate solve at the probe.
#'
#' @param shi21hMin Lower bound on the adaptive shi21 finite-difference
#'   step size for FOCEi gradients.  The floor is limited by the ODE
#'   solver tolerance (atol/rtol), not machine precision; below it the
#'   finite difference is dominated by solver noise.
#'
#' @param centralDerivEps Central difference tolerances (relative,
#'   absolute); step size \code{h = abs(x)*derivEps[1] + derivEps[2]}.
#'
#' @param lbfgsLmm An integer giving the number of BFGS updates
#'     retained in the "L-BFGS-B" method, It defaults to 7.
#'
#' @param lbfgsPgtol Projected-gradient convergence tolerance for
#'     "L-BFGS-B": iteration stops when
#'     \code{max(| proj g_i |) <= lbfgsPgtol}. Defaults to `0` (check
#'     suppressed).
#'
#' @param lbfgsFactr Convergence factor for "L-BFGS-B": converges when the
#'     objective reduction is within \code{lbfgsFactr * .Machine$double.eps}.
#'     Derived from \code{sigdig} as \code{10^(-sigdig-2) / .Machine$double.eps},
#'     two orders tighter than the other \code{sigdig}-derived tolerances.  It
#'     tests the objective reduction of a SINGLE step rather than stationarity,
#'     so a target of \code{10^-sigdig} stops as soon as one step is small; the
#'     extra two orders are what make the analytic-gradient (\code{fast=TRUE})
#'     methods reach the same optimum as the derivative-free default.
#'
#' @param diagXform Transformation used on the diagonal of
#'     \code{chol(solve(omega))} (the FOCEi-estimated parameters): one of
#'     \code{"sqrt"} (default), \code{"log"}, or \code{"identity"}.
#'
#' @param iovXform Transformation used on the diagonal of the IOV: one of
#'     \code{"sd"}, \code{"var"}, \code{"logsd"}, or \code{"logvar"}.
#'     This parameterizes the magnitude theta of the \code{"theta"}
#'     \code{iovMethod}; it has no effect under \code{"omega"}, where the
#'     magnitude is fixed at one and the variability is carried by the
#'     omega block itself.
#'
#' @param iovMethod How inter-occasion variability is expanded before
#'     estimation: one of \code{"auto"} (default), \code{"theta"} or
#'     \code{"omega"}.
#'
#'     \code{"theta"} is the long-standing expansion: each occasion
#'     parameter becomes a magnitude theta, with one unit-variance eta per
#'     occasion fixed to it.  That shape cannot represent a correlation
#'     between two occasion parameters.
#'
#'     \code{"omega"} instead fixes the magnitude theta at one and
#'     estimates the per-occasion eta blocks directly, occasion one being
#'     the block and the rest repeating it (NONMEM's
#'     \code{$OMEGA BLOCK(n) SAME}).  The correlation then lives in the
#'     estimated block.
#'
#'     \code{"auto"} picks \code{"omega"} when the IOV block has any
#'     off-diagonal element, since \code{"theta"} provably cannot
#'     represent one, and \code{"theta"} otherwise.  \code{"omega"} may
#'     be asked for on a diagonal block as well, so the two can be
#'     compared on the same model.  The choice is made per LEVEL of
#'     variability, so a correlation on \code{occ} does not change how
#'     an unrelated diagonal \code{occ2} is expanded.
#'
#'     \code{"omega"} is only available for estimation methods that
#'     honour the repeated block (the FOCEi family).  \code{saem} and
#'     the variational, nonparametric and importance-sampling methods
#'     estimate omega elsewhere, so they continue to refuse a
#'     correlated occasion block rather than silently estimate each
#'     occasion on its own.
#'
#'     The two are the same statistical model -- at an occasion variance
#'     of one, where the parameterizations coincide, they agree on the
#'     objective to machine precision, and evaluated at matched random
#'     effects they agree to ~2e-8 at any variance.  They are not
#'     interchangeable in practice: \code{"theta"} hands FOCEi's inner
#'     optimizer unit-scale etas and so converges the inner problem
#'     better when the occasion variance is far from one.  That is why
#'     \code{"auto"} keeps \code{"theta"} unless a correlation forces
#'     \code{"omega"}.
#'
#' @param sumProd Is a boolean indicating if the model should change
#'     multiplication to high precision multiplication and sums to
#'     high precision sums using the PreciseSums package.  By default
#'     this is \code{FALSE}.
#'
#' @param optExpression Optimize the rxode2 expression to speed up
#'     calculation. By default this is turned on.
#'
#' @param literalFix boolean, substitute fixed population values as
#'   literals and re-adjust ui and parameter estimates after
#'   optimization; Default is `TRUE`.
#'
#' @param literalFixRes boolean, substitute fixed population values as
#'   literals and re-adjust ui and parameter estimates after
#'   optimization; Default is `TRUE`.
#'
#' @param ci Confidence level for some tables.  By default this is
#'     0.95 or 95\% confidence.
#'
#' @param boundTol Tolerance for boundary issues.
#'
#' @param calcTables This boolean is to determine if the foceiFit
#'     will calculate tables. By default this is \code{TRUE}
#'
#' @param ... Ignored parameters
#'
#' @param maxInnerIterations Number of iterations for n1qn1
#'     optimization.
#'
#' @param maxOuterIterations Maximum number of L-BFGS-B optimization
#'     for outer problem.
#'
#' @param n1qn1nsim Number of function evaluations for n1qn1
#'     optimization.
#'
#' @param eigen A boolean indicating if eigenvectors are calculated
#'     to include a condition number calculation.
#'
#' @param noAbort Boolean to indicate if you should abort the FOCEi
#'     evaluation if it runs into troubles.  (default TRUE)
#'
#' @param interaction Boolean indicate FOCEi should be used (TRUE)
#'     instead of FOCE (FALSE)
#'
#' @param foce Controls how FOCE (\code{interaction = FALSE}) evaluates the
#'     residual variance R in the inner objective; ignored for FOCEi.  Either
#'     \code{"nonmem"} (default) or \code{"foce+"}:
#'
#'     \itemize{
#'
#'     \item \code{"nonmem"} freezes R at the \code{eta = 0} population
#'     prediction and holds it constant across the inner optimization. This
#'     follows NONMEM's residual-variance convention for FOCE without interaction.
#'
#'     \item \code{"foce+"} evaluates R at the current conditional
#'     \code{eta} (the live variance), keeping the truncated FOCE
#'     inner score, which is refined before evaluating the marginal objective.
#'     It retains the live-variance convention used in \pkg{nlmixr2est} 6.0.1
#'     and earlier. It differs from NONMEM's FOCE without interaction and
#'     omits the variance derivatives included in FOCEI.
#'
#'     }
#'
#' @param cholSEOpt Boolean indicating if the generalized Cholesky
#'     should be used while optimizing.
#'
#' @param cholSECov Boolean indicating if the generalized Cholesky
#'     should be used while calculating the Covariance Matrix.
#'
#' @param fo is a boolean indicating if this is a FO approximation routine.
#'
#' @param cholSEtol tolerance for Generalized Cholesky
#'     Decomposition.  Defaults to suggested (.Machine$double.eps)^(1/3)
#'
#' @param cholAccept Tolerance to accept a Generalized Cholesky
#'     Decomposition for a R or S matrix.
#'
#' @param outerOpt optimization method for the outer problem
#'   Fast \code{"nlminb"} fits automatically use the analytical outer Hessian
#'   for supported Gaussian FOCE/FOCE+/FOCEI/AGQ models, restarting with gradients only if
#'   curvature is unavailable.  \code{"trust"} is a trust-region Newton method
#'   (\pkg{RcppTrust}) built on that same Hessian; see
#'   \code{outerTrustHessian}.  It is unbounded, so a trial point outside the
#'   box is reported as an infinite objective and the region shrinks instead.
#'
#' @param outerTrustHessian Curvature source for \code{outerOpt="trust"}.
#'   \code{"auto"} (default) uses the analytical outer Hessian when
#'   \code{fast=TRUE} makes it available and the damped-BFGS update otherwise,
#'   and falls back to that update if the Hessian is refused mid-fit.
#'   \code{"analytic"} requires \code{fast=TRUE}.  \code{"bfgs"} is the
#'   damped BFGS update (Nocedal & Wright, Numerical Optimization 2nd ed.,
#'   Procedure 18.2) built from consecutive gradients, so it adds no
#'   evaluations.  \code{"fd"} differences the outer gradient, costing one
#'   extra population gradient per parameter per iteration.
#'
#' @param outerTrustRinit,outerTrustRmax Initial and maximum trust-region
#'   radius for \code{outerOpt="trust"}, in the scaled-parameter space the
#'   outer problem optimizes in.  \code{NULL} (default) derives
#'   \code{outerTrustRinit} from \code{minqa::bobyqa()}'s own default-rhobeg
#'   formula and \code{outerTrustRmax} as \code{8 * outerTrustRinit}.
#'
#' @param outerTrustFterm,outerTrustMterm Function-value and predicted-decrease
#'   convergence tolerances for \code{outerOpt="trust"}.  \code{NULL}
#'   (default) uses \code{10^(-sigdig-2)}; \code{outerTrustMterm} defaults to
#'   \code{outerTrustFterm}.
#'
#' @param outerTrustRelStep Relative step handed to the analytical outer
#'   Hessian, and used for the \code{outerTrustHessian="fd"} gradient
#'   difference.
#'
#' @param outerTrustRestarts How many times \code{outerOpt="trust"} may
#'   re-enter the trust region from its own reported solution when the Newton
#'   decrement there says the point is not stationary.  A collapsing trust
#'   region satisfies the solver's own convergence test at a point that is not
#'   a minimum; re-entering restores the initial radius.
#'   \code{maxOuterIterations} is the total across restarts, not per restart.
#'
#' @details Custom outer optimizers receive \code{control$hessian(par, relStep=1e-3)}.
#'   It settles the requested point and assembles the reported objective's
#'   Hessian in the existing C++ sensitivity pool. Third-order terms are obtained
#'   by differencing second-order sensitivities in ETA directions. The result uses
#'   optimizer coordinates and objective scaling and retains negative curvature.
#'   This requires \code{fast=TRUE}. M2/M3/M4 censoring uses the analytical-SE
#'   \code{censOption="gauss"} convention. Censored \code{"laplace"} curvature,
#'   priors, clipped AGQ and estimated transformations are unsupported.
#'   Unsuccessful inner solves and an
#'   active variance floor also make curvature unavailable.
#'
#' @param innerOpt optimization method for the inner (per-subject eta)
#'     problem: `"auto"` (default), `"trust"` (RcppTrust trust-region Newton,
#'     using an exact Gauss-Newton+Omega^-1 Hessian every iteration) or
#'     `"n1qn1"` (quasi-Newton, gets a Hessian only once as a warm-start
#'     seed).  `"BFGS"` is accepted but not implemented -- it silently falls
#'     back to `"n1qn1"`.
#'
#'     `"auto"` picks `"n1qn1"` for a generalized-likelihood endpoint
#'     (`dnorm()`, `ll()`, `dpois()`, ...) and `"trust"` for everything else.
#'     Such an endpoint has no Gauss-Newton shortcut, so the inner eta Hessian
#'     is finite-differenced; `"trust"` rebuilds it at every trial point, which
#'     costs 2*neta inner solves each time, while `"n1qn1"` builds it once as a
#'     warm-start seed and corrects it with its own quasi-Newton updates.  On
#'     everything else `"trust"` is typically the faster of the two.
#'
#' @param trustConf confidence level defining the `innerOpt="trust"`
#'     trust-region radius: since eta ~ N(0, Omega), the radius (in
#'     sqrt(diag(Omega))-scaled units) is `sqrt(qchisq(trustConf, df=neta))`,
#'     the boundary of the `trustConf`-level eta confidence region. Default
#'     0.975.
#'
#' @param trustRinit initial `innerOpt="trust"` trust-region radius. `NULL`
#'     (default) derives it from `trustConf`.
#'
#' @param trustRmax maximum `innerOpt="trust"` trust-region radius. `NULL`
#'     (default) derives it from `trustConf`.
#'
#' @param trustFterm,trustMterm `innerOpt="trust"`'s own function-value and
#'     predicted-decrease convergence tolerances for the per-subject Newton
#'     solve. `NULL` (default) uses `10^(-sigdig-2)`, two orders tighter than
#'     the plain `10^(-sigdig)` most tolerances here use, for the same reason
#'     `lbfgsFactr` is: the inner solve is the function the outer problem
#'     differentiates, so this tolerance sets the objective's noise floor and a
#'     finite-difference outer gradient cannot resolve a step below it. Left at
#'     the plain `10^(-sigdig)`, an `nAGQ=2` `theo_sd` fit stopped at an
#'     objective of 134.46 against 118.52, and took longer doing it -- the
#'     noisy gradient misleads the outer search as well as lengthening it.
#'     Deliberately NOT derived from `epsilon` (`"n1qn1"`'s own, unrelated
#'     "precision of estimate" tolerance) -- tying `"trust"`'s stopping
#'     criterion to a value picked for a different optimizer is exactly the
#'     coupling these parameters exist to remove. Has no effect unless
#'     `innerOpt="trust"`.
#'
#' @param innerHessian Inner optimization curvature: `"focei"` (default) or
#'   `"conditional"`. Full conditional curvature requires fast Gaussian FOCEI.
#'   Inner trust uses it at each trial; n1qn1 uses it with `warm="calc"`.
#'   Value, gradient and full curvature share one sensitivity solve.
#'   The marginal objective's FOCEI curvature is unchanged.  It is not
#'   supported with registered external likelihood contributions.
#' @param detHessian Curvature entering the objective's Laplace
#'   log-determinant: `"focei"` (default), the Gauss-Newton expected
#'   information, or `"conditional"`, the full conditional Hessian (the
#'   observed information at the conditional mode, as NONMEM's LAPLACE).
#'   `"conditional"` requires fast Gaussian FOCEI; the analytic outer
#'   gradient then carries the matching third-order terms, and the
#'   analytic covariance falls back to finite differences.
#' @param hessianMethod For a non-normal-endpoint model (any distribution
#'     other than \code{norm}), the per-subject inner Hessian has no
#'     Gaussian Gauss-Newton shortcut and falls back to a finite difference
#'     of the gradient every `innerOpt="trust"` Newton step
#'     (`calcEtaHessian()`, `src/inner.cpp`). `"fd"` (default) recomputes
#'     this Hessian from scratch every Newton step via finite difference.
#'     `"bfgs"` is the damped BFGS update (Nocedal & Wright, *Numerical
#'     Optimization*, 2nd ed., 2006, Procedure 18.2), always positive
#'     definite; `"sr1"` is the Symmetric Rank-1 update (Nocedal & Wright;
#'     Murtagh & Sargent, *Comput. J.* 13, 1970), not forced positive
#'     definite; `"bofill"` is Bofill's (1994, *J. Comput. Chem.* 15, 1-11)
#'     SR1/Powell-Symmetric-Broyden blend. All three are built from
#'     consecutive Newton steps' already-computed gradients (no extra
#'     evaluations), seeded from one `"fd"`-style Hessian on the first
#'     Newton step of each inner solve.
#'
#'     Unlike `trustControl(hessianMethod=)`'s analogous OUTER-theta option
#'     (where `"sr1"` is the default), this inner Hessian is not just a
#'     step-direction aid: `LikInner2()` (`src/inner.cpp`) adds this
#'     Hessian's log-determinant directly into the reported Laplace
#'     objective, which the OUTER optimizer then searches over. A
#'     quasi-Newton estimate built from the handful of Newton steps one
#'     subject's inner solve takes is accurate enough to guide the step but
#'     not accurate enough to serve as that objective term -- confirmed on
#'     a real one-compartment IV model fit as a general \code{ll()}/`dnorm()`
#'     endpoint: `"bfgs"`/`"sr1"`/`"bofill"` all converged to the same
#'     wrong `Vc` (about 90 against a simulated 70 and a plain (non-`ll()`)
#'     `focei` fit's 67, with a *worse* reported objective than `"fd"`'s
#'     correct answer) -- the biased log-determinant misleads the outer
#'     search into a worse point it reports as better. `"fd"`'s fresh
#'     finite difference has no such bias, so it stays the default here even
#'     though `"bfgs"`/`"sr1"`/`"bofill"` remain available (and are
#'     genuinely faster, per this package's own small benchmark,
#'     `inst/benchmarks/benchmark-focei-hessian-method.R`) for a caller who
#'     has verified their model does not depend on this Hessian's precision.
#'     Has no effect for normal-endpoint models (the Gauss-Newton inner
#'     Hessian is used unconditionally there). Only meaningful with
#'     `innerOpt="trust"`, which the default `innerOpt="auto"` does not pick
#'     for these models -- so setting this needs `innerOpt="trust"` pinned as
#'     well. Asking for `"bfgs"`/`"sr1"`/`"bofill"` under any other inner
#'     optimizer is an error rather than a silent no-op: `foceiControl()`
#'     refuses a pinned `innerOpt`, and the fit refuses one `"auto"` resolves
#'     to `"n1qn1"`.
#'
#' @param stateTrim Trim state amounts/concentrations to this value.
#'
#' @param resetEtaP P-value for resetting an individual ETA to 0 during
#'     optimization, based on a z-test of \code{chol(omega^-1) \%*\% eta}
#'     or \code{eta/sd(allEtas)}. `0` = never reset, `1` = always reset.
#'
#' @param resetThetaP P-value for resetting mu-referenced THETAs based on
#'     ETA drift, checked at the start and near a local minimum (see
#'     \code{resetThetaCheckPer}). `0` = never reset (the default); `1` is
#'     not allowed.  Defaults to off: when the etas cannot re-center the
#'     reset repeats without progress and can error the fit out, and where
#'     it converges it reaches a worse optimum than leaving it off.
#'
#' @param resetThetaCheckPer represents objective function
#'     \% percentage below which resetThetaP is checked.
#'
#' @param resetThetaFinalP represents the p-value for resetting the
#'     population mu-referenced THETA parameters based on ETA drift
#'     during optimization, and resetting the optimization one final time.
#'     `0` = never reset (the default); see \code{resetThetaP}.
#'
#' @param resetHessianAndEta is a boolean representing if the
#'     individual Hessian is reset when ETAs are reset using the
#'     option \code{resetEtaP}.
#'
#' @param muModel Mu-referenced-FOCEI-family regression variant: \code{"none"}
#'     (default, ordinary FOCEI); \code{"lin"}
#'     (\code{mfocei}/\code{mfoce}/\code{magq}/\code{mlaplace}) profiles
#'     mu-referenced population thetas and covariate coefficients out of the
#'     outer optimizer via closed-form OLS regression of each subject's
#'     back-calculated value on the covariates (\code{muModelTol}/
#'     \code{muModelMaxCycles}); \code{"irls"}
#'     (\code{ifocei}/\code{ifoce}/\code{iagq}/\code{ilaplace}) reweights that
#'     by inner-optimization curvature.  Bounded mu parameters are
#'     regression-updated with a clamped step (\code{muModelClampRetries});
#'     a user-fixed (\code{fix()}) mu theta is never updated.
#'
#' @param muRefCovAlg When `TRUE` (default), algebraic expressions that can
#'     be mu-referenced are internally rewritten as mu-referenced
#'     covariates and restored after optimization. Mirrors
#'     \code{saemControl(muRefCovAlg=)}/\code{nlmeControl(muRefCovAlg=)};
#'     for \code{foceiControl()} only takes effect when
#'     \code{muModel != "none"}.
#'
#' @param muModelTol Convergence tolerance for the mu-referenced-FOCEI-family
#'     "re-optimize etas, then regress" cycle (\code{muModel != "none"}):
#'     repeats until the max mu-group theta change drops below this value
#'     or \code{muModelMaxCycles} is reached.
#'
#' @param muModelMaxCycles Maximum number of "re-optimize etas, regress"
#'     cycles per outer iteration (see \code{muModel}, \code{muModelTol}).
#'
#' @param muModelClampRetries Maximum number of active-set re-solve passes
#'     per group per regression update when a bounded mu-referenced
#'     parameter must be clamped to its bound (see \code{muModel}); on
#'     hitting the cap the current clamped-feasible solution is used.
#'
#' @param diagOmegaBoundUpper Upper bound of the diagonal omega matrix, as
#'     \code{diag(omega)*diagOmegaBoundUpper}. `1` = no upper bound.
#'
#' @param diagOmegaBoundLower Lower bound of the diagonal omega matrix, as
#'     \code{diag(omega)/diagOmegaBoundLower}. `1` = no lower bound.
#'
#' @param rhobeg Initial trust region radius for the bobyqa outer optimizer
#'     (with `rhoend`, must satisfy `0 < rhoend < rhobeg`). Default `0.2`
#'     (20% of scaled parameters); adjusted upward if smaller than
#'     `abs(upper-lower)/2`. (bobyqa)
#'
#' @param rhoend Final trust region radius. If not defined,
#'     `10^(-sigdig)` is used. (bobyqa)
#'
#' @param npt Number of points for bobyqa's quadratic approximation to the
#'     objective; must be in `[n+2, (n+1)(n+2)/2]`. Defaults to `2*n + 1`.
#'     (bobyqa)
#'
#' @param eval.max Number of maximum evaluations of the objective function (nlmimb)
#'
#' @param iter.max Maximum number of iterations allowed (nlmimb)
#'
#' @param rel.tol Relative tolerance before nlminb stops (nlmimb).
#'
#' @param x.tol X tolerance for nlmixr2 optimizer
#'
#' @param abstol Absolute tolerance for nlmixr2 optimizer (BFGS)
#'
#' @param reltol  tolerance for nlmixr2 (BFGS)
#'
#' @param gillK Max steps to determine the optimal forward/central
#'     difference step size per parameter (Gill 1983). `0` = no optimal
#'     step size determined.
#'
#' @param gillKcovLlik Same as \code{gillK} but for the generalized focei
#'   log-likelihood method (Gill 1986).
#'
#' @param gillRtol The relative tolerance used for Gill 1983
#'     determination of optimal step size.
#'
#' @param scaleType The scaling scheme for nlmixr2: \code{"nlmixr2"}
#'     (default) scales as \code{(current-init)*scaleC[i] + scaleTo}, with
#'     \code{scaleTo} from \code{normType} and scales from \code{scaleC};
#'     \code{"norm"} uses the simple scaling from \code{normType};
#'     \code{"mult"} scales multiplicatively as \code{current/init*scaleTo};
#'     \code{"multAdd"} scales linearly (\code{(current-init)+scaleTo}) for
#'     parameters in an exponential block (e.g. \code{exp(theta)}) and
#'     multiplicatively otherwise.
#'
#' @param scaleC Scaling constant used with \code{scaleType="nlmixr2"};
#'     when not specified, chosen by parameter type to keep gradient sizes
#'     similar on a log scale: `1` for exp()-transformed/power/boxCox/
#'     yeoJohnson parameters, `0.5*abs(est)` for additive/proportional/
#'     lognormal error parameters, `abs(1/digamma(est+1))` for factorials,
#'     and `log(abs(est))*abs(est)` for log-scale parameters. May be set
#'     explicitly per parameter if these defaults don't apply well.
#'
#' @param scaleC0 Number to adjust the scaling factor by if the initial
#'     gradient is zero.
#'
#' @param scaleCmax Maximum value of the scaleC to prevent overflow.
#'
#' @param scaleCmin Minimum value of the scaleC to prevent underflow.
#'
#' @param scaleCband Length-2 increasing pair `c(low, high)` (default
#'   `c(0.1, 10)`).  Each `theta`'s derivative-based scaling constant
#'   (`1/|init|` for a linear parameter, or the transform-specific
#'   formula) is kept when it lands inside this band, and otherwise
#'   replaced by the parameter's native magnitude `|init|`.  This catches
#'   the singular cases -- `1/|init|` blowing up for a small covariate
#'   initial estimate, `log()` at init `1`, `logit` at the interval
#'   midpoint, `factorial`/`gamma` at a digamma zero -- while leaving the
#'   well-scaled common case (and its results) untouched.
#'
#' @param normType Parameter normalization/scaling used to get scaled
#'     initial values for \code{scaleType}, of the form
#'     \code{Vscaled = (Vunscaled-C1)/C2} (see
#'     \href{https://en.wikipedia.org/wiki/Feature_scaling}{Feature Scaling};
#'     \code{rescale2} follows the
#'     \href{http://apmonitor.com/me575/uploads/Main/optimization_book.pdf}{OptdesX}
#'     manual): \code{"rescale2"} scales all parameters to (-1, 1);
#'     \code{"rescale"} (min-max) scales to (0, 1); \code{"mean"} centers on
#'     the mean with range (0, 1); \code{"std"} standardizes by mean/sd;
#'     \code{"len"} scales to unit (Euclidean) length; \code{"constant"}
#'     performs no normalization (\code{C1=0}, \code{C2=1}).
#'
#' @param gillStep When looking for the optimal forward difference
#'     step size, this is This is the step size to increase the
#'     initial estimate by.  So each iteration the new step size =
#'     (prior step size)*gillStep
#'
#' @param gillFtol The gillFtol is the gradient error tolerance that
#'     is acceptable before issuing a warning/error about the gradient estimates.
#'
#' @param gillKcov Max steps to determine the optimal forward/central
#'     difference step size per parameter (Gill 1983) during the
#'     covariance step. `0` = no optimal step size determined.
#'
#' @param gillStepCov When looking for the optimal forward difference
#'     step size, this is This is the step size to increase the
#'     initial estimate by.  So each iteration during the covariance
#'     step is equal to the new step size = (prior step size)*gillStepCov
#'
#' @param gillStepCovLlik Same as above but during generalized focei
#'   log-likelihood
#'
#' @param gillFtolCov The gillFtol is the gradient error tolerance
#'     that is acceptable before issuing a warning/error about the
#'     gradient estimates during the covariance step.
#'
#' @param gillFtolCovLlik Same as above but applied during generalized
#'   log-likelihood estimation.
#'
#' @param rmatNorm A parameter to normalize gradient step size by the
#'     parameter value during the calculation of the R matrix
#'
#' @param rmatNormLlik A parameter to normalize gradient step size by
#'   the parameter value during the calculation of the R matrix if you
#'   are using generalized log-likelihood Hessian matrix.
#'
#' @param smatNorm A parameter to normalize gradient step size by the
#'     parameter value during the calculation of the S matrix
#'
#' @param smatNormLlik A parameter to normalize gradient step size by
#'   the parameter value during the calculation of the S matrix if you
#'   are using the generalized log-likelihood.
#'
#' @param covGillF Use the Gill calculated optimal Forward difference
#'     step size for the instead of the central difference step size
#'     during the central difference gradient calculation.
#'
#' @param optGillF Use the Gill calculated optimal Forward difference
#'     step size for the instead of the central difference step size
#'     during the central differences for optimization.
#'
#' @param covSmall Small number used to compare covariance estimates
#'     (sandwich vs R/S matrix) before rejecting one as too small to be
#'     the final covariance estimate.
#'
#' @param adjLik When `TRUE`, adjusts the likelihood by the 2*pi constant
#'     nlmixr2's objective function otherwise omits (to match NONMEM),
#'     more closely matching nlme/SAS likelihood approximations. The
#'     objective function itself always matches NONMEM regardless.
#'
#' @param gradTrim The parameter to adjust the gradient to if the
#'     |gradient| is very large.
#'
#' @param gradCalcCentralSmall A small number that represents the value
#'     where |grad| < gradCalcCentralSmall where forward differences
#'     switch to central differences.
#'
#' @param gradCalcCentralLarge A large number that represents the value
#'     where |grad| > gradCalcCentralLarge where forward differences
#'     switch to central differences.
#'
#' @param etaNudge When n1qn1 optimization of an ETA (starting at zero)
#'   misbehaves, reset the Hessian and nudge the ETA up by this value, then
#'   down if it still doesn't move. Defaults to
#'   `qnorm(1-0.05/2)*1/sqrt(3)`. Falls back to \code{etaNudge2}, then to
#'   zero (stop optimizing) if unsuccessful.
#'
#' @param etaNudge2 This is the second eta nudge.  By default it is
#'   qnorm(1-0.05/2)*sqrt(3/5), which is the n=3 quadrature point
#'   (excluding zero) times by the 0.95\% normal region
#'
#' @param etaRestart Number of Omega draws the inner restart cascade tries
#'   once the `etaNudge`/`etaNudge2` restarts are spent and the ETA solve is
#'   still not converged.  Every nudge sets EVERY ETA to the same constant,
#'   which explores poorly when the inner problem has more than one basin; a
#'   draw from Omega is a starting point from the distribution the ETAs
#'   actually come from.  The draws are taken once per fit, seeded from
#'   `seed` (so the objective stays a function of theta alone and the fit
#'   stays reproducible), and are only read after an inner solve has already
#'   failed -- a fit whose inner solves converge never pays for them.  Use 0
#'   to disable.
#'
#'   This applies to `innerOpt="trust"` (the default for a normal endpoint),
#'   which reports a convergence verdict per solve.  `n1qn1` reports none, so
#'   its own restart cascade is unchanged.
#'
#' @param maxOdeRecalc Maximum number of times to reduce the ODE
#'     tolerances and try to resolve the system if there was a bad
#'     ODE solve.
#'
#' @param repeatGillMax If the tolerances were reduced when
#'     calculating the initial Gill differences, the Gill difference
#'     is repeated up to a maximum number of times defined by this
#'     parameter.
#'
#' @param stickyRecalcN The number of bad ODE solves before reducing
#'     the atol/rtol for the rest of the problem.
#'
#' @param outerMaxOdeRecalc Maximum number of times to reduce the ODE
#'     tolerances for a single subject and retry when the analytic
#'     outer (augmented sensitivity) solve fails.  Tracked separately
#'     from `maxOdeRecalc`, which governs the inner problem.  A subject
#'     that solves after loosening still contributes an analytic
#'     gradient instead of dropping the whole gradient to finite
#'     differences.
#'
#' @param outerOdeRecalcFactor The factor the atol/rtol is loosened by
#'     on each analytic outer retry; the outer counterpart of
#'     `odeRecalcFactor`.
#'
#' @param outerStickyRecalcN The number of bad analytic outer solves
#'     for a subject before its loosened tolerance is kept for the rest
#'     of the problem; the outer counterpart of `stickyRecalcN`.
#'
#' @param indTolRelax When `TRUE` (default), only subjects whose ODE
#'     solve produced NaN/Inf have their tolerances relaxed, and the
#'     relaxed tolerance persists across optimizer calls (sticky).
#'     When `FALSE`, all subjects have their tolerances relaxed on
#'     each retry and tolerances are reset afterward.
#'
#' @param nRetries If FOCEi doesn't fit with the current parameter
#'     estimates, randomly sample new parameter estimates and restart
#'     the problem.  This is similar to 'PsN' resampling.
#'
#' @param eventType Event gradient type for dosing events; Can be
#'   "central" or "forward"
#'
#' @param eventSens How sensitivities of dosing/event parameters
#'   (absorption lag time, bioavailability, infusion rate and duration,
#'   etc.) are computed.  `"fd"` uses the legacy finite
#'   differences.  `"jump"` (the default) uses the analytic event ("jump")
#'   sensitivities provided by `rxode2`, which add accuracy and can speed
#'   up the gradient/Hessian by avoiding the extra finite-difference
#'   solves for these parameters.  Also gates the analytic moving-boundary
#'   correction for a modeled `alag()`/`f()` on a `linCmt()` compartment; set
#'   `"fd"` if that model infuses a dose into the lagged/scaled compartment
#'   (rxode2/rxode2#1236), or if the regimen also doses an *unlagged/unscaled*
#'   compartment alongside the lagged/scaled one -- a common design for
#'   estimating `f()` from paired IV+oral data (rxode2/rxode2#1237).
#'
#' @param gradProgressOfvTime This is the time for a single objective
#'     function evaluation (in seconds) to start progress bars on gradient evaluations
#'
#' @param badSolveObjfAdj The objective function adjustment when the
#'   ODE system cannot be solved.  It is based on each individual bad
#'   solve.
#'
#' @param compress Should the object have compressed items
#'
#' @param etaMat Initial (or final) ETA estimates; can also be a prior fit,
#'   whose final ETAs are then used as initial values. By default, uses the
#'   last fit's ETAs if supplied, else all ETAs start at zero (`NULL`).
#'   `NA` disables reuse from a prior fit.
#'
#' @param addProp Type of additive-plus-proportional error: `"combined1"`,
#'   where standard deviations add:
#'   \deqn{y = f + (a + b\times f^c) \times \varepsilon}{y = f + (a + b*f^c)*err};
#'   or `"combined2"`, where variances add:
#'   \deqn{y = f + \sqrt{a^2 + b^2\times f^{2\times c}} \times \varepsilon}{y = f + sqrt(a^2 + b^2*(f^c)^2)*err}.
#'   Here y = observed, f = predicted, a = additive sd, b = proportional/power
#'   sd, c = power exponent (1 in the proportional case).
#'
#' @param odeRecalcFactor The ODE recalculation factor when ODE
#'   solving goes bad, this is the factor the rtol/atol is reduced
#'
#' @param rxControl `rxode2` ODE solving options during fitting, created with `rxControl()`
#'
#' @param fallbackFD Fallback to the finite differences if the
#'   sensitivity equations do not solve.
#'
#' @param smatPer Percentage of failed per-individual parameter gradients
#'   (replaced with the overall parameter gradient) out of the total
#'   (`ntheta*nsub`) above which the S matrix is considered bad.
#'
#' @param sdLowerFact Factor multiplying the estimate when the lower bound
#'   is zero for a standard-deviation error parameter (add.sd, prop.sd,
#'   etc); e.g. estimate 0.15 with lower bound 0 assumes a lower bound of
#'   0.00015. `0` disables this.
#'
#' @param zeroGradFirstReset When `TRUE` (default), reset a zero first
#'   gradient to `sqrt(.Machine$double.eps)` instead of erroring; `FALSE`
#'   errors; `NA` ignores it only on the last reset attempt.
#'
#' @param zeroGradRunReset When `TRUE` (default), reset a zero gradient
#'   encountered mid-run to `sqrt(.Machine$double.eps)` instead of erroring.
#'
#' @param zeroGradBobyqa When `TRUE` (default), a zero-gradient reset
#'   switches to the gradient-free bobyqa method; `NA` only does so for the
#'   first zero gradient.
#'
#' @param mceta Monte Carlo sampling for the best initial ETA estimate
#'   (based on `omega`): `-2` (default) uses the Almquist (2015) Eq-48
#'   extrapolation `eta^0 = eta* + (d eta*/d theta)(theta_new - theta_old)`
#'   when the analytic gradient supplies `d eta*/d theta` (`fast = TRUE`),
#'   accepting the extrapolated eta only when it is within the standardized-eta
#'   reset bound (else keeping the last eta, or resetting to 0 when that is also
#'   out of bound); `-1` jumps between the extrapolated eta and eta=0, keeping
#'   the better; both `-2` and `-1` fall back to keeping the last eta when no
#'   analytic `d eta*/d theta` is available (`fast = FALSE`).  `0` uses eta=0
#'   for each inner optimization; for `n>0`, eta=0 and n-1 etas sampled from
#'   omega are each evaluated and the best (by inner objective) starts the
#'   inner optimization.  The carried last eta is deliberately not one of the
#'   `n>0` candidates -- it is the previous iteration's converged conditional
#'   mode, so it would win for every subject and `n>0` would reduce to the
#'   keep-last behavior of `-1`/`-2`.  When a sampled eta wins, the inner
#'   problem is also solved from eta=0 and the better converged result kept --
#'   compared on the marginal objective the fit reports, which carries the
#'   Laplace `log|H|` term, not on the inner objective the optimizer minimizes --
#'   so no inner solve ends above the one `0` would have reached.  The search is
#'   skipped while the outer gradient is being differenced, where the eta is
#'   pinned to the central evaluation's mode.
#'
#' @param seed Integer seed (default `42`) used to make a FOCEi fit
#'   reproducible and self-contained.  The fit (including the `mceta`
#'   Monte-Carlo initial-ETA draws, which pull from rxode2's threefry engine)
#'   runs inside [rxode2::rxWithSeed()], so it neither depends on the ambient
#'   RNG state nor advances/leaks it -- repeated fits in the same session, and
#'   fits following other estimation methods, give identical results.
#'
#' @param warm Seeding of the n1qn1 inner-optimization Hessian:
#'   `"calc"` (default) warm-starts each inner problem with the eta
#'   Hessian calculated at the starting eta and the current theta;
#'   since theta moves between outer evaluations it is always
#'   recalculated, never reused from an earlier round.  `"save"`
#'   restarts from the curvature n1qn1 built during the subject's
#'   previous inner solve.  `"none"` lets n1qn1 initialize its own
#'   diagonal Hessian.  Ignored by `innerOpt = "trust"`, which always
#'   supplies its own exact Hessian.
#'
#' @param nAGQ Number of Gauss-Hermite adaptive quadrature points. `0`
#'   disables AGQ; `1` is equivalent to Laplace. Cost grows quickly with
#'   ETAs: once the EBE is found, expect `nAGQ^neta` (even `nAGQ`) or
#'   `(nAGQ^neta)-1` (odd `nAGQ`) additional evaluations per subject.
#'
#' @param agqLow The lower bound for adaptive quadrature
#'   log-likelihood. By default this is -Inf; in the original nlmixr's
#'   gnlmm it was -700.
#'
#' @param agqHi The upper bound for adaptive quadrature
#'   log-likelihood.  By default this is Inf; in the original nlmixr's
#'   gnlmm was 400.
#'
#' @param boundedTransform When `TRUE` (default), bounded parameters are
#'   transformed for unbounded optimization methods and back-transformed
#'   for final estimates. `FALSE` optimizes on the original scale with
#'   bounds passed to the optimizer. `NA` transforms for optimization but
#'   skips the final back-transform.
#'
#' @param zeroTheta Positive magnitude (default `0.001`) used to nudge a
#'   population parameter (`theta`) whose initial estimate is exactly `0`
#'   off zero before estimation.  FOCEi scales a linear parameter by its
#'   native magnitude `|init|`, which is `0` (no scale) for a zero
#'   initial estimate, so the parameter is moved to `+zeroTheta` when it
#'   is within the parameter's bounds, otherwise `-zeroTheta`; if neither
#'   is within the bounds an error is raised.  Fixed parameters
#'   (including those fixed at `0`) are left untouched.
#'
#' @param eventSens Controls how dosing/event-parameter (`alag`, `F`,
#'   `rate`, `dur`) sensitivities are computed for THETA/ETA gradients:
#'   `"jump"` (default) uses rxode2's analytic event sensitivities; `"fd"`
#'   uses the legacy finite-difference behavior.  Also gates the analytic
#'   moving-boundary correction for a modeled `alag()`/`f()` on a `linCmt()`
#'   compartment; set `"fd"` if that model infuses a dose into the
#'   lagged/scaled compartment (rxode2/rxode2#1236), or if the regimen also
#'   doses an *unlagged/unscaled* compartment alongside the lagged/scaled one
#'   -- a common design for estimating `f()` from paired IV+oral data
#'   (rxode2/rxode2#1237).
#'
#' @param sensMethod Method used to compute the ODE parameter sensitivities.
#'   `"forward"` uses the classic variational (forward) sensitivity ODEs;
#'   `"default"` is the same thing.
#'
#' @param linCmtSensCarry `"auto"` (default) substitutes the exact
#'   sensitivity-carry gradient for a `linCmt()` parameter driven by both an
#'   eta and a time-varying covariate (needs an rxode2 with the carry
#'   sentinels; silently keeps the standard gradient otherwise); `"none"`
#'   always keeps the standard gradient.
#'
#' @inheritParams rxode2::rxSolve
#' @inheritParams minqa::bobyqa
#'
#' @details
#'
#' Uses R's L-BFGS-B (\code{\link{optim}}) for the outer problem and BFGS
#' \code{\link[n1qn1]{n1qn1}} (restoring the prior individual Hessian) for
#' the inner problem, which is left unscaled since eta estimates start near
#' zero. The covariance step is performed on the unscaled problem, so its
#' condition number may differ from the scaled problem's.
#'
#' @author Matthew L. Fidler
#'
#' @return The control object that changes the options for the FOCEi
#'   family of estimation methods
#'
#' @seealso \code{\link{optim}}
#' @seealso \code{\link[n1qn1]{n1qn1}}
#' @seealso \code{\link[rxode2]{rxSolve}}
#' @references
#'
#' Gill, P.E., Murray, W., Saunders, M.A., & Wright,
#' M.H. (1983). Computing Forward-Difference Intervals for Numerical
#' Optimization. Siam Journal on Scientific and Statistical Computing,
#' 4, 310-321.
#'
#' Shi, H.M., Xie, Y., Xuan, M.Q., & Nocedal, J. (2021). Adaptive
#' Finite-Difference Interval Estimation for Noisy Derivative-Free
#' Optimization.
#'
#' @family Estimation control
#' @export
foceiControl <- function(
  sigdig = 3, #
  ...,
  epsilon = NULL, # 1e-4,
  maxInnerIterations = 1000, #
  maxOuterIterations = 5000, #
  n1qn1nsim = NULL, #
  print = 1L, #
  printNcol = NULL, #
  scaleTo = 1.0, #
  scaleObjective = 0, #
  normType = c("rescale2", "mean", "rescale", "std", "len", "constant"), #
  scaleType = c("nlmixr2", "norm", "mult", "multAdd"), #
  scaleCmax = 1e5, #
  scaleCmin = 1e-5, #
  scaleCband = c(0.1, 10), #
  scaleC = NULL, #
  scaleC0 = 1e5, #
  derivEps = rep(20 * sqrt(.Machine$double.eps), 2), #
  derivMethod = c("switch", "forward", "central"), #
  derivSwitchTol = NULL, #
  covDerivMethod = c("central", "forward"), #
  covMethod = c("r,s", "analytic", "r", "s", "sa", "imp", ""), #
  covSolveTol = NULL, #
  covFull = TRUE, #
  fast = FALSE, #
  priorMethod = c("auto", "general", "nwpri", "tnpri"), #
  fdOutlierZ = 3.5, #
  fdOutlierScale = TRUE, #
  fdRefine = c("chartrand", "lanczos", "richardson"), #
  fdLanczosM = 2L, #
  fdRichardsonR = 2L, #
  fdRichardsonV = 2.0, #
  fdChartrandAll = FALSE, #
  fdOutlierAny = FALSE, #
  fdIndividualStep = TRUE, #
  fdChartrand = TRUE, #
  # norm of weights = 1/0.225
  # hessEps = (1/0.225*.Machine$double.eps)^(1 / 4), #
  foceEbeTol = NULL, #
  hessEps = (.Machine$double.eps)^(1 / 3),
  # hessEpsLlik =(1/0.225*.Machine$double.eps)^(1/4),
  hessEpsLlik = (.Machine$double.eps)^(1 / 3),
  optimHessType = c("central", "forward"),
  optimHessCovType = c("central", "forward"),
  hessEtaStepMin = 0.05,
  censOption = c("gauss", "laplace"),
  eventType = c("central", "forward"), #
  eventSens = c("jump", "fd"), #
  centralDerivEps = rep(20 * sqrt(.Machine$double.eps), 2), #
  lbfgsLmm = 7L, #
  lbfgsPgtol = 0, #
  lbfgsFactr = NULL, #
  eigen = TRUE, #
  diagXform = c("sqrt", "log", "identity"), #
  iovXform = c("sd", "var", "logsd", "logvar"), #
  iovMethod = c("auto", "theta", "omega"), #
  sumProd = FALSE, #
  optExpression = TRUE, #
  literalFix = TRUE,
  literalFixRes = TRUE,
  ci = 0.95, #
  useColor = NULL, #
  boundTol = NULL, #
  calcTables = TRUE, #
  noAbort = TRUE, #
  interaction = TRUE, #
  foce = c("nonmem", "foce+"), #
  cholSEtol = (.Machine$double.eps)^(1 / 3), #
  cholAccept = 1e-3, #
  resetEtaP = 0.15, #
  # Default OFF.  The ETA-drift theta reset re-centers a
  # mu-referenced theta by the mean eta and restarts.  When the
  # etas cannot re-center -- e.g. every omega fixed, or a model
  # whose misfit the etas must absorb -- the shift does not stick,
  # the drift returns and the reset repeats until the restart cap
  # errors the fit out.  Where it does converge it lands on a worse
  # optimum than not resetting at all.  Same failure mode as the
  # mu-referenced (lin/irls) families' linear centering.
  resetThetaP = 0, #
  resetThetaFinalP = 0, #
  diagOmegaBoundUpper = 5, # diag(omega) = diag(omega)*diagOmegaBoundUpper; =1 no upper
  diagOmegaBoundLower = 100, # diag(omega) = diag(omega)/diagOmegaBoundLower; = 1 no lower
  cholSEOpt = FALSE, #
  cholSECov = FALSE, #
  fo = FALSE, #
  covTryHarder = FALSE, #
  outerOpt = c(
    "bobyqa",
    "nlminb",
    "lbfgsb3c",
    "L-BFGS-B",
    "mma",
    "lbfgsbLG",
    "slsqp",
    "uobyqa",
    "newuoa",
    "trust"
  ), #
  innerOpt = c("auto", "trust", "n1qn1", "BFGS"), #
  innerHessian = c("focei", "conditional"), #
  detHessian = c("focei", "conditional"), #
  hessianMethod = c("fd", "bfgs", "sr1", "bofill"), #
  ## trust-region inner optimizer (RcppTrust)
  trustConf = 0.975, # confidence level defining the trust-region radius
  trustRinit = NULL, # NULL -> derived from trustConf/neta
  trustRmax = NULL, # NULL -> derived from trustConf/neta
  trustFterm = NULL, # NULL -> 10^(-sigdig), NOT epsilon
  trustMterm = NULL, # NULL -> 10^(-sigdig), NOT epsilon
  ## trust-region OUTER optimizer (outerOpt="trust")
  outerTrustHessian = c("auto", "analytic", "bfgs", "fd"),
  outerTrustRinit = NULL, # NULL -> min(0.95, 0.2*max(abs(par)))
  outerTrustRmax = NULL, # NULL -> 8*outerTrustRinit
  outerTrustFterm = NULL, # NULL -> 10^(-sigdig-2)
  outerTrustMterm = NULL, # NULL -> outerTrustFterm
  outerTrustRelStep = 1e-3,
  outerTrustRestarts = 3L,
  ##
  rhobeg = .2, #
  rhoend = NULL, #
  npt = NULL, #
  ## nlminb
  rel.tol = NULL, #
  x.tol = NULL, #
  eval.max = 4000, #
  iter.max = 2000, #
  abstol = NULL, #
  reltol = NULL, #
  resetHessianAndEta = FALSE, #
  muModel = c("none", "irls", "lin"), #
  muRefCovAlg = TRUE, #
  muModelTol = 1e-5, #
  muModelMaxCycles = 20L, #
  muModelClampRetries = 10L, #
  stateTrim = Inf, #
  shi21maxOuter = 0L,
  shi21maxInner = 20L,
  shi21maxInnerCov = 20L,
  shi21maxFD = 20L,
  shi21hMax = 2.0,
  shi21hMin = 1e-4,
  gillK = 10L, #
  gillStep = 4, #
  gillFtol = 0, #
  gillRtol = sqrt(.Machine$double.eps), #
  gillKcov = 10L, #
  # gillKcovLlik = 20L,
  gillKcovLlik = 10L,
  gillStepCovLlik = 4.5,
  # gillStepCovLlik = 2,
  gillStepCov = 2, #
  gillFtolCov = 0, #
  gillFtolCovLlik = 0, #
  rmatNorm = TRUE, #
  # rmatNormLlik= FALSE, #
  rmatNormLlik = TRUE, #
  smatNorm = TRUE, #
  ## smatNormLlik = FALSE,
  smatNormLlik = TRUE,
  covGillF = TRUE, #
  optGillF = TRUE, #
  covSmall = 1e-5, #
  adjLik = TRUE, ## Adjust likelihood by 2pi for FOCEi methods
  gradTrim = Inf, #
  maxOdeRecalc = 5, #
  odeRecalcFactor = 10^(0.5), #
  gradCalcCentralSmall = 1e-4, #
  gradCalcCentralLarge = 1e4, #
  etaNudge = qnorm(1 - 0.05 / 2) / sqrt(3), #
  etaNudge2 = qnorm(1 - 0.05 / 2) * sqrt(3 / 5), #
  etaRestart = 4L, #
  nRetries = 3, #
  seed = 42, #
  resetThetaCheckPer = 0.1, #
  etaMat = NULL, #
  repeatGillMax = 1, #
  stickyRecalcN = 4, #
  outerMaxOdeRecalc = 5, #
  outerOdeRecalcFactor = 10^(0.5), #
  outerStickyRecalcN = 4, #
  indTolRelax = TRUE, #
  gradProgressOfvTime = 10, #
  addProp = c("combined2", "combined1"),
  badSolveObjfAdj = 100, #
  compress = FALSE, #
  rxControl = NULL,
  sigdigTable = NULL,
  fallbackFD = FALSE,
  smatPer = 0.6,
  sdLowerFact = 0.001,
  zeroGradFirstReset = TRUE,
  zeroGradRunReset = TRUE,
  zeroGradBobyqa = TRUE,
  mceta = -2L,
  warm = c("calc", "save", "none"),
  nAGQ = 0,
  agqLow = -Inf,
  agqHi = Inf,
  sensMethod = c("default", "forward"),
  linCmtSensCarry = c("auto", "none"),
  zeroTheta = 0.001,
  boundedTransform = TRUE
) {
  #
  ## sensMethod: forward (variational) ODE parameter sensitivities.
  sensMethod <- match.arg(sensMethod)
  linCmtSensCarry <- match.arg(linCmtSensCarry)
  if (!is.null(sigdig)) {
    checkmate::assertNumeric(sigdig, lower = 1, finite = TRUE, any.missing = TRUE, len = 1)
    if (is.null(boundTol)) {
      boundTol <- 5 * 10^(-sigdig + 1)
    }
    if (is.null(epsilon)) {
      epsilon <- 10^(-sigdig)
    }
    if (is.null(abstol)) {
      abstol <- 10^(-sigdig)
    }
    if (is.null(reltol)) {
      reltol <- 10^(-sigdig)
    }
    if (is.null(rhoend)) {
      rhoend <- 10^(-sigdig)
    }
    if (is.null(lbfgsFactr)) {
      # Two orders TIGHTER than the plain 10^-sigdig the other optimizer
      # tolerances use.  `factr` tests the objective reduction of a SINGLE step,
      # so at 10^-sigdig L-BFGS-B stops as soon as one step is small rather than
      # when the gradient is flat -- measured on a 2-cmt oral fit, that left 8.6
      # OFV units on the table (6 outer evaluations); at 10^-(sigdig+2) the same
      # fit beats the bobyqa reference in 13.  See plans/foceif-outer-opt-pgtol.md.
      # Floor at 1: `factr` is a MULTIPLE of machine epsilon, so factr < 1 asks
      # for a reduction smaller than eps itself.  That is unreachable, and since
      # lbfgsPgtol is 0 by default it would leave L-BFGS-B with no active
      # stopping rule at all.  Reached at sigdig >= 14 here (>= 16 before the
      # two-order tightening).
      lbfgsFactr <- max(10^(-sigdig - 2) / .Machine$double.eps, 1)
    }
    if (is.null(trustFterm)) {
      # Two orders tighter than the plain 10^-sigdig the other tolerances use,
      # for the same reason lbfgsFactr above is: the inner solve IS the function
      # the outer problem differentiates, so its stopping tolerance sets the
      # objective's noise floor, and a finite-difference outer gradient cannot
      # resolve a step below it.  At the plain 10^-sigdig an nAGQ=2 theo_sd fit
      # ended at objf 134.46 against 118.52 here, taking 6.8s against 1.5s --
      # the noisy gradient both misleads the outer search and lengthens it.
      # NOT tied to epsilon ("n1qn1"'s own, unrelated tolerance): independence
      # from it is the point of having these as their own parameters.
      trustFterm <- 10^(-sigdig - 2)
    }
    if (is.null(trustMterm)) {
      trustMterm <- 10^(-sigdig - 2)
    }
    if (is.null(rel.tol)) {
      rel.tol <- 10^(-sigdig)
    }
    if (is.null(x.tol)) {
      x.tol <- 10^(-sigdig)
    }
    if (is.null(derivSwitchTol)) {
      derivSwitchTol <- 2 * 10^(-sigdig)
    }
  }
  if (is.null(sigdigTable)) {
    if (is.null(sigdig)) {
      sigdigTable <- 3L
    } else {
      sigdigTable <- sigdig
    }
  } else {
    checkmate::assertNumeric(sigdigTable, lower = 1, finite = TRUE, any.missing = TRUE, len = 1)
  }

  checkmate::assertNumeric(epsilon, lower = 0, finite = TRUE, any.missing = FALSE, len = 1)
  checkmate::assertIntegerish(maxInnerIterations, lower = 0, any.missing = FALSE, len = 1)
  checkmate::assertIntegerish(maxInnerIterations, lower = 0, any.missing = FALSE, len = 1)
  checkmate::assertIntegerish(maxOuterIterations, lower = 0, any.missing = FALSE, len = 1)

  checkmate::assertNumeric(sdLowerFact, lower = 0, finite = TRUE, upper = 0.1, any.missing = FALSE, len = 1)

  if (is.null(n1qn1nsim)) {
    n1qn1nsim <- 10 * maxInnerIterations + 1
  }
  checkmate::assertIntegerish(n1qn1nsim, len = 1, lower = 1, any.missing = FALSE)
  # Print args are absorbed/validated by iterPrintControl(); `iterPrintControl`
  # picked up from `...` handles the round-trip via do.call(foceiControl, .ctl).
  .iterPrintControl <- .absorbIterPrintControl(
    print = print,
    printNcol = printNcol,
    useColor = useColor,
    iterPrintControl = list(...)$iterPrintControl
  )
  checkmate::assertNumeric(scaleTo, len = 1, lower = 0, any.missing = FALSE)
  checkmate::assertNumeric(scaleObjective, len = 1, lower = 0, any.missing = FALSE)
  checkmate::assertNumeric(scaleCmax, lower = 0, any.missing = FALSE, len = 1)
  checkmate::assertNumeric(scaleCmin, lower = 0, any.missing = FALSE, len = 1)
  checkmate::assertNumeric(scaleCband, lower = 0, finite = TRUE, any.missing = FALSE, len = 2)
  if (scaleCband[1] >= scaleCband[2]) {
    stop("'scaleCband' must be an increasing pair (low, high)", call. = FALSE)
  }
  if (!is.null(scaleC)) {
    checkmate::assertNumeric(scaleC, lower = 0, any.missing = FALSE)
  }
  checkmate::assertNumeric(scaleC0, lower = 0, any.missing = FALSE, len = 1)
  checkmate::assertNumeric(derivEps, lower = 0, len = 2, any.missing = FALSE)
  checkmate::assertNumeric(derivSwitchTol, lower = 0, len = 1, any.missing = FALSE)
  if (checkmate::testIntegerish(covTryHarder, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    covTryHarder <- as.integer(covTryHarder)
  } else {
    checkmate::assertLogical(covTryHarder, any.missing = FALSE, len = 1)
    checkmate::assertNumeric(fdOutlierZ, lower = 0, finite = TRUE, any.missing = FALSE, len = 1)
    checkmate::assertLogical(fdOutlierScale, any.missing = FALSE, len = 1)
    fdRefine <- match.arg(fdRefine)
    checkmate::assertIntegerish(fdLanczosM, lower = 1, any.missing = FALSE, len = 1)
    checkmate::assertIntegerish(fdRichardsonR, lower = 1, any.missing = FALSE, len = 1)
    checkmate::assertNumeric(fdRichardsonV, lower = 1.0000001, finite = TRUE, any.missing = FALSE, len = 1)
    checkmate::assertNumeric(hessEtaStepMin, lower = 0, finite = TRUE, any.missing = FALSE, len = 1)
    checkmate::assertLogical(fdChartrandAll, any.missing = FALSE, len = 1)
    checkmate::assertLogical(fdOutlierAny, any.missing = FALSE, len = 1)
    checkmate::assertLogical(fdIndividualStep, any.missing = FALSE, len = 1)
    checkmate::assertLogical(fdChartrand, any.missing = FALSE, len = 1)
    covTryHarder <- as.integer(covTryHarder)
  }

  checkmate::assertNumeric(rhobeg, lower = 0, len = 1, finite = TRUE, any.missing = FALSE)
  checkmate::assertNumeric(rhoend, lower = 0, len = 1, finite = TRUE, any.missing = FALSE)
  if (rhoend >= rhobeg) {
    stop("the trust region method needs '0 < rhoend < rhobeg'", call. = FALSE)
  }
  if (!is.null(npt)) {
    checkmate::assertIntegerish(npt, lower = 1, len = 1, any.missing = FALSE)
  }
  checkmate::assertIntegerish(eval.max, lower = 1, len = 1, any.missing = FALSE)
  checkmate::assertIntegerish(iter.max, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertNumeric(rel.tol, lower = 0, len = 1, any.missing = FALSE, finite = TRUE)
  checkmate::assertNumeric(x.tol, lower = 0, len = 1, any.missing = FALSE, finite = TRUE)
  checkmate::assertNumeric(abstol, lower = 0, len = 1, any.missing = FALSE, finite = TRUE)
  checkmate::assertNumeric(reltol, lower = 0, len = 1, any.missing = FALSE, finite = TRUE)

  checkmate::assertIntegerish(gillK, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertIntegerish(gillKcov, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertIntegerish(gillKcovLlik, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertNumeric(gillStep, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertNumeric(gillStepCov, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertNumeric(gillStepCovLlik, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertNumeric(gillFtol, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertNumeric(gillFtolCov, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertNumeric(gillFtolCovLlik, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertNumeric(gillRtol, lower = 0, len = 1, any.missing = FALSE, finite = TRUE)
  # gillRtolCov is calculated in the `inner.cpp`
  if (!checkmate::testIntegerish(rmatNorm, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    checkmate::assertLogical(rmatNorm, any.missing = FALSE, len = 1)
  }
  rmatNorm <- as.integer(rmatNorm)
  if (!checkmate::testIntegerish(rmatNormLlik, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    checkmate::assertLogical(rmatNormLlik, any.missing = FALSE, len = 1)
  }
  rmatNormLlik <- as.integer(rmatNormLlik)
  if (!checkmate::testIntegerish(smatNorm, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    checkmate::assertLogical(smatNorm, any.missing = FALSE, len = 1)
  }
  smatNorm <- as.integer(smatNorm)
  if (!checkmate::testIntegerish(smatNormLlik, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    checkmate::assertLogical(smatNormLlik, any.missing = FALSE, len = 1)
  }
  smatNormLlik <- as.integer(smatNormLlik)
  if (!checkmate::testIntegerish(covGillF, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    checkmate::assertLogical(covGillF, any.missing = FALSE, len = 1)
  }
  covGillF <- as.integer(covGillF)
  if (!checkmate::testIntegerish(optGillF, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    checkmate::assertLogical(optGillF, any.missing = FALSE, len = 1)
  }
  optGillF <- as.integer(optGillF)

  # FOCE EBE Newton tolerance.  Deliberately NOT derived from sigdig: this is a score
  # convergence target on an inner Newton, not a solve precision, and coupling it to
  # sigdig made the analytic FOCE gradient available or not depending on the requested
  # digits.  Fixed at the value the routine shipped with.
  if (is.null(foceEbeTol)) {
    foceEbeTol <- 1e-9
  }
  checkmate::assertNumeric(foceEbeTol, lower = 0, finite = TRUE, any.missing = FALSE, len = 1)
  checkmate::assertNumeric(hessEps, lower = 0, any.missing = FALSE, len = 1)
  checkmate::assertNumeric(hessEpsLlik, lower = 0, any.missing = FALSE, len = 1)
  checkmate::assertNumeric(centralDerivEps, lower = 0, any.missing = FALSE, len = 2)

  checkmate::assertIntegerish(lbfgsLmm, lower = 1L, any.missing = FALSE, len = 1)
  lbfgsLmm <- as.integer(lbfgsLmm)
  checkmate::assertNumeric(lbfgsPgtol, lower = 0, any.missing = FALSE, len = 1)
  checkmate::assertNumeric(lbfgsFactr, lower = 0, any.missing = FALSE, len = 1)
  if (!checkmate::testIntegerish(eigen, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    checkmate::assertLogical(eigen, any.missing = FALSE, len = 1)
  }
  eigen <- as.integer(eigen)

  checkmate::assertLogical(sumProd, any.missing = FALSE, len = 1)
  checkmate::assertLogical(optExpression, any.missing = FALSE, len = 1)
  checkmate::assertLogical(literalFix, any.missing = FALSE, len = 1)
  checkmate::assertLogical(literalFixRes, any.missing = FALSE, len = 1)

  checkmate::assertNumeric(ci, any.missing = FALSE, len = 1, lower = 0, upper = 1)
  checkmate::assertNumeric(boundTol, lower = 0, any.missing = FALSE, len = 1)

  checkmate::assertLogical(calcTables, len = 1, any.missing = FALSE)
  if (!checkmate::testIntegerish(noAbort, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    checkmate::assertLogical(noAbort, len = 1, any.missing = FALSE)
  }
  noAbort <- as.integer(noAbort)

  if (!checkmate::testIntegerish(interaction, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    checkmate::assertLogical(interaction, len = 1, any.missing = FALSE)
  }
  interaction <- as.integer(interaction)

  foce <- match.arg(foce)
  ## FOCE (interaction=FALSE) residual-variance choice: 0="nonmem" (eta=0 frozen R),
  ## 1="foce+" (live conditional R; also covMethod="analytic" via ef$focePlus).
  ## Ignored when interaction=TRUE (FOCEi).
  foceType <- as.integer(foce == "foce+")

  checkmate::assertNumeric(cholSEtol, lower = 0, any.missing = FALSE, len = 1)
  checkmate::assertNumeric(cholAccept, lower = 0, any.missing = FALSE, len = 1)

  ## .methodIdx <- c("lsoda"=1L, "dop853"=0L, "liblsoda"=2L);
  ## method <- as.integer(.methodIdx[method]);
  if (checkmate::testIntegerish(scaleType, len = 1, lower = 1, upper = 4, any.missing = FALSE)) {
    scaleType <- as.integer(scaleType)
  } else {
    .scaleTypeIdx <- c("norm" = 1L, "nlmixr2" = 2L, "mult" = 3L, "multAdd" = 4L)
    scaleType <- setNames(.scaleTypeIdx[match.arg(scaleType)], NULL)
  }

  if (checkmate::testIntegerish(optimHessType, len = 1, lower = 1, upper = 3, any.missing = FALSE)) {
    optimHessType <- as.integer(optimHessType)
  } else {
    .optimHessTypeIdx <- c("central" = 1L, "forward" = 3L)
    optimHessType <- setNames(.optimHessTypeIdx[match.arg(optimHessType)], NULL)
  }

  if (checkmate::testIntegerish(optimHessCovType, len = 1, lower = 1, upper = 3, any.missing = FALSE)) {
    optimHessCovType <- as.integer(optimHessCovType)
  } else {
    .optimHessCovTypeIdx <- c("central" = 1L, "forward" = 3L)
    optimHessCovType <- setNames(.optimHessCovTypeIdx[match.arg(optimHessCovType)], NULL)
  }
  # censOption: the censored (M2/M3/M4) inner-Hessian / 2nd-derivative treatment.
  # "gauss" (default) keeps the historic uncensored Gauss-Newton curvature; "laplace"
  # uses the exact censored 2nd derivative (a proper Laplace).  Shared with saem/nlm.
  if (checkmate::testIntegerish(censOption, len = 1, lower = 0, upper = 1, any.missing = FALSE)) {
    censOption <- as.integer(censOption)
  } else {
    censOption <- setNames(c("gauss" = 0L, "laplace" = 1L)[match.arg(censOption)], NULL)
  }
  if (checkmate::testIntegerish(eventType, len = 1, lower = 1, upper = 3, any.missing = FALSE)) {
    eventType <- as.integer(eventType)
  } else {
    .eventTypeIdx <- c("central" = 2L, "forward" = 3L)
    eventType <- setNames(.eventTypeIdx[match.arg(eventType)], NULL)
  }
  ## How dosing/event-parameter (alag, F, rate, dur, ...) sensitivities are
  ## computed: "fd" (legacy finite differences) or "jump" (analytic jump/event
  ## sensitivities from rxode2).  "fd" is the backward-compatible default.
  eventSens <- match.arg(eventSens)

  .normTypeIdx <- c("rescale2" = 1L, "rescale" = 2L, "mean" = 3L, "std" = 4L, "len" = 5L, "constant" = 6L)
  if (checkmate::testIntegerish(normType, len = 1, lower = 1, upper = 6, any.missing = FALSE)) {
    normType <- as.integer(normType)
  } else {
    normType <- setNames(.normTypeIdx[match.arg(normType)], NULL)
  }
  .methodIdx <- c("forward" = 0L, "central" = 1L, "switch" = 3L)
  if (checkmate::testIntegerish(derivMethod, len = 1, lower = 0L, upper = 3L, any.missing = FALSE)) {
    derivMethod <- as.integer(derivMethod)
  } else {
    derivMethod <- match.arg(derivMethod)
    derivMethod <- setNames(.methodIdx[derivMethod], NULL)
  }
  if (checkmate::testIntegerish(covDerivMethod, len = 1, lower = 0L, upper = 3L, any.missing = FALSE)) {
    covDerivMethod <- as.integer(covDerivMethod)
  } else {
    covDerivMethod <- match.arg(covDerivMethod)
    covDerivMethod <- setNames(.methodIdx[covDerivMethod], NULL)
  }
  # covMethod folds in the R-matrix (Hessian) source: "analytic" (the default) uses the
  # exact analytic observed-information R-matrix, reported with the observed-information
  # "r" formula; "r,s"/"r"/"s" use the finite-difference Hessian with that formula; ""
  # skips the covariance step.  The analytic-vs-finite-difference choice is carried to the
  # solver as the (internal, derived) covType string, which also travels via ... so a
  # built control round-trips.
  covType <- "fd"
  # "sa"/"imp" are foreign to the focei kernel; skip the in-kernel cov step and
  # recompute them post-fit at the converged estimates (see .covRecompute).
  covMethodDeferred <- NA_character_
  if (checkmate::testIntegerish(covMethod, len = 1, lower = 0L, upper = 3L, any.missing = FALSE)) {
    covMethod <- as.integer(covMethod)
    .ct <- list(...)$covType
    if (!is.null(.ct)) covType <- match.arg(.ct, c("analytic", "fd"))
  } else if (rxode2::rxIs(covMethod, "character")) {
    if (all(covMethod == "")) {
      covMethod <- 0L
    } else {
      covMethod <- match.arg(covMethod)
      if (covMethod %in% c("sa", "imp")) {
        covMethodDeferred <- covMethod
        covMethod <- 0L
      } else if (identical(covMethod, "analytic")) {
        covType <- "analytic"
        covMethod <- 2L
      } else {
        .covMethodIdx <- c("r,s" = 1L, "r" = 2L, "s" = 3L)
        covMethod <- setNames(.covMethodIdx[covMethod], NULL)
      }
    }
  }
  # round-tripped controls carry the deferred request as a ... field
  if (is.na(covMethodDeferred) && !is.null(list(...)$covMethodDeferred)) {
    covMethodDeferred <- list(...)$covMethodDeferred
  }
  if (!is.null(covSolveTol)) {
    checkmate::assertNumeric(covSolveTol, len = 1, lower = 0, finite = TRUE, any.missing = FALSE)
  }
  checkmate::assertFlag(covFull)
  checkmate::assertFlag(fast)
  priorMethod <- match.arg(priorMethod)
  .xtra <- list(...)
  .bad <- names(.xtra)
  .bad <- .bad[!(.bad %in% .foceiControlInternal)]
  if (length(.bad) > 0) {
    stop("unused argument: ", paste(paste0("'", .bad, "'", sep = ""), collapse = ", "), call. = FALSE)
  }
  .skipCov <- NULL
  if (!is.null(.xtra$skipCov)) {
    .skipCov <- .xtra$skipCov
  }
  .outerOptTxt <- "custom"
  if (!is.null(.xtra$outerOptTxt)) {
    .outerOptTxt <- .xtra$outerOptTxt
  }
  .outerOptDefault <- isTRUE(.xtra$outerOptDefault)
  outerOptFun <- NULL
  if (!is.null(.xtra$outerOptFun)) {
    outerOptFun <- .xtra$outerOptFun
  } else if (rxode2::rxIs(outerOpt, "character")) {
    # Default outer optimizer (when the user did not specify one): lbfgsb3c for
    # the analytic-gradient ("fast") methods, nlminb for the finite-difference
    # methods.  An explicit outerOpt (a single string) skips this;
    # outerOptDefault records that the default was taken so a *f wrapper
    # (.foceiFastCtl) can re-default a round-tripped control under fast=TRUE.
    if (missing(outerOpt)) {
      outerOpt <- if (isTRUE(fast)) "lbfgsb3c" else "bobyqa"
      .outerOptDefault <- TRUE
    }
    outerOpt <- match.arg(outerOpt)
    .outerOptTxt <- outerOpt
    if (outerOpt == "bobyqa") {
      rxode2::rxReq("minqa")
      outerOptFun <- .bobyqa
      outerOpt <- -1L
    } else if (outerOpt == "nlminb") {
      outerOptFun <- .nlminb
      outerOpt <- -1L
    } else if (outerOpt == "mma") {
      outerOptFun <- .nloptr
      outerOpt <- -1L
    } else if (outerOpt == "slsqp") {
      outerOptFun <- .slsqp
      outerOpt <- -1L
    } else if (outerOpt == "lbfgsbLG") {
      outerOptFun <- .lbfgsbLG
      outerOpt <- -1L
    } else if (outerOpt == "uobyqa") {
      outerOptFun <- .uobyqa
      outerOpt <- -1L
    } else if (outerOpt == "newuoa") {
      outerOptFun <- .newuoa
      outerOpt <- -1L
    } else if (outerOpt == "trust") {
      rxode2::rxReq("RcppTrust")
      outerOptFun <- .trustOuter
      outerOpt <- -1L
    } else {
      if (checkmate::testIntegerish(outerOpt, lower = 0, upper = 1, len = 1)) {
        outerOpt <- as.integer(outerOpt)
      } else {
        .outerOptIdx <- c("L-BFGS-B" = 0L, "lbfgsb3c" = 1L)
        outerOpt <- .outerOptIdx[outerOpt]
        if (outerOpt == 1L) {
          rxode2::rxReq("lbfgsb3c")
        }
      }
      outerOptFun <- NULL
    }
  } else if (is(outerOpt, "function")) {
    outerOptFun <- outerOpt
    outerOpt <- -1L
  }
  # A derivative-free outer optimizer never consumes the analytic 'fast' gradient,
  # so computing it is wasted work: downgrade to fast=FALSE with a warning.
  if (isTRUE(fast) && .outerOptTxt %in% c("bobyqa", "uobyqa", "newuoa")) {
    warning(
      "outerOpt='",
      .outerOptTxt,
      "' is derivative-free; the analytic 'fast' gradient is unused -- reverting to fast=FALSE",
      call. = FALSE
    )
    fast <- FALSE
  }
  if (checkmate::testIntegerish(innerOpt, lower = 1, upper = 4, len = 1)) {
    innerOpt <- as.integer(innerOpt)
  } else {
    innerOpt <- setNames(.innerOptFun[match.arg(innerOpt)], NULL)
  }
  if (checkmate::testIntegerish(hessianMethod, len = 1, lower = 1, upper = 4, any.missing = FALSE)) {
    hessianMethod <- as.integer(hessianMethod)
  } else {
    hessianMethod <- setNames(.hessianMethodIdx[match.arg(hessianMethod)], NULL)
  }
  .foceiAssertHessianMethod(hessianMethod, innerOpt)
  innerHessian <- match.arg(innerHessian)
  detHessian <- match.arg(detHessian)
  outerTrustHessian <- match.arg(outerTrustHessian)
  # Same strict-bound handling as the inner trustRinit/trustRmax below:
  # checkmate's lower= is inclusive, and a zero radius can never step.
  checkmate::assertNumeric(outerTrustRinit, lower = 0, finite = TRUE, null.ok = TRUE, len = 1)
  checkmate::assertNumeric(outerTrustRmax, lower = 0, finite = TRUE, null.ok = TRUE, len = 1)
  if (!is.null(outerTrustRinit) && outerTrustRinit <= 0) {
    stop("'outerTrustRinit' must be > 0", call. = FALSE)
  }
  if (!is.null(outerTrustRmax) && outerTrustRmax <= 0) {
    stop("'outerTrustRmax' must be > 0", call. = FALSE)
  }
  if (!is.null(outerTrustRinit) && !is.null(outerTrustRmax) && outerTrustRinit > outerTrustRmax) {
    stop("'outerTrustRinit' cannot be larger than 'outerTrustRmax'", call. = FALSE)
  }
  checkmate::assertNumeric(outerTrustFterm, lower = 0, finite = TRUE, null.ok = TRUE, len = 1)
  checkmate::assertNumeric(outerTrustMterm, lower = 0, finite = TRUE, null.ok = TRUE, len = 1)
  checkmate::assertNumeric(outerTrustRelStep, lower = 0, finite = TRUE, any.missing = FALSE, len = 1)
  if (outerTrustRelStep <= 0) {
    stop("'outerTrustRelStep' must be > 0", call. = FALSE)
  }
  checkmate::assertIntegerish(outerTrustRestarts, lower = 0, any.missing = FALSE, len = 1)
  if (outerTrustHessian == "analytic" && .outerOptTxt == "trust" && !isTRUE(fast)) {
    stop("outerTrustHessian=\"analytic\" requires fast=TRUE", call. = FALSE)
  }
  # `fast` can still be downgraded AFTER this (a linCmt() model has no 2nd-order
  # sensitivities); .trustOuterMethod() demotes to BFGS with a warning there.
  checkmate::assertNumeric(trustConf, lower = 0, upper = 1, finite = TRUE, any.missing = FALSE, len = 1)
  if (trustConf <= 0 || trustConf >= 1) {
    # qchisq(0, df)==0 (zero trust-region radius, no step ever taken) and
    # qchisq(1, df)==Inf (unbounded radius, no trust-region constraint at all)
    # are both degenerate -- checkmate's lower/upper bounds are inclusive, so
    # this has to be checked separately.
    stop("'trustConf' must be strictly between 0 and 1", call. = FALSE)
  }
  # lower=0 is inclusive (checkmate has no strict-bound form); trustRinit/
  # trustRmax==0 is a zero-radius trust region that can never step, the same
  # degenerate case trustConf==0 is rejected for above.
  checkmate::assertNumeric(trustRinit, lower = 0, finite = TRUE, null.ok = TRUE, len = 1)
  if (!is.null(trustRinit) && trustRinit <= 0) {
    stop("'trustRinit' must be > 0", call. = FALSE)
  }
  checkmate::assertNumeric(trustRmax, lower = 0, finite = TRUE, null.ok = TRUE, len = 1)
  if (!is.null(trustRmax) && trustRmax <= 0) {
    stop("'trustRmax' must be > 0", call. = FALSE)
  }
  if (!is.null(trustRinit) && !is.null(trustRmax) && trustRinit > trustRmax) {
    stop("'trustRinit' cannot be larger than 'trustRmax'", call. = FALSE)
  }
  # Resolved above (like epsilon) whenever sigdig is non-NULL; stays NULL (and
  # errors here, matching epsilon's own strictness) only if the caller also
  # passed sigdig=NULL without supplying trustFterm/trustMterm directly.
  checkmate::assertNumeric(trustFterm, lower = 0, finite = TRUE, any.missing = FALSE, len = 1)
  if (trustFterm <= 0) {
    stop("'trustFterm' must be > 0", call. = FALSE)
  }
  checkmate::assertNumeric(trustMterm, lower = 0, finite = TRUE, any.missing = FALSE, len = 1)
  if (trustMterm <= 0) {
    stop("'trustMterm' must be > 0", call. = FALSE)
  }
  if (checkmate::testIntegerish(warm, lower = 0, upper = 2, len = 1, any.missing = FALSE)) {
    warm <- as.integer(warm)
  } else {
    .warmIdx <- c("calc" = 1L, "save" = 0L, "none" = 2L)
    warm <- setNames(.warmIdx[match.arg(warm)], NULL)
  }
  # Checked here, AFTER `warm` is normalized to an integer, because n1qn1 reaches the
  # conditional curvature only through warmZm(), which runs only when warm=="calc".
  # innerOpt="auto" resolves in C++ (needOptimHess ? n1qn1 : trust) and conditional
  # curvature already rejects needOptimHess, so auto cannot land on n1qn1 here.
  if (innerHessian == "conditional") {
    if (!isTRUE(fast) || !isTRUE(as.logical(interaction)) || !(innerOpt %in% c(1L, 3L, 4L))) {
      stop("Conditional inner Hessian requires fast FOCEI with trust or n1qn1", call. = FALSE)
    }
    if (innerOpt == 1L && warm != 1L) {
      stop("innerHessian=\"conditional\" with innerOpt=\"n1qn1\" requires warm=\"calc\"", call. = FALSE)
    }
  }
  if (detHessian == "conditional" && (!isTRUE(fast) || !isTRUE(as.logical(interaction)))) {
    stop("detHessian=\"conditional\" requires fast FOCEI", call. = FALSE)
  }
  if (!is.null(.xtra$resetEtaSize)) {
    .resetEtaSize <- .xtra$resetEtaSize
  } else {
    checkmate::assertNumeric(resetEtaP, lower = 0, upper = 1, len = 1)
    if (resetEtaP > 0 && resetEtaP < 1) {
      .resetEtaSize <- qnorm(1 - (resetEtaP / 2))
    } else if (resetEtaP <= 0) {
      .resetEtaSize <- Inf
    } else {
      .resetEtaSize <- 0
    }
  }
  if (!is.null(.xtra$resetThetaSize)) {
    .resetThetaSize <- .xtra$resetThetaSize
  } else {
    checkmate::assertNumeric(resetThetaP, lower = 0, upper = 1, len = 1)
    if (resetThetaP > 0 && resetThetaP < 1) {
      .resetThetaSize <- qnorm(1 - (resetThetaP / 2))
    } else if (resetThetaP <= 0) {
      .resetThetaSize <- Inf
    } else {
      stop("cannot always reset THETAs", call. = FALSE)
    }
  }
  if (!is.null(.xtra$resetThetaFinalSize)) {
    .resetThetaFinalSize <- .xtra$resetThetaFinalSize
  } else {
    checkmate::assertNumeric(resetThetaFinalP, lower = 0, upper = 1, len = 1)
    if (resetThetaFinalP > 0 && resetThetaFinalP < 1) {
      .resetThetaFinalSize <- qnorm(1 - (resetThetaFinalP / 2))
    } else if (resetThetaFinalP <= 0) {
      .resetThetaFinalSize <- Inf
    } else {
      stop("cannot always reset THETAs", call. = FALSE)
    }
  }
  if (checkmate::testIntegerish(addProp, lower = 1, upper = 1, len = 1)) {
    addProp <- c("combined1", "combined2")[addProp]
  } else {
    addProp <- match.arg(addProp)
  }
  checkmate::assertLogical(compress, any.missing = FALSE, len = 1)
  if (!is.null(.xtra$genRxControl)) {
    genRxControl <- .xtra$genRxControl
  } else {
    genRxControl <- FALSE
    if (is.null(rxControl)) {
      rxControl <- .rxControlScaleSigdig(
        rxode2::rxControl(
          sigdig = sigdig,
          maxsteps = 500000L
        ),
        sigdig
      )
      genRxControl <- TRUE
    } else if (inherits(rxControl, "rxControl")) {
      # a fully-formed rxControl object is the user's explicit solving spec; leave
      # it untouched so any atol/rtol it carries is respected
    } else if (is.list(rxControl)) {
      rxControl <- .rxControlScaleSigdig(do.call(rxode2::rxControl, rxControl), sigdig, skip = names(rxControl))
    }
    if (!inherits(rxControl, "rxControl")) {
      stop("rxControl needs to be ode solving options from rxode2::rxControl()", call. = FALSE)
    }
  }
  checkmate::assertNumeric(diagOmegaBoundUpper, lower = 1, len = 1, any.missing = FALSE, finite = TRUE)
  checkmate::assertNumeric(diagOmegaBoundLower, lower = 1, len = 1, any.missing = FALSE, finite = TRUE)

  if (!checkmate::testIntegerish(cholSEOpt, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    checkmate::assertLogical(cholSEOpt, any.missing = FALSE, len = 1)
  }
  cholSEOpt <- as.integer(cholSEOpt)

  if (!checkmate::testIntegerish(cholSECov, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    checkmate::assertLogical(cholSECov, any.missing = FALSE, len = 1)
  }
  cholSECov <- as.integer(cholSECov)

  if (!checkmate::testIntegerish(fo, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    checkmate::assertLogical(fo, any.missing = FALSE, len = 1)
  }
  fo <- as.integer(fo)

  if (!checkmate::testIntegerish(resetHessianAndEta, lower = 0, upper = 1, any.missing = FALSE, len = 1)) {
    checkmate::assertLogical(resetHessianAndEta, any.missing = FALSE, len = 1)
  }
  resetHessianAndEta <- as.integer(resetHessianAndEta)

  muModel <- match.arg(muModel)
  checkmate::assertLogical(muRefCovAlg, any.missing = FALSE, len = 1)
  checkmate::assertNumeric(muModelTol, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertIntegerish(muModelMaxCycles, lower = 1, len = 1, any.missing = FALSE)
  muModelMaxCycles <- as.integer(muModelMaxCycles)
  checkmate::assertIntegerish(muModelClampRetries, lower = 1, len = 1, any.missing = FALSE)
  muModelClampRetries <- as.integer(muModelClampRetries)

  checkmate::assertNumeric(stateTrim, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertNumeric(covSmall, lower = 0, any.missing = FALSE, finite = TRUE)
  checkmate::assertLogical(adjLik, any.missing = FALSE, len = 1)
  checkmate::assertNumeric(gradTrim, any.missing = FALSE, len = 1)
  checkmate::assertIntegerish(maxOdeRecalc, any.missing = FALSE, len = 1)
  checkmate::assertNumeric(odeRecalcFactor, len = 1, lower = 1, any.missing = FALSE)
  checkmate::assertNumeric(gradCalcCentralSmall, len = 1, lower = 0, any.missing = FALSE, finite = TRUE)
  checkmate::assertNumeric(gradCalcCentralLarge, len = 1, lower = 0, any.missing = FALSE, finite = TRUE)
  checkmate::assertNumeric(etaNudge, len = 1, lower = 0, any.missing = FALSE, finite = TRUE)
  checkmate::assertNumeric(etaNudge2, len = 1, lower = 0, any.missing = FALSE, finite = TRUE)
  checkmate::assertIntegerish(etaRestart, len = 1, lower = 0, any.missing = FALSE)
  checkmate::assertIntegerish(nRetries, lower = 0, any.missing = FALSE)
  if (!is.null(seed)) {
    checkmate::assertIntegerish(seed, any.missing = FALSE, min.len = 1)
  }
  checkmate::assertNumeric(resetThetaCheckPer, lower = 0, upper = 1, any.missing = FALSE, finite = TRUE)
  checkmate::assertIntegerish(repeatGillMax, any.missing = FALSE, lower = 0, len = 1)
  checkmate::assertIntegerish(stickyRecalcN, any.missing = FALSE, lower = 0, len = 1)
  checkmate::assertIntegerish(outerMaxOdeRecalc, any.missing = FALSE, lower = 0, len = 1)
  checkmate::assertNumeric(outerOdeRecalcFactor, len = 1, lower = 1, any.missing = FALSE)
  checkmate::assertIntegerish(outerStickyRecalcN, any.missing = FALSE, lower = 0, len = 1)
  checkmate::assertLogical(indTolRelax, any.missing = FALSE, len = 1)
  checkmate::assertNumeric(gradProgressOfvTime, any.missing = FALSE, lower = 0, len = 1)
  checkmate::assertNumeric(badSolveObjfAdj, any.missing = FALSE, len = 1)
  checkmate::assertLogical(fallbackFD, any.missing = FALSE, len = 1)
  checkmate::assertLogical(zeroGradFirstReset, any.missing = TRUE, len = 1)
  checkmate::assertLogical(zeroGradRunReset, any.missing = FALSE, len = 1)
  checkmate::assertLogical(zeroGradBobyqa, any.missing = TRUE, len = 1)

  checkmate::assertIntegerish(shi21maxOuter, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertIntegerish(shi21maxInner, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertIntegerish(shi21maxInnerCov, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertIntegerish(shi21maxFD, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertNumber(zeroTheta, lower = 0, finite = TRUE)
  if (zeroTheta <= 0) {
    stop("'zeroTheta' must be a positive number", call. = FALSE)
  }
  checkmate::assertNumber(shi21hMin, lower = 0, finite = TRUE)
  checkmate::assertNumber(shi21hMax, lower = 0, finite = TRUE)
  if (shi21hMax <= shi21hMin) {
    stop("'shi21hMax' must be greater than 'shi21hMin'", call. = FALSE)
  }
  checkmate::assertIntegerish(mceta, lower = -2, len = 1, any.missing = FALSE)

  checkmate::assertNumeric(smatPer, any.missing = FALSE, lower = 0, upper = 1, len = 1)
  checkmate::assertIntegerish(nAGQ, lower = 0, len = 1, any.missing = FALSE)
  checkmate::assertNumeric(agqHi, len = 1, any.missing = FALSE)
  checkmate::assertNumeric(agqLow, len = 1, any.missing = FALSE)
  checkmate::assertLogical(boundedTransform, len = 1, any.missing = FALSE)
  .ret <- list(
    maxOuterIterations = as.integer(maxOuterIterations),
    maxInnerIterations = as.integer(maxInnerIterations),
    n1qn1nsim = as.integer(n1qn1nsim),
    iterPrintControl = .iterPrintControl,
    lbfgsLmm = as.integer(lbfgsLmm),
    lbfgsPgtol = as.double(lbfgsPgtol),
    lbfgsFactr = as.double(lbfgsFactr),
    scaleTo = scaleTo,
    epsilon = epsilon,
    derivEps = derivEps,
    derivMethod = derivMethod,
    covDerivMethod = covDerivMethod,
    covMethod = covMethod,
    covType = covType,
    covMethodDeferred = covMethodDeferred,
    covSolveTol = covSolveTol,
    covFull = covFull,
    fast = fast,
    priorMethod = priorMethod,
    fdOutlierZ = as.double(fdOutlierZ),
    # Kept as the CHARACTER name, and read as a string in C++.  Storing the integer code
    # instead does not round-trip: a control is re-passed through foceiControl() (as named
    # arguments -- every field here must be a formal, which is why there is no companion
    # "fdRefineMethod" field), and match.arg() on an integer yields NA silently rather than
    # erroring, so the setting was quietly lost and the default estimator used.
    fdOutlierScale = as.integer(fdOutlierScale),
    fdRefine = fdRefine,
    fdLanczosM = as.integer(fdLanczosM),
    fdRichardsonR = as.integer(fdRichardsonR),
    fdRichardsonV = as.double(fdRichardsonV),
    fdChartrandAll = as.integer(fdChartrandAll),
    fdOutlierAny = as.integer(fdOutlierAny),
    fdIndividualStep = as.integer(fdIndividualStep),
    fdChartrand = as.integer(fdChartrand),
    centralDerivEps = centralDerivEps,
    eigen = eigen,
    diagXform = match.arg(diagXform),
    iovXform = match.arg(iovXform),
    iovMethod = match.arg(iovMethod),
    sumProd = sumProd,
    optExpression = optExpression,
    literalFix = literalFix,
    literalFixRes = literalFixRes,
    outerOpt = as.integer(outerOpt),
    ci = as.double(ci),
    sigdig = as.double(sigdig),
    sigdigTable = sigdigTable,
    scaleObjective = as.double(scaleObjective),
    boundTol = as.double(boundTol),
    calcTables = calcTables,
    noAbort = noAbort,
    interaction = interaction,
    foce = foce,
    foceType = foceType,
    cholSEtol = as.double(cholSEtol),
    foceEbeTol = as.double(foceEbeTol),
    hessEps = as.double(hessEps),
    hessEpsLlik = as.double(hessEpsLlik),
    optimHessType = optimHessType,
    optimHessCovType = optimHessCovType,
    hessEtaStepMin = as.double(hessEtaStepMin),
    censOption = censOption,
    cholAccept = as.double(cholAccept),
    resetEtaSize = as.double(.resetEtaSize),
    resetThetaSize = as.double(.resetThetaSize),
    resetThetaFinalSize = as.double(.resetThetaFinalSize),
    diagOmegaBoundUpper = diagOmegaBoundUpper,
    diagOmegaBoundLower = diagOmegaBoundLower,
    cholSEOpt = cholSEOpt,
    cholSECov = cholSECov,
    fo = fo,
    covTryHarder = covTryHarder,
    outerOptFun = outerOptFun,
    ## bobyqa
    rhobeg = as.double(rhobeg),
    rhoend = as.double(rhoend),
    npt = npt,
    ## nlminb
    rel.tol = as.double(rel.tol),
    x.tol = as.double(x.tol),
    eval.max = eval.max,
    iter.max = iter.max,
    innerOpt = innerOpt,
    hessianMethod = hessianMethod,
    innerHessian = innerHessian,
    detHessian = detHessian,
    ## trust-region inner optimizer (RcppTrust)
    trustConf = as.double(trustConf),
    trustRinit = trustRinit,
    trustRmax = trustRmax,
    trustFterm = trustFterm,
    trustMterm = trustMterm,
    ## trust-region outer optimizer (outerOpt="trust")
    outerTrustHessian = outerTrustHessian,
    outerTrustRinit = outerTrustRinit,
    outerTrustRmax = outerTrustRmax,
    outerTrustFterm = outerTrustFterm,
    outerTrustMterm = outerTrustMterm,
    outerTrustRelStep = as.double(outerTrustRelStep),
    outerTrustRestarts = as.integer(outerTrustRestarts),
    ## BFGS
    abstol = abstol,
    reltol = reltol,
    derivSwitchTol = derivSwitchTol,
    resetHessianAndEta = resetHessianAndEta,
    muModel = muModel,
    muRefCovAlg = muRefCovAlg,
    muModelTol = as.double(muModelTol),
    muModelMaxCycles = muModelMaxCycles,
    muModelClampRetries = muModelClampRetries,
    stateTrim = as.double(stateTrim),
    gillK = as.integer(gillK),
    gillKcov = as.integer(gillKcov),
    gillKcovLlik = as.integer(gillKcovLlik),
    gillRtol = as.double(gillRtol),
    gillStep = as.double(gillStep),
    gillStepCov = as.double(gillStepCov),
    gillStepCovLlik = as.double(gillStepCovLlik),
    scaleType = scaleType,
    normType = normType,
    scaleC = scaleC,
    scaleCmin = as.double(scaleCmin),
    scaleCband = as.double(scaleCband),
    scaleCmax = as.double(scaleCmax),
    scaleC0 = as.double(scaleC0),
    outerOptTxt = .outerOptTxt,
    outerOptDefault = .outerOptDefault,
    rmatNorm = rmatNorm,
    rmatNormLlik = rmatNormLlik,
    smatNorm = smatNorm,
    smatNormLlik = smatNormLlik,
    covGillF = covGillF,
    optGillF = optGillF,
    gillFtol = as.double(gillFtol),
    gillFtolCov = as.double(gillFtolCov),
    gillFtolCovLlik = as.double(gillFtolCovLlik),
    covSmall = as.double(covSmall),
    adjLik = adjLik,
    gradTrim = as.double(gradTrim),
    gradCalcCentralSmall = as.double(gradCalcCentralSmall),
    gradCalcCentralLarge = as.double(gradCalcCentralLarge),
    etaNudge = as.double(etaNudge),
    etaNudge2 = as.double(etaNudge2),
    etaRestart = as.integer(etaRestart),
    maxOdeRecalc = as.integer(maxOdeRecalc),
    odeRecalcFactor = as.double(odeRecalcFactor),
    nRetries = nRetries,
    seed = seed,
    resetThetaCheckPer = resetThetaCheckPer,
    etaMat = etaMat,
    repeatGillMax = as.integer(repeatGillMax),
    stickyRecalcN = as.integer(max(1, abs(stickyRecalcN))),
    outerMaxOdeRecalc = as.integer(outerMaxOdeRecalc),
    outerOdeRecalcFactor = as.double(outerOdeRecalcFactor),
    outerStickyRecalcN = as.integer(max(1, abs(outerStickyRecalcN))),
    indTolRelax = as.logical(indTolRelax),
    eventType = eventType,
    eventSens = eventSens,
    gradProgressOfvTime = gradProgressOfvTime,
    addProp = addProp,
    badSolveObjfAdj = badSolveObjfAdj,
    compress = compress,
    rxControl = rxControl,
    genRxControl = genRxControl,
    skipCov = .skipCov,
    fallbackFD = fallbackFD,
    shi21maxOuter = shi21maxOuter,
    shi21maxInner = shi21maxInner,
    shi21maxInnerCov = shi21maxInnerCov,
    shi21maxFD = shi21maxFD,
    shi21hMax = shi21hMax,
    shi21hMin = shi21hMin,
    smatPer = smatPer,
    sdLowerFact = sdLowerFact,
    zeroGradFirstReset = zeroGradFirstReset,
    zeroGradRunReset = zeroGradRunReset,
    zeroGradBobyqa = zeroGradBobyqa,
    mceta = as.integer(mceta),
    warm = warm,
    nAGQ = as.integer(nAGQ),
    agqHi = as.double(agqHi),
    agqLow = as.double(agqLow),
    sensMethod = sensMethod,
    linCmtSensCarry = linCmtSensCarry,
    boundedTransform = boundedTransform,
    zeroTheta = zeroTheta
  )
  if (!is.null(.xtra$est)) {
    .ret$est <- .xtra$est
  }
  if (length(etaMat) == 1L && is.na(etaMat)) {
    .ret$etaMat <- NA
  } else if (!is.null(etaMat)) {
    .doWarn <- TRUE
    if (inherits(etaMat, "nlmixr2FitCore")) {
      etaMat <- etaMat$etaMat
      .doWarn <- FALSE
    }
    if (.doWarn && missing(maxInnerIterations)) {
      warning(sprintf(
        "using 'etaMat' assuming 'maxInnerIterations=%d', set 'maxInnerIterations' explicitly to avoid this warning",
        maxInnerIterations
      ))
    }
    checkmate::assertMatrix(etaMat, mode = "double", any.missing = FALSE, min.rows = 1, min.cols = 1)
    .ret$etaMat <- etaMat
  }
  class(.ret) <- "foceiControl"
  .ret
}

.rxUiDeparseFoceiControl <- function(object, var, type = "foceiControl") {
  .ret <- eval(str2lang(paste0(type, "()")))
  .outerOpt <- character(0)
  if (object$outerOpt == -1L && object$outerOptTxt == "custom") {
    warning("functions for `outerOpt` cannot be deparsed, reset to default", call. = FALSE)
  } else if (!(object$outerOptTxt %in% c(.ret$outerOptTxt, "stats::optimize"))) {
    .outerOpt <- paste0("outerOpt = ", deparse1(object$outerOptTxt))
  }
  .w <- .deparseDifferent(.ret, object, .foceiControlInternal)
  # covMethod folds the analytic-vs-finite-difference R-matrix choice (carried by the
  # derived internal covType) into a single token; covType is never deparsed on its own.
  .covMethodStr <- function(o) {
    if (identical(o$covType, "analytic")) {
      return("analytic")
    }
    if (identical(as.integer(o$covMethod), 0L)) {
      return("")
    }
    .idx <- c("r,s" = 1L, "r" = 2L, "s" = 3L)
    names(.idx)[match(as.integer(o$covMethod), .idx)]
  }
  .covTok <- character(0)
  if (!identical(.covMethodStr(object), .covMethodStr(.ret))) {
    .covTok <- paste0("covMethod = ", deparse1(.covMethodStr(object)))
  }
  if (length(.w) == 0 && length(.outerOpt) == 0 && length(.covTok) == 0) {
    return(str2lang(paste0(var, " <- ", type, "()")))
  }
  .n <- names(.ret)[.w]
  .n <- .n[!(.n %in% c("outerOpt", "covMethod"))]
  if (length(.covTok) > 0) {
    .n <- c(.n, "covMethod")
  }
  # preserve the formal-argument declaration order (names(.ret)) so the covMethod
  # token lands in its natural position instead of always first
  .n <- .n[order(match(.n, names(.ret)))]
  .retD <- c(
    vapply(
      .n,
      function(x) {
        if (x == "covMethod") {
          return(.covTok)
        }
        .val <- .deparseShared(x, object[[x]])
        if (!is.na(.val)) {
          return(.val)
        }
        if (x == "innerOpt") {
          paste0("innerOpt = ", deparse1(names(.innerOptFun[which(object[[x]] == .innerOptFun)])))
        } else if (x == "warm") {
          .warmIdx <- c("calc" = 1L, "save" = 0L, "none" = 2L)
          paste0("warm = ", deparse1(names(.warmIdx[which(object[[x]] == .warmIdx)])))
        } else if (x %in% c("optimHessType", "optimHessCovType")) {
          .methodIdx <- c("central" = 1L, "forward" = 3L)
          paste0(x, " = ", deparse1(names(.methodIdx[which(object[[x]] == .methodIdx)])))
        } else if (x == "eventType") {
          .methodIdx <- c("central" = 2L, "forward" = 3L)
          paste0(x, " = ", deparse1(names(.methodIdx[which(object[[x]] == .methodIdx)])))
        } else if (x == "hessianMethod") {
          paste0(x, " = ", deparse1(names(.hessianMethodIdx[which(object[[x]] == .hessianMethodIdx)])))
        } else if (x %in% c("derivMethod", "covDerivMethod")) {
          .methodIdx <- c("forward" = 0L, "central" = 1L, "switch" = 3L)
          paste0(x, " = ", deparse1(names(.methodIdx[which(object[[x]] == .methodIdx)])))
        } else if (x == "covMethod") {
          if (object[[x]] == 0L) {
            paste0(x, " = \"\"")
          } else {
            .covMethodIdx <- c("r,s" = 1L, "r" = 2L, "s" = 3L)
            paste0(x, " = ", deparse1(names(.covMethodIdx[which(object[[x]] == .covMethodIdx)])))
          }
        } else {
          paste0(x, " = ", deparse1(object[[x]]))
        }
      },
      character(1)
    ),
    .outerOpt
  )
  str2lang(paste(var, " <- ", type, "(", paste(.retD, collapse = ", "), ")"))
}

#' @export
rxUiDeparse.foceiControl <- function(object, var) {
  .rxUiDeparseFoceiControl(object, var, type = "foceiControl")
}

Try the nlmixr2est package in your browser

Any scripts or data that you put into this service are public.

nlmixr2est documentation built on Sept. 20, 2026, 9:08 a.m.