# ParmOff: Powerful Parameter Passing

## Overview

`ParmOff` solves a common pain point in R programming: you have a large named vector or list of parameters, and you want to call a function with only the subset it understands — without writing boilerplate code to select, rename, clamp, or transform arguments by hand.

A typical workflow looks like this:

1. You collect a set of named parameters (e.g. from an optimiser, a configuration file, or a fit result).
2. The target function only uses some of those parameters, and you may want to restrict, rename, log-transform, or bound them before the call.
3. `ParmOff` handles all of that in a single, composable call.

Processing steps happen in this fixed order:

1. Strip a prefix/suffix from argument names (`.strip`).
2. Merge `...` into `.args`, with `.args` taking precedence on name conflicts.
3. Clamp arguments to minimum / maximum values (`.lower`, `.upper`) — before de-logging when `.bound_raw = TRUE` (default), after when `.bound_raw = FALSE`.
4. De-log arguments stored in log₁₀ space (`.logged`).
5. Restrict to a named subset (`.use_args`).
6. Remove named arguments (`.rem_args`).
7. Drop arguments not in the function’s formals unless the function accepts `...` and `.pass_dots = TRUE`.
8. Optionally add functional argument constraints with `.constrain`.
9. Call the function or return the processed argument list (`.return`).

## 1 The basics

### 1.1 Dropping extra arguments automatically

    model <- function(x, y, z) x * y + z

    # 't' is not a formal of model — ParmOff silently drops it
    params <- c(x = 1, y = 2, z = 3, t = 4)
    ParmOff(model, params)
    #> [1] 5

### 1.2 Named vector vs. named list

Both work. A named vector is coerced to a list element-by-element, which is fine as long as all values share a common type. Use a named list when values have mixed types.

    # Named numeric vector
    ParmOff(model, c(x = 2, y = 3, z = 1))
    #> [1] 7

    # Named list — preferred when types differ
    ParmOff(model, list(x = 2L, y = 3.0, z = TRUE))
    #> [1] 7

### 1.3 Mixing `.args` and `...`

Arguments passed via `...` are merged with `.args`, but `.args` always wins on duplicates.

    # z is in .args; the conflicting z in ... is ignored
    ParmOff(model, list(x = 1, y = 2, z = 3), z = 99)
    #> [1] 5

    # z is missing from .args but supplied via ...
    ParmOff(model, list(x = 1, y = 2), z = 3)
    #> [1] 5

## 2 Selecting and removing arguments

### 2.1 `.use_args` — allowlist

    f <- function(x, y) x + y

    # Only x and y are passed; the extra z never reaches f
    ParmOff(f, list(x = 2, y = 3, z = 99), .use_args = c("x", "y"))
    #> [1] 5

### 2.2 `.rem_args` — blocklist

    # Remove z before passing; f receives x and y only
    f_xyz <- function(x = 1, y = 2, z = 3) x * y + z
    ParmOff(f_xyz, list(x = 2, y = 3, z = 99), .rem_args = "z")
    #> [1] 9

### 2.3 Combining both

`.use_args` is applied first (keep only these), then `.rem_args` (remove these from what’s left).

    # Keep x, y, z — then remove z
    ParmOff(f, list(x = 2, y = 3, z = 99, w = 0),
            .use_args = c("x", "y", "z"), .rem_args = "z")
    #> [1] 5

## 3 Stripping name prefixes / suffixes

When parameters come from an optimiser or a configuration system they often carry a common prefix. `.strip` is a regex applied to all argument names via `sub()`.

    # A parameter vector from, say, a Bayesian sampler with a "fit." prefix
    fit_params <- c(fit.x = 1, fit.y = 2, fit.z = 3, fit.t = 4)
    ParmOff(model, fit_params, .strip = "fit\\.")
    #> [1] 5

