Introduction

Trends in EMC2 allow you to model how parameters change as a function of covariates or other parameters. They provide a flexible way to capture systematic changes in model parameters across conditions or over time. This vignette shows:

Trend Composition

A trend is composed of a kernel and a base. The kernel maps inputs to a per‑trial value k; the base then maps k into the parameter scale. Use trend_help() to list kernels and bases. The timing of when a trend is applied is controlled by phase (see Phases below).

trend_help()
## Available kernels:
##   custom: Custom C++ kernel: provided via register_trend().
##   lin_decr: Decreasing linear kernel: k = -c
##   lin_incr: Increasing linear kernel: k = c
##   exp_decr: Decreasing exponential kernel: k = exp(-d_ed * c)
##   exp_incr: Increasing exponential kernel: k = 1 - exp(-d_ei * c)
##   pow_decr: Decreasing power kernel: k = (1 + c)^(-d_pd)
##   pow_incr: Increasing power kernel: k = 1 - (1 + c)^(-d_pi)
##   poly2: Quadratic polynomial: k = d1 * c + d2 * c^2
##   poly3: Cubic polynomial: k = d1 * c + d2 * c^2 + d3 * c^3
##   poly4: Quartic polynomial: k = d1 * c + d2 * c^2 + d3 * c^3 + d4 * c^4
##   delta: Standard delta rule kernel: k = q[i].
##          Updates q[i] = q[i-1] + alpha * (c[i-1] - q[i-1]).
##          Parameters: q0 (initial value), alpha (learning rate).
##   delta2kernel: Dual kernel delta rule: k = q[i].
##           Combines fast and slow learning rates
##           and switches between them based on dSwitch.
##           Parameters: q0 (initial value), alphaFast (fast learning rate),
##           propSlow (alphaSlow = propSlow * alphaFast), dSwitch (switch threshold).
##   delta2lr: Dual learning rate delta rule: k = q[i].
##           Like the standard delta rule, but with separate
##           learning rates for positive and negative prediction errors.
##           Parameters: q0 (initial value), alphaPos (learning rate for positive PEs),
##           alphaNeg (learning rate for negative PEs).
## 
## Available base types:
##   lin: Linear base: parameter + w * k
##   exp_lin: Exponential linear base: exp(parameter) + exp(w) * k
##   centered: Centered mapping: parameter + w*(k - 0.5)
##   add: Additive base: parameter + k
##   identity: Identity base: k
## 
## Phase options:
##   premap: Trend is applied before parameter mapping. This means the trend parameters
##           are mapped first, then used to transform cognitive model parameters before 
##           their mapping.
##   pretransform: Trend is applied after parameter mapping but before transformations.
##                 Cognitive model parameters are mapped first, then trend is applied, 
##                 followed by transformations.
##   posttransform: Trend is applied after both mapping and transformations.
##                  Cognitive model parameters are mapped and transformed first, 
##                  then trend is applied.

Quick Start: From trend to model

Below is a minimal pipeline using the data in samples_LNR (three subjects from Forstmann 2008) to demonstrate where a trend fits in. We create a trend, attach it in the design, and show how it plugs into make_emc() and fit() (calls not evaluated here).

# Example trend: log-mean increases linearly with trial
trend_quick <- make_trend(
  par_names = "m",
  cov_names = "trial_nr",
  kernels   = "lin_incr",
  bases     = "lin",
  phase     = "pretransform"
)

data <- get_data(samples_LNR)
# This does not take subject id into account
data$trial_nr <- 1:nrow(data)
data$covariate1 <- rnorm(nrow(data))
data$covariate2 <- rnorm(nrow(data))


# Build a design with the trend
design_trend <- design(
  data     = data,
  trend    = trend_quick,
  matchfun = function(d) d$S == d$lR,
  formula  = list(m ~ lM, s ~ 1, t0 ~ 1),
  contrasts = list(lM = matrix(c(-1/2, 1/2), ncol = 1, dimnames = list(NULL, "d"))),
  model    = LNR
)
## Intercept formula added for trend_pars: m.w
## 
##  Sampled Parameters: 
## [1] "m"     "m_lMd" "s"     "t0"    "m.w"  
## 
##  Design Matrices: 
## $m
##     lM m m_lMd
##   TRUE 1   0.5
##  FALSE 1  -0.5
## 
## $s
##  s
##  1
## 
## $t0
##  t0
##   1
## 
## $m.w
##  m.w
##    1
# How you would run (not executed here)
# emc <- make_emc(data, design_trend, type = "single")
# fit <- fit(emc)

