Skip to contents

Cognitive Models in Plain brms

brms can fit some cognitive models on its own. wiener() is the classic four-parameter drift diffusion model, shifted_lognormal() a shifted reaction-time distribution, and exgaussian() the ex-Gaussian. They are among the most widely used and best tested families in the field, they need no extra package, and if one of them is the model you need and your data have been trimmed of anticipatory responses, plain brms is the simplest option.

But it comes with limitations:

  • A short list of models. No race models (LBA, RDM, LNR), no between-trial variability in the DDM, and no shifted distributions beyond the LogNormal.
  • Non-standard parametrisations. In exgaussian(), mu is the mean of the whole distribution, not of its Gaussian component as in the experimental-psychology literature, which is confusing (see Geller, Angele & Makowski, 2026).
  • Nothing for fast responses. Very fast responses (below ndt) are not allowed, and will make the fitting fail, which requires extra preprocessing (trimming the fast responses) and fidly parametrization.
  • Flat priors and random starts. Several parameters are left with flat priors, and chains start at random values that, leading to a lot of initialization failures.

This is why bespoke packages exist. They add models and make them convenient to use.

cogmod’s Design Principles

It just works

cogmod’s aim is to bring cognitive models within the familiar realm of regular brms models, so that the standard Bayesian workflow (fit, check, compare, summarise, report, predict, etc.) “just works” on them. A cogmod model is a brm() call. The family goes in bf() like any brms family, and the only extra bit is the need for a cogmod_stanvars(), which adds the necessary Stan functions.

Transparency

Nothing sits between you and the model. brms::stancode(f, data = df, stanvars = cogmod_stanvars(f)) prints the whole program, family functions included, and that program is the one that gets compiled. Priors and starting values are not applied behind the scenes: cogmod_priors() and cogmod_inits() return ordinary brms objects that you pass to brm() yourself, so they are written into every script, and can be printed, inspected, overwritten and edited there. The source code carries the reasoning for every prior, link and constant.

Lightness

cogmod is a set of families, not a layer. It defines no class for fitted models and no methods for brmsfit. Random-effect structures, smooths, within-chain threading, update() and, on the RT-only families, cens() work just as for any other brms models.

Inside the package, each family is defined once. A single registry entry generates its Stan code, its R density and random generator, its log_lik(), posterior_predict() and posterior_epred() methods, its parameter checks and its priors, so the R and Stan sides cannot drift apart. The densities are exported (dcogmod_*(), rcogmod_*() and, for most families, pcogmod_*()), so a model can be simulated or used in a parameter-recovery study without fitting anything.

A Minimal, Consistent API

cogmod’s interface has essentially four functions. The family and cogmod_stanvars() are required. cogmod_priors() and cogmod_inits() are optional. The pattern is the same for every family.

brms’ Quirks Are Inherited Too

Some of brms’s conventions and idiosyncrasies apply. Parameters are named the way brms names them: mu for the main parameters, even if they are drift rates, as in brms’s own wiener(). A parameter written in bf() lives on its link scale, while one left out is estimated on its natural scale. The best way to learn how to use cogmod is to learn how to use brms, which is the point of the package.

Post-Processing by the Packages That Do It Best

cogmod has no summary(), plotting, marginal-effects or reporting functions of its own. It implements what brms asks of a family, and everything downstream builds on those: loo, pp_check() and bayesplot, and the packages built for brms models, such as easystats (parameters, modelbased, performance, bayestestR, report), marginaleffects and emmeans.

parameters::parameters(m)

# Non-decision time by condition, and its difference between conditions
marginaleffects::avg_predictions(m, by = "Condition", dpar = "ndt")
marginaleffects::avg_comparisons(m, variables = "Condition", dpar = "ndt")

loo::loo_compare(loo(m_brms), loo(m))

These packages are maintained by people who specialise in them and are used on every kind of brms model, so a cogmod fit benefits from each of their improvements without a line of cogmod changing. Being also the coauthor of easystats, I (Dominique Makowski) made sure that its awesome post-processing features also work seamlessly for cogmod models.

Speed, Measured

The giam of cogmod is to fit these models correctly and as fast as they can be fitted, and neither is assumed:

  • Gradients are checked, not only values. An R density and a Stan density can agree on every value while the gradient the sampler follows is wrong. On every pull request, a continuous-integration job compares each family’s gradient with finite differences over a grid that includes responses in the far tails.
  • Every change to a Stan density is timed against the version it replaces, and a slower gradient is treated as a regression and investigated.
  • Features and documentation is there to help speed. cogmod_inits() starts chains in a sensible region, and cogmod_warmstart() reuses the adaptation of one fit for the next. The Performance article covers the rest.

Alternatives

bmm

bmm (Frischkorn & Popov, 2025) is a great atlernative, also built on brms, and differs from cogmod in design rather than in quality. It puts an interface between the user and brms. A model is written with its own formula function, bmf(), using the parameter names of the model’s literature, with the response variables named in the model constructor. bmm() translates this into a brms formula, family and Stan code, checks the data (reaction times in milliseconds, responses not coded 0/1), applies model-specific priors and starting values, calls brm(), and returns a bmmfit with methods of its own: a summary(), a conditional_effects() that shows parameters on their native scale, a pp_check() that can check every observable of the likelihood. Contamination is handled before the fit, with flag_contaminant_rts() identifying fast guesses and attention lapses for you to remove.

That is a full solution from beginning to end, in one vocabulary. It suits users who are new to these models and want one package to take them from data to a reported fit, without necessarily wanting cognitive models to sit alongside their other brms models in the same workflow. The costs are the mirror of cogmod’s choices: a layer and a set of methods that have to keep pace with brms, and conveniences that stay within the package.

Outside brms

EMC2 fits hierarchical DDM, LBA, RDM and LNR models with particle-based samplers in R, using its own design language and post-processing. HSSM (Python) adds likelihood approximation networks for models without a closed-form likelihood. rtdists provides densities and random generation for the DDM and LBA, without fitting.

Choosing

  • Plain brms if its native families cover your model (shifted LogNormal, ex-Gaussian with its overall-mean parametrisation, four-parameter DDM) and your data have been trimmed of anticipatory responses.
  • bmm for visual working memory, M3, EZ-diffusion from summary statistics, or if you prefer an integrated toolkit with a model-specific vocabulary.
  • cogmod for race models, the DDM with between-trial variability, a choice of RT distributions to compare, rating-scale models, or outliers modelled inside the likelihood, and whenever you want the model to remain a plain brms call.