`.strip` happens before filtering, so `.use_args` / `.rem_args` work on the post-stripped names.

    ParmOff(f, list(p.x = 2, p.y = 3, p.z = 99),
            .strip = "p\\.", .use_args = c("x", "y"))
    #> [1] 5

## 4 Log-transformed parameters (`.logged`)

Optimisers work best when parameters live on an unbounded real line. Scale parameters (standard deviations, amplitudes, etc.) are naturally expressed in log₁₀ space during fitting. `.logged` names arguments that are stored as log₁₀ values and should be back-transformed before being passed to the target function.

    # y is stored as log10(2) ≈ 0.301; 10^0.301 ≈ 2
    ParmOff(model, list(x = 1, y = log10(2), z = 3), .logged = "y")
    #> [1] 5

    # Both x and y are in log10 space
    ParmOff(model, list(x = 1, y = 1, z = 3), .logged = c("x", "y"))
    #> [1] 103
    # x = 10^1 = 10, y = 10^1 = 10 → 10*10 + 3 = 103

## 5 Bounding parameters (`.lower`, `.upper`)

`.lower` and `.upper` are named numeric vectors (or lists) that clamp individual arguments. Only names present in the current argument list are affected; extra names in the bound vectors are silently ignored.

    # Clamp y upward, z downward
    ParmOff(model, list(x = 1, y = 0.1, z = 15),
            .lower = c(y = 1), .upper = c(z = 10))
    #> [1] 11
    # y was 0.1, clamped to 1  → 1*1 + 10 = 11

### 5.1 `.bound_raw = TRUE` (default) — bounds in log₁₀ space

When parameters are log-transformed, you usually want to express bounds in the same space as the optimiser sees them — i.e. in log₁₀ units. The default `bound_raw = TRUE` applies bounds before de-logging.

    # y is in log10 space; clamp to [-1, 1] before back-transforming
    # log10(y) clamped to 1 → 10^1 = 10
    ParmOff(model, list(x = 1, y = 2, z = 3),
            .logged = "y", .lower = c(y = -1), .upper = c(y = 1))
    #> [1] 13
    # Result: 1 * 10 + 3 = 13

### 5.2 `.bound_raw = FALSE` — bounds in real space

When you instead want to bound the physical value after back-transformation, set `.bound_raw = FALSE`.

    # y = 2 (log10) → 10^2 = 100; upper 5 (real) → clamp to 5
    # Result: 1 * 5 + 3 = 8
    ParmOff(model, list(x = 1, y = 2, z = 3),
            .logged = "y", .upper = c(y = 5), .bound_raw = FALSE)
    #> [1] 8

### 5.3 The underlying `ParmLim*` helpers

Internally, `ParmOff` uses three helper functions:

- `ParmLimLo(x, lower)` for lower clamping,
- `ParmLimHi(x, upper)` for upper clamping,
- `ParmLimBoth(x, lower, upper)` for both in sequence.

These helpers are fully recursive, so they work on nested list structures of arbitrary depth — not just simple vectors. The key matching rules are:

Bound is a named list — each child of `x` is matched by name. Children with no matching name are left unchanged, so you only need to specify bounds for the parameters you care about.

Bound is a scalar or atomic vector — broadcast to every leaf in the tree, regardless of nesting depth.

