Skip to contents

Registers an addition term (a brms "aterm": the trials(n) in y | trials(n) ~ x) that frmtmb will accept on the left-hand side of a formula. A family that lives outside frmtmb uses this to give its per-row data the spelling its literature uses, instead of asking users for vint(). Register from the contributing package's .onLoad().

Usage

frmtmb_register_aterm(name, arity = 1L, coerce = as.numeric, option = NULL)

Arguments

name

The term's name, as it is written in a formula, without parentheses: "dec" accepts y | dec(response) ~ x.

arity

How many arguments the term takes. With arity = 1 the value arrives as aterms[[name]]; above one it arrives as aterms[[paste0(name, i)]] for each argument, which is the vint() convention.

coerce

Function applied to each evaluated argument, once, at frame assembly. It must return a numeric vector, because the value is baked into the tape as data. The default is as.numeric(); a term whose natural spelling is a factor gives a function that maps the factor to the numbers the density wants. A factor, character or logical value is coerced on the fitted data only. Newdata and refits are coded through a table of what each value became there, so that a coercion that builds levels from its input cannot code new rows by their own levels, and a value the fit did not see is refused.

option

Optional function (family, call, env) that makes the term an option of the family rather than data (see the section "An option of the family"). The default, NULL, is a data term.

Value

NULL, invisibly. Called for the registration.

Details

The registered term carries DATA and nothing else. Its evaluated, coerced value reaches the family's lpdf, lcdf, post$mean_fn and sim in the aterms list under name (or under name1, name2, ... when arity is above one, following vint()), it is required on newdata, and it is what frmtmb_family(required_aterms =) names. It cannot reshape the likelihood: censoring, truncation and case weights are core terms because the core acts on them, and a contributed term is not a route to that.

Registering a name twice replaces the earlier entry, so that reloading the contributing package is not an error. The eight core terms cannot be replaced.

The term joins the compatibility vocabulary at the same time, as the feature "<name>()" of kind "aterm", so that frm_compat() can be asked about it and frmtmb_register_compat() rules may name it. A term frm() accepts and the table cannot describe would be a gap by construction, so the registrant is not asked to declare it twice; declaring it in frmtmb_register_compat(features =) anyway is a no-op rather than a duplicate. Register the term BEFORE the rules that name it.

An option of the family

Some terms carry no data. brms's bhaz(df = 6) in time | bhaz(df = 6) ~ x sets the basis of the baseline hazard of cox(): it is an argument of the family, written on the response side. Give such a term an option function. Then the term is not an addition-term value: it does not reach aterms, it is not required on newdata, and arity and coerce are not used. When the formula is parsed and the response's family is known, frmtmb calls option(family, call, env) once, with the family, the term's call as written (quote(bhaz(df = 6))), and the formula's environment, in which the function evaluates the call's arguments. The function returns the family that the fit uses, or stops with a message that says why it refuses the call. Build that family again from its constructor, as the example does: a density is a closure over the constructor's arguments, so a slot set on the family afterwards changes nothing that the likelihood reads.

The family must accept the term. A family whose frmtmb_family(accepts_aterms =) does not name the term is refused before option() runs. A family that declares no allow-list reaches option(), which must refuse a family it cannot change. update(), predict() on new data and refits parse the formula again, each time from the family as it was written, so option() must return the same family for the same family and call.

A name the vocabulary already holds under another kind is refused, and nothing is registered on either side: s() is a smooth, so frmtmb_register_aterm("s") would give one spelling two meanings in one formula. Thirteen names are taken this way: s, t2, mo, mi_pred, gp_pred, cs_pred, ar, ma, arma, cosy, unstr, mm and mmc.

See also

frmtmb_family() for required_aterms, which is how a family says the term is not optional, frmtmb_register_frame_check() for the seam that refuses a data problem, and frmtmb-extension-api for the accessors an extension may use

Examples

# a decision indicator, given as a factor and delivered as 0/1
if (FALSE) { # \dontrun{
frmtmb_register_aterm("dec", arity = 1, coerce = function(x) {
  as.integer(factor(x)) - 1L
})
} # }

# an option of a family: `y | knownsd(2) ~ x` is a normal model whose
# standard deviation is known to be 2. The family declares the term,
# and the option builds the family again with the new value.
normal_sd <- function(sd = 1) {
  force(sd)
  frmtmb_family(
    "normal_sd", dpars = "mu", links = list(mu = "identity"),
    accepts_aterms = "knownsd",
    lpdf = function(y, dpars, aterms, extra) {
      -0.5 * ((y - dpars$mu) / sd)^2 - log(sd) - 0.5 * log(2 * pi)
    })
}
set.seed(1)
d <- data.frame(x = rnorm(50))
d$y <- 1 + d$x + rnorm(50, sd = 2)
# frmtmb_local_registries() undoes the registration when the function
# returns, so the example leaves the registry as it found it
fit_knownsd <- function() {
  frmtmb_local_registries()
  frmtmb_register_aterm("knownsd", option = function(family, call, env) {
    normal_sd(eval(call[[2L]], env))
  })
  frm(bf(y | knownsd(2) ~ x), family = normal_sd(), data = d)
}
if (requireNamespace("withr", quietly = TRUE)) {
  fit <- fit_knownsd()
  # the log likelihood of the least-squares line with sd = 2
  print(logLik(fit))
  print(sum(dnorm(d$y, fitted(lm(y ~ x, d)), 2, log = TRUE)))
}
#> 'log Lik.' -103.5655 (df=2)
#> [1] -103.5655