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:
ParmOff handles all of that in a single, composable
call.Processing steps happen in this fixed order:
.strip).... into .args, with
.args taking precedence on name conflicts..lower,
.upper) — before de-logging when
.bound_raw = TRUE (default), after when
.bound_raw = FALSE..logged)..use_args)..rem_args).... and .pass_dots = TRUE..return).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.
.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.use_args — allowlist.rem_args — blocklistWhen 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.
.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.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.bound_raw = TRUE (default) — bounds in log₁₀
spaceWhen 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.
.bound_raw = FALSE — bounds in real spaceWhen 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] 8optimA 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.944Now 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.77A 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.r is injected cleanly via
... without polluting the parameter vector.optim’s L-BFGS-B
here, but ParmOff’s .lower /
.upper could alternatively enforce them inside the
objective..return = 'args' for debuggingWhen 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] 99For 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))| 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 vector | Arguments stored in log₁₀ space |
.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=) |