Bound is a named atomic vector and `x` is a named atomic vector — elements are aligned by name; unmatched elements of `x` are left unchanged.

    # Named vector: partial named bound — only 'a' is clamped
    ParmLimLo(c(a = -5, b = 10, c = 3), lower = c(a = 0))
    #>  a  b  c
    #>  0 10  3

    # Named vectors: both lower and upper, leaving c untouched
    ParmLimBoth(c(a = -5, b = 10, c = 3),
                lower = c(a = 0),
                upper = c(b = 7))
    #> a b c
    #> 0 7 3

    # Partial named-list bound: only supply bounds for the children you need
    x <- list(a = -1, b = 5, c = -3)
    ParmLimLo(x, lower = list(a = 0, c = 0))  # b is left unchanged
    #> $a
    #> [1] 0
    #>
    #> $b
    #> [1] 5
    #>
    #> $c
    #> [1] 0

    # Scalar broadcast: one value applied to every leaf, regardless of nesting
    deep <- list(p = list(q = list(r = -5, s = 20), t = 3), u = -1)
    ParmLimBoth(deep, lower = 0, upper = 10)
    #> $p
    #> $p$q
    #> $p$q$r
    #> [1] 0
    #>
    #> $p$q$s
    #> [1] 10
    #>
    #>
    #> $p$t
    #> [1] 3
    #>
    #>
    #> $u
    #> [1] 0

    # Deep partial bound: supply bounds only at the levels you need
    ParmLimBoth(deep,
      lower = list(p = list(q = list(r = 0), t = 0), u = 0),
      upper = list(p = list(q = list(s = 10), t = 5))
    )
    #> $p
    #> $p$q
    #> $p$q$r
    #> [1] 0
    #>
    #> $p$q$s
    #> [1] 10
    #>
    #>
    #> $p$t
    #> [1] 3
    #>
    #>
    #> $u
    #> [1] 0
    # p$q$r: 0  (was -5)       p$q$s: 10 (was 20)
    # p$t:   3  (unchanged)    u:     0  (was -1)

## 6 Log/unlog helpers (`ParmLog` / `ParmUnLog`)

`ParmOff` delegates all log-transformation work to two standalone helpers that can also be used directly:

- `ParmLog(x, logged, log_type = 'log10')` — applies a forward log transformation to selected elements of `x`.
- `ParmUnLog(x, logged, log_type = 'log10')` — applies the inverse transformation (what `.logged` inside `ParmOff` does).

Both accept the same flexible `logged` selector that `ParmOff`’s `.logged` accepts:

- character vector — transform elements whose names match.
- logical vector — transform elements where the flag is `TRUE` (must be the same length as `x`).

The `log_type` argument controls the flavour of the transformation:

- `'log10'` (default) — `log10()` / `10^x`
- `'ln'` — `log()` / `exp()`

The shape of each element (matrix, array, vector) is always preserved because the helpers use `lapply` internally and R’s log/exp functions respect dimensions.

    params <- list(amplitude = 100, scale = 10, offset = 3)

    # Forward-transform two parameters to log10 space
    logged_params <- ParmLog(params, logged = c("amplitude", "scale"))
    logged_params
    #> $amplitude
    #> [1] 2
    #>
    #> $scale
    #> [1] 1
    #>
    #> $offset
    #> [1] 3

    # Back-transform them
    ParmUnLog(logged_params, logged = c("amplitude", "scale"))
    #> $amplitude
    #> [1] 100
    #>
    #> $scale
    #> [1] 10
    #>
    #> $offset
    #> [1] 3

    # Logical-vector selector: transform the first two elements
    ParmUnLog(list(a = 2, b = 1, c = 5), logged = c(TRUE, TRUE, FALSE))
    #> $a
    #> [1] 100
    #>
    #> $b
    #> [1] 10
    #>
    #> $c
    #> [1] 5
    # a = 10^2 = 100, b = 10^1 = 10, c unchanged

    # Natural-log flavour
    ParmLog(list(sigma = exp(3), mu = 0), logged = "sigma", log_type = 'ln')
    #> $sigma
    #> [1] 3
    #>
    #> $mu
    #> [1] 0
    # sigma = log(exp(3)) = 3

    # Matrix elements retain their shape
    mat_params <- list(cov = matrix(c(100, 0, 0, 100), 2, 2), mu = 5)
    out <- ParmLog(mat_params, logged = "cov")
    out$cov   # still a 2×2 matrix, values are log10 of originals
    #>      [,1] [,2]
    #> [1,]    2 -Inf
    #> [2,] -Inf    2

## 7 Functional argument constraints

Sometimes it is necessary to apply additional constraints to input arguments. This is especially true when they represent parameters with phsyical relationship, like property `y` always has to be double `y`:

    model_ex = function(x,y,z){x * y + z}
    input = c(x=1, y=2, z=3)

    ParmOff(model_ex, input, .logged='y', .lower=list(y=0), .upper=list(y=1), .return='args')
    #> $current_args
    #> $current_args$x
    #> [1] 1
    #>
    #> $current_args$y
    #> [1] 10
    #>
    #> $current_args$z
    #> [1] 3
    #>
    #>
    #> $ignore_args
    #> named list()

    #Make y constrained to be 2*x:
    constrain_func = function(x, y, z){list(x=x, y = 2 * x, z=z)}
    ParmOff(model_ex, input, .logged='y', .lower=list(y=0), .upper=list(y=1),
      .constrain=constrain_func, .return='args')
    #> $current_args
    #> $current_args$x
    #> [1] 1
    #>
    #> $current_args$y
    #> [1] 2
    #>
    #> $current_args$z
    #> [1] 3
    #>
    #>
    #> $ignore_args
    #> named list()

Note how `y` goes from being 10 to being 2. Constraints are executed as the very last act before our target function (`.func`) is called.

## 8 Real-world example: fitting a Normal distribution with `optim`

A common pattern is to fit a parametric model using `optim`. The parameter vector passed to the objective function needs to be forwarded cleanly to the likelihood function, with scale parameters back-transformed from log space.

    set.seed(42)
    data_obs <- rnorm(200, mean = 5, sd = 2)

    # Negative log-likelihood; 'sd' is optimised in log10 space
    neg_ll <- function(par, data) {
      -sum(dnorm(data, mean = par["mean"], sd = 10^par["log_sd"], log = TRUE))
    }

    # Starting values: mean ~ 0, log10(sd) ~ 0 (i.e. sd ~ 1)
    start <- c(mean = 0, log_sd = 0)
    fit   <- optim(start, neg_ll, data = data_obs, method = "BFGS")

    cat("Estimated mean  :", round(fit$par["mean"],   3), "\n")
    #> Estimated mean  : 4.945
    cat("Estimated sd    :", round(10^fit$par["log_sd"], 3), "\n")
    #> Estimated sd    : 1.944

Now suppose we want to evaluate the model at the fitted parameters using `ParmOff`. The fitted vector uses the internal name `log_sd`, but `dnorm` expects `sd`. We can strip the `log_` prefix and de-log in one step:

    fitted_par <- fit$par  # named c(mean=..., log_sd=...)

    # strip "log_" prefix → names become mean, sd
    # de-log "sd"        → 10^log_sd
    ll_val <- ParmOff(
      function(mean, sd) sum(dnorm(data_obs, mean, sd, log = TRUE)),
      fitted_par,
      .strip  = "log_",
      .logged = "sd"
    )
    cat("Log-likelihood at fitted parameters:", round(ll_val, 2), "\n")
    #> Log-likelihood at fitted parameters: -416.77

## 9 Complex example: multi-component galaxy surface-brightness fitting

A realistic scientific use case: fitting a galaxy image with a multi-component model where different parameters have different transformations and constraints.

We simulate a simplified galaxy model with two components — a Sérsic bulge and an exponential disc — each described by a handful of parameters. The fitting uses `optim`, and several parameters are optimised in log space; bounds keep them physically sensible.

    # Sérsic profile I(r) = I0 * exp(-b_n * ((r/Re)^(1/n) - 1))
    # For simplicity we integrate along a 1-D radial profile

    sersic <- function(r, I0, Re, n) {
      bn <- 2 * n - 1/3   # approximation valid for n > 0.5
      I0 * exp(-bn * ((r / Re)^(1 / n) - 1))
    }

    # Exponential disc: I(r) = I0d * exp(-r / Rd)
    disc <- function(r, I0d, Rd) I0d * exp(-r / Rd)

    # Combined profile
    galaxy_profile <- function(r, I0, Re, n, I0d, Rd) {
      sersic(r, I0, Re, n) + disc(r, I0d, Rd)
    }

    # Simulate "observed" data
    set.seed(7)
    r_grid  <- seq(0.1, 10, length.out = 60)
    true_par <- c(I0 = 100, Re = 2, n = 4, I0d = 50, Rd = 3)
    obs      <- galaxy_profile(r_grid, I0 = 100, Re = 2, n = 4, I0d = 50, Rd = 3) *
                  exp(rnorm(60, 0, 0.01))   # 1 % Gaussian scatter in log space

    # Chi-squared objective — parameters in "fit space":
    #   log10(I0), log10(Re), n (linear), log10(I0d), log10(Rd)
    # Bounds:  I0 in [1,1e4], Re in [0.1,20], n in [0.5,8],
    #          I0d in [1,1e4], Rd in [0.1,20]

    fit_bounds_lower <- c(log10_I0 = 0,    log10_Re = -1,   n = 0.5,
                          log10_I0d = 0,   log10_Rd = -1)
    fit_bounds_upper <- c(log10_I0 = 4,    log10_Re =  log10(20), n = 8,
                          log10_I0d = 4,   log10_Rd =  log10(20))

    # Objective function — par is named in "log/linear fit space"
    chi_sq <- function(par, r, obs, lower, upper) {
      # Clamp to bounds (ParmOff does this too, but optim L-BFGS-B handles it here)
      # par <- pmax(pmin(par, upper[names(par)]), lower[names(par)])

      # Use ParmOff to back-transform and forward to galaxy_profile
      model_val <- ParmOff(
        galaxy_profile,
        .args   = as.list(par),
        .strip  = "log10_",          # log10_I0 -> I0, log10_Re -> Re, etc.
        .logged = c("I0", "Re", "I0d", "Rd"),  # back-transform these four
        r       = r                  # extra arg via ...
      )
      sum((log(obs) - log(model_val))^2)
    }

    start_fit <- c(log10_I0 = 1.5, log10_Re = 0, n = 2,
                   log10_I0d = 1.5, log10_Rd = 0.3)

    fit_gal <- optim(
      start_fit, chi_sq,
      r = r_grid, obs = obs,
      lower = fit_bounds_lower, upper = fit_bounds_upper,
      method = "L-BFGS-B"
    )

    # Recover physical parameters
    recovered <- ParmOff(
      function(log10_I0, log10_Re, n, log10_I0d, log10_Rd)
        c(I0 = 10^log10_I0, Re = 10^log10_Re, n = n,
          I0d = 10^log10_I0d, Rd = 10^log10_Rd),
      as.list(fit_gal$par)
    )

    cat("True   :", paste(names(true_par), round(true_par, 2), sep = "=", collapse = ", "), "\n")
    #> True   : I0=100, Re=2, n=4, I0d=50, Rd=3
    cat("Fitted :", paste(names(recovered), round(recovered, 2), sep = "=", collapse = ", "), "\n")
    #> Fitted : I0=83.55, Re=2.23, n=4.26, I0d=47.94, Rd=2.84

    #We can then re-produce the best fit profile easily and compare:
    model_val <- ParmOff(
        galaxy_profile,
        .args   = fit_gal$par,
        .strip  = "log10_",          # log10_I0 -> I0, log10_Re -> Re, etc.
        .logged = c("I0", "Re", "I0d", "Rd"),  # back-transform these four
        r       = r_grid                  # extra arg via ...
      )

    magplot(r_grid, obs, type='l', log='xy', xlab='Rad', ylab='Intensity', lwd=5)
    lines(r_grid, model_val, col='lightgreen', lwd=3)

Key points illustrated:

- `.strip = "log10_"` renames fit-space parameters back to the names `galaxy_profile` expects.
- `.logged` back-transforms four of the five parameters without touching `n`.
- The extra argument `r` is injected cleanly via `...` without polluting the parameter vector.
- Bounds are handled by `optim`’s `L-BFGS-B` here, but `ParmOff`’s `.lower` / `.upper` could alternatively enforce them inside the objective.

## 10 Using `.return = 'args'` for debugging

When things go wrong it is useful to inspect exactly which arguments `ParmOff` would pass.

    result <- ParmOff(
      model,
      list(x = 1, y = 2, z = 3, t = 99),
      .return = "args"
    )

    cat("Arguments that WILL be passed:\n")
    #> Arguments that WILL be passed:
    print(result$current_args)
    #> $x
    #> [1] 1
    #>
    #> $y
    #> [1] 2
    #>
    #> $z
    #> [1] 3

    cat("\nArguments that were IGNORED:\n")
    #>
    #> Arguments that were IGNORED:
    print(result$ignore_args)
    #> $t
    #> [1] 99

## 11 Verbose debugging (`.verbose`)

Setting `.verbose = TRUE` activates diagnostic output at three levels:

1. Argument selection — names of arguments that will be passed to the function and those that will be ignored (the existing `.verbose` behaviour).
2. Limit clamping — for each argument that is actually changed by `.lower` or `.upper`, a message reports the parameter name and its before/after values.
3. De-logging — for each argument transformed by `.logged`, a message reports the parameter name and its before/after values.

All three levels fire from a single `.verbose = TRUE` flag on `ParmOff`. The same flag can also be passed directly to the standalone helpers `ParmLimLo`, `ParmLimHi`, `ParmLimBoth`, `ParmLog`, and `ParmUnLog`.

Output is only emitted when the value actually changes, and only for elements that are reasonably small: scalars, vectors of length ≤ 20, or matrices of size ≤ 10 × 10. Larger structures are silently skipped to avoid flooding the console.

    model <- function(x, y, z) x * y + z

    # x is below its lower bound, y is above its upper bound
    # z is in log10 space and will be de-logged
    ParmOff(model,
            list(x = -1, y = 20, z = 1),
            .lower  = list(x = 0),
            .upper  = list(y = 10),
            .logged = "z",
            .verbose = TRUE)
    #> Lower limit imposed on 'x'
    #>   before: -1
    #>   after:  0
    #> Upper limit imposed on 'y'
    #>   before: 20
    #>   after:  10
    #> ParmUnLog (10^x) applied to 'z'
    #>   before: 1
    #>   after:  10
    #> Used arguments:
    #> x y z
    #>
    #> Ignored arguments:
    #> [1] 10
    # Messages emitted (multi-line format):
    #   Lower limit imposed on 'x'
    #     before: -1
    #     after:  0
    #   Upper limit imposed on 'y'
    #     before: 20
    #     after:  10
    #   ParmUnLog (10^x) applied to 'z'
    #     before: 1
    #     after:  10
    #   Used arguments:
    #   x y z
    #
    #   Ignored arguments:
    # (blank — no ignored arguments)
    # Result: 0 * 10 + 10 = 10

You can also call the helpers directly with `verbose = TRUE` to inspect individual transformations without going through `ParmOff`:

    # Lower limit
    ParmLimLo(list(alpha = -0.5, beta = 2), lower = list(alpha = 0), verbose = TRUE)
    #> Lower limit imposed on 'alpha'
    #>   before: -0.5
    #>   after:  0
    #> $alpha
    #> [1] 0
    #>
    #> $beta
    #> [1] 2

    # Upper limit
    ParmLimHi(list(sigma = 100, mu = 5), upper = list(sigma = 50), verbose = TRUE)
    #> Upper limit imposed on 'sigma'
    #>   before: 100
    #>   after:  50
    #> $sigma
    #> [1] 50
    #>
    #> $mu
    #> [1] 5

    # Forward log (e.g. before passing to an optimiser)
    ParmLog(list(amplitude = 500, index = 1.5), logged = "amplitude", verbose = TRUE)
    #> ParmLog (log10) applied to 'amplitude'
    #>   before: 500
    #>   after:  2.69897
    #> $amplitude
    #> [1] 2.69897
    #>
    #> $index
    #> [1] 1.5

    # De-log
    ParmUnLog(list(amplitude = 2.7, index = 1.5), logged = "amplitude", verbose = TRUE)
    #> ParmUnLog (10^x) applied to 'amplitude'
    #>   before: 2.7
    #>   after:  501.1872
    #> $amplitude
    #> [1] 501.1872
    #>
    #> $index
    #> [1] 1.5