Defining a Trend

A trend is created using the make_trend() function with the following main arguments:

Let’s take the following example:

trend_lin_decr <- make_trend(
  par_names = "v",
  cov_names = "trial_nr",
  kernels = "lin_incr",
  bases = "lin"
)

This trend applies a linear increasing kernel to v using trial as input. This captures a hypothesis that the drift rate (v) increases over trials. To see how the trend is formulated use trend_help().

trend_help(kernel = "lin_incr")
## Description: 
## Increasing linear kernel: k = c 
##  
## Available bases, first is the default: 
## lin, exp_lin, centered 
## 

The function tells us how the kernel maps the covariate to the kernel k. In this case the kernel k is just the covariate c. The kernel is then used in the base function. The trend_help() function also shows us the available base functions for this kernel. In this case we will use the lin base function. To see how this base function is formulated we can use the trend_help() function again.

trend_help(base = "lin")
## Description: 
## Linear base: parameter + w * k 
##  
## Default transformations: 
## list(w = "identity")
## 

Together these specify how the trend affects the model parameter. With base lin, the mapping is v <- v + B0 * k, where B0 is the base parameter and k comes from the kernel (here k = c, the covariate). Although this separation may seem verbose for simple cases, it makes more complex specifications clear and composable.

EMC2 automatically creates parameter names for the trend, which can be accessed using the get_trend_pnames() function.

get_trend_pnames(trend_lin_decr)
## [1] "v.w"

These parameter names are now included in the parameter types of the model.

Available Kernel Types

EMC2 provides several kernel functions for modeling how cognitive model parameters change as a function of covariates.

Phases

Control when a trend is applied via the phase argument:

# Pre-mapping trend
trend_premap <- make_trend(
  par_names = "v",
  cov_names = "trial_nr",
  kernels   = "exp_incr",
  phase     = "premap"
)

# Pre-transform trend
trend_pretrans <- make_trend(
  par_names = "v",
  cov_names = "trial_nr",
  kernels   = "exp_incr",
  phase     = "pretransform"
)

# Post-transform trend
trend_posttrans <- make_trend(
  par_names = "v",
  cov_names = "trial_nr",
  kernels   = "exp_incr",
  phase     = "posttransform"
)

Phases change how trends interact with mapping and transformations. For example, a premap trend can feed transformed outputs into parameter mapping, while posttransform trends act on the final, transformed parameter scale.

Parameter Inputs (par_input)

Trends can also depend on other parameters via par_input. For example, use t0 as an input to a trend on m:

trend_par_input <- make_trend(
  par_names = "m",
  cov_names = NULL,
  kernels   = "lin_incr",
  par_input = list("t0"),
  phase     = "pretransform"
)

This example mirrors the tests: the input matrix provided to the kernel will contain the covariate columns (none here) followed by the par_input columns (here t0).

Apply a trend only at a factor level (at)

Use at to apply a trend only for rows at a specific factor level (e.g., first level of lR) and expand its contribution to the other levels within the same subject.

trend_at <- make_trend(
  par_names = c("m"),
  cov_names = list("covariate1"),
  kernels   = c("exp_incr"),
  phase     = "pretransform",
  at        = "lR"   # apply only at first level of lR, then expand within subject
)

This is useful when a covariate (or its effect) should only be applied to a specific level of a factor (most commonly lR), so that if the design is replicated across levels of the factor, the trend is only applied to the first level of the factor.

Multiple contributions to the same parameter

You can place multiple trend entries on the same target parameter. Their kernel outputs will be summed (after any optional per‑column map weights) before the base is applied.

trend_multi_same_par <- make_trend(
  par_names = c("m", "m"),
  cov_names = list("covariate1", c("covariate2")),  # second entry could also use par_input
  kernels   = c("exp_incr", "delta"),
  phase     = "pretransform",
  at        = "lR"
)