### When is verbose useful?

The verbose flag is designed for subtle debugging situations where it is not obvious whether bounds or log transformations are firing. Common scenarios:

- An optimiser is converging to a boundary value and you want to confirm that a bound is actually clamping the parameter.
- A parameter value looks unexpectedly large or small after de-logging and you want to trace the before/after.
- You are building up a complex multi-step `ParmOff` call and want to verify each transformation step.

Because verbose output uses `message()` it is easy to suppress in production code (`suppressMessages(...)`) or capture for logging (`withCallingHandlers(..., message = ...)`).

## 12 Performance note

For trivially fast functions, `ParmOff`’s bookkeeping adds measurable overhead. It is designed for functions that take at least tens of milliseconds to run. For inner loops over fast functions, set `.check = FALSE` to skip the `checkmate` validation pass.

    arg_list <- list(x = 1, y = 2, z = 3)

    # Baseline: direct call
    system.time(for (i in 1:1e4) model(1, 2, 3))

    # do.call
    system.time(for (i in 1:1e4) do.call(model, arg_list))

    # ParmOff with full checking
    system.time(for (i in 1:1e4) ParmOff(model, arg_list))

    # ParmOff without checking (closer to do.call overhead)
    system.time(for (i in 1:1e4) ParmOff(model, arg_list, .check = FALSE))

## 13 Quick-reference table

Parameter | Type | Purpose
--- | --- | ---
`.func` | function | Target function to call
`.args` | list / named vector | Arguments to consider passing
`.use_args` | character vector | Allowlist of argument names
`.rem_args` | character vector | Blocklist of argument names
`.strip` | string (regex) | Regex stripped from all argument names
`.lower` | named numeric / list | Lower bounds per argument
`.upper` | named numeric / list | Upper bounds per argument
`.bound_raw` | logical | Apply bounds before (`TRUE`) or after (`FALSE`) de-logging
`.logged` | character or logical vector | Arguments stored in log₁₀ space (character: match by name; logical: TRUE positions de-logged; must equal `length(.args)`); see also `ParmUnLog`
`.constrain` | a function to achieve additional argument constraints |
`.pass_dots` | logical | Pass unmatched args through `...` in `.func`
`.return` | `'function'`|`'args'` | Return call result or processed argument list
`.check` | logical | Enable/disable checkmate input validation
`.quote` | logical | Passed to `do.call(quote=)`
`.envir` | environment | Passed to `do.call(envir=)`
`.verbose` | logical | Print used/ignored args and forward verbose flag to `ParmLimLo`, `ParmLimHi`, `ParmUnLog` (before/after values for each changed parameter)

Standalone helpers

Function | Purpose
--- | ---
`ParmLog(x, logged, log_type, verbose)` | Forward-log selected list elements (`'log10'`, `'ln'`, or `'log2'`)
`ParmUnLog(x, logged, log_type, verbose)` | Inverse-log selected list elements (`'log10'`, `'ln'`, or `'log2'`)
`ParmLimLo(x, lower, verbose)` | Apply lower bounds recursively
`ParmLimHi(x, upper, verbose)` | Apply upper bounds recursively
`ParmLimBoth(x, lower, upper, verbose)` | Apply lower then upper bounds recursively