In the internal input matrix, covariate columns come first, followed by any par_input columns. The per‑column kernel contributions are summed row‑wise.

Phase per trend entry

Instead of one phase for all entries, you can provide a vector matching par_names:

trend_phases <- make_trend(
  par_names = c("m", "s", "t0"),
  cov_names = list("covariate1", "covariate1", "covariate2"),
  kernels   = c("lin_incr", "exp_decr", "pow_incr"),
  phase     = c("premap", "pretransform", "posttransform")
)

Non‑premap targets must be actual model parameter names (checked in the code), since these act after mapping.

Sharing kernel parameters (not only base weights)

Sharing can also target kernel parameters, not just the base weight. For example, share d1 across two parameters:

trend_shared_kernel <- make_trend(
  par_names = c("m", "s"),
  cov_names = list("covariate1", "covariate2"),
  kernels   = c("poly3", "poly4"),
  shared    = list(shrd = list("m.d1", "s.d1"))
)

After sharing, get_trend_pnames() will include the shared name once (here shrd) and remove duplicates.

Missing input values (NA handling)

Trend kernels ignore rows where any input used by the kernel is NA. For those rows, the kernel contributes zero. This applies to both built‑in and custom kernels. In effect:

  • Built‑in kernels evaluate only on “good” rows and expand results back; NA rows contribute 0.
  • Custom kernels receive only the subset of non‑NA rows; any NA in their outputs is coerced to 0 before being expanded back.

This behavior makes trends robust when covariates are intermittently missing (as exercised in tests that set some covariate entries to NA).

Trial‑wise evaluation and conditional covariates

  • Delta‑rule kernels (delta, delta2) are sequential by construction; they update across trials within a subject.
  • Using behavioral covariates (e.g., rt, response R, or outputs of functions in the design) can force a trial‑wise evaluation path so that data generation is not conditional on the observed data, but rather on the simulated outcomes.
# Example delta trend, capturing trial-wise dynamics
trend_delta <- make_trend(
  par_names = "m",
  cov_names = "trial_nr",
  kernels   = "delta",
  phase     = "pretransform"
)

design_delta <- design(
  factors    = list(subjects = 1, S = 1:2),
  Rlevels    = 1:2,
  covariates = "trial_nr",
  matchfun   = function(d) d$S == d$lR,
  trend      = trend_delta,
  formula    = list(m ~ lM, s ~ 1, t0 ~ 1),
  contrasts  = list(lM = matrix(c(-1/2, 1/2), ncol = 1, dimnames = list(NULL, "d"))),
  model      = LNR
)
## Intercept formula added for trend_pars: m.w, m.q0, m.alpha
## 
##  Sampled Parameters: 
## [1] "m"       "m_lMd"   "s"       "t0"      "m.w"     "m.q0"    "m.alpha"
## 
##  Design Matrices: 
## $m
##     lM m m_lMd
##   TRUE 1   0.5
##  FALSE 1  -0.5
## 
## $s
##  s
##  1
## 
## $t0
##  t0
##   1
## 
## $m.w
##  m.w
##    1
## 
## $m.q0
##  m.q0
##     1
## 
## $m.alpha
##  m.alpha
##        1
# Retrieve trial-wise parameters alongside generated data
# (not executed here)
# res <- make_data(p_vector, design_delta, n_trials = 10,
#                  conditional_on_data = FALSE,
#                  return_trialwise_parameters = TRUE)
# str(attr(res, "trialwise_parameters"))

Column names in the returned trial‑wise matrix are formed as parameter_inputName, where inputName comes from cov_names followed by any par_input names.

Vary trend parameters via the design formula

Trend parameters (base and kernel parameters) can vary by experimental factors using the formula argument of design(). Use get_trend_pnames(trend) to get the full names to place on the left-hand side of a formula. Names follow:

  • Base parameter: <targetParam>.w (for bases like lin, exp_lin, centered)
  • Kernel parameters: <targetParam>.<default_par_name> (e.g., d1, d2, … for poly*; q0, alpha for delta)

Premap example: vary a kernel parameter (d1) by lR

trend_premap <- make_trend(
  par_names = c("m", "lMd"),
  cov_names = list("covariate1", "covariate2"),
  kernels   = c("exp_incr", "poly2"),
  phase     = "premap"
)

design_premap <- design(
  data     = data,                   
  trend    = trend_premap,
  formula  = list(m ~ 1, s ~ 1, t0 ~ 1, lMd.d1 ~ lR),
  model    = LNR
)
## Intercept formula added for trend_pars: m.w, m.d_ei, lMd.d2
## 
##  Sampled Parameters: 
## [1] "m"              "s"              "t0"             "lMd.d1"        
## [5] "lMd.d1_lRright" "m.w"            "m.d_ei"         "lMd.d2"        
## 
##  Design Matrices: 
## $m
##  m
##  1
## 
## $s
##  s
##  1
## 
## $t0
##  t0
##   1
## 
## $lMd.d1
##     lR lMd.d1 lMd.d1_lRright
##   left      1              0
##  right      1              1
## 
## $m.w
##  m.w
##    1
## 
## $m.d_ei
##  m.d_ei
##       1
## 
## $lMd.d2
##  lMd.d2
##       1
# mapped_pars(design_premap)  # inspect mapped parameter structure

Pretransform example: vary the base weight (w) for s by lR

trend_pretrans <- make_trend(
  par_names = c("m", "s"),
  cov_names = list("covariate1", "covariate2"),
  kernels   = c("delta", "exp_decr"),
  phase     = "pretransform"
)

design_pretrans <- design(
  data     = data,
  trend    = trend_pretrans,
  formula  = list(m ~ 1, s ~ 1, t0 ~ 1, s.w ~ lR),
  model    = LNR
)
## Intercept formula added for trend_pars: m.w, m.q0, m.alpha, s.d_ed
## 
##  Sampled Parameters: 
## [1] "m"           "s"           "t0"          "s.w"         "s.w_lRright"
## [6] "m.w"         "m.q0"        "m.alpha"     "s.d_ed"     
## 
##  Design Matrices: 
## $m
##  m
##  1
## 
## $s
##  s
##  1
## 
## $t0
##  t0
##   1
## 
## $s.w
##     lR s.w s.w_lRright
##   left   1           0
##  right   1           1
## 
## $m.w
##  m.w
##    1
## 
## $m.q0
##  m.q0
##     1
## 
## $m.alpha
##  m.alpha
##        1
## 
## $s.d_ed
##  s.d_ed
##       1
# mapped_pars(design_pretrans)

Posttransform example: again vary s.w by lR

trend_posttrans <- make_trend(
  par_names = c("m", "s"),
  cov_names = list("covariate1", "covariate2"),
  kernels   = c("pow_decr", "pow_incr"),
  phase     = "posttransform"
)

design_posttrans <- design(
  data     = data,
  trend    = trend_posttrans,
  formula  = list(m ~ 1, s ~ 1, t0 ~ 1, s.w ~ lR),
  model    = LNR
)
## Intercept formula added for trend_pars: m.w, m.d_pd, s.d_pi
## 
##  Sampled Parameters: 
## [1] "m"           "s"           "t0"          "s.w"         "s.w_lRright"
## [6] "m.w"         "m.d_pd"      "s.d_pi"     
## 
##  Design Matrices: 
## $m
##  m
##  1
## 
## $s
##  s
##  1
## 
## $t0
##  t0
##   1
## 
## $s.w
##     lR s.w s.w_lRright
##   left   1           0
##  right   1           1
## 
## $m.w
##  m.w
##    1
## 
## $m.d_pd
##  m.d_pd
##       1
## 
## $s.d_pi
##  s.d_pi
##       1
# mapped_pars(design_posttrans)

Notes: - If a trend parameter is not listed in formula, an intercept-only formula is added automatically so it can still be estimated. - For non-premap phases, the target parameter names must exist in the cognitive model (e.g., m, s, t0 for LNR).

Interpreting Bases and Transforms

Trend parameters can have default transforms (from trend_help()), and custom kernels can declare their own transforms via register_trend(). These transforms feed into the model’s transform pipeline, ensuring parameters respect intended bounds.