This is the 1.4.0 release resubmitted: 1.4.0 was sent to CRAN on 2026-10-15 and returned by the incoming checks because its check took too long on Windows. 1.4.1 runs fewer tests on CRAN and is otherwise the same package.
sdt_yn) for detection and old/new recognition tasks with
aggregated response counts. It estimates sensitivity (d),
the decision criterion and optionally the unequal-variance ratio
(sdratio). The criterion is estimated as
criterion, measured from the noise mean, or as
bias, measured from the midpoint between the means: write
the one your design holds constant, e.g. criterion for weak
and strong targets tested against shared lures (#543). Also adds
dsdt_yn(), rsdt_yn(), sdt_d(),
sdt_criterion() and sdt_bias(). See
?sdt_yn for the parameters, links, default priors and the
designs that identify sdratio. Thanks to @GidonFrischkornsdt_mafc) for accuracy-only m-AFC tasks,
where one of m alternatives carries the signal (DeCarlo,
2012). It estimates sensitivity (d) from response counts
and has no bias parameter. m may be a constant or the name
of a data column, so trials with different numbers of alternatives can
be fit jointly. Also adds dsdt_mafc() and
rsdt_mafc(). See ?sdt_mafc for the parameters,
the noise distributions, the default priors and how d
relates to the sensitivity sdt_yn() reports. Thanks to
@GidonFrischkornsdt_ranking) for tasks where observers rank m
items, one of them a studied target, by how strongly they recognise them
(Meyer-Grant et al., 2026). It estimates sensitivity (d)
from the target’s rank counts, with Gumbel ("gumbel_min")
or Gaussian ("normal") noise. m may be a
constant or the name of a data column, so trials with different set
sizes can be fit jointly. Also adds dsdt_ranking() and
rsdt_ranking(). See ?sdt_ranking for the
parameters, the response format and the default priors. Thanks to @GidonFrischkornsdt_rating) for old/new recognition and detection tasks
with ordered confidence ratings, aggregated to counts per rating
category. It estimates sensitivity (d), the criterion (as
criterion or bias, as in
sdt_yn()) and the confidence thresholds, and optionally the
unequal-variance ratio (sdratio);
threshold_type chooses how the thresholds are
parameterized. Also adds dsdt_rating() and
rsdt_rating(). See ?sdt_rating for the
parameters, the threshold types, the default priors and how
d relates to the sensitivity sdt_yn() reports.
Thanks to @GidonFrischkornsdt_rating(), selected with
version: "dpsdt", the dual-process model
(Yonelinas, 1994) with recollection of old items (Ro) and
recall-to-reject of new items (Rn; as in Yonelinas, 2024),
and "metad", the meta-d’ model, which estimates
metacognitive efficiency as the log M-ratio logmratio
(Maniscalco & Lau, 2012). Both reduce to the standard model while
their extra parameters are off. Also adds mratio(),
dsdt_dpsdt(), rsdt_dpsdt(),
dsdt_metad() and rsdt_metad(). See
?sdt_rating. Thanks to @GidonFrischkornsdt_cdp) for old/new recognition with Remember/Know
judgments (Wixted & Mickes, 2010). It estimates familiarity
(dfam) and recollection (drec) sensitivity,
the old/new criterion, the confidence thresholds and the
Remember criterion (rcrit) from aggregated response counts;
the recollection SD (sigmar), the familiarity-recollection
correlation (rho) and a Know/Guess criterion
(kcrit) can be freed through the formula. Also adds
aggregate_sdt_cdp_data(), dsdt_cdp() and
rsdt_cdp(). See ?sdt_cdp for the parameters,
the response format and how drec relates to the sensitivity
sdt_yn() reports. Thanks to @GidonFrischkornmpt) for categorical responses from discrete processing
states, such as the two-high-threshold model of recognition. Write each
tree’s branches with mpt_tree(), as formulas or as a named
list of strings, name a shared parameter alike in every tree, and fit
the model to response counts, one row per participant and tree, with
tree_id naming each row’s tree. links sets a
logit or probit link for all or single parameters; parameters can be
fixed in the formula (bmf(g = 0.5)) or given non-linear
formulas. Also adds dmpt() and rmpt(). See
?mpt (#255). Thanks to @GidonFrischkornmpt() takes restrictions that equate
parameters or fix them to constants, in MPTinR and TreeBUGS syntax
(c("Dn = Do", "g = 0.5"), or readLines() of
such a restrictions file), and simplex for parameters that
sum to 1, such as guessing rates. A simplex group’s default uniform
prior gives each of K members prior mean 1/K whatever their order;
?mpt shows how to get the per-branch uniform priors of
TreeBUGS, which give the first member prior mean 0.5. Printing the model
lists the restrictions, the simplex groups with their prior and an
identifiability check (#255). Thanks to @GidonFrischkorncovariates, for probabilities the design fixes, such as
guessing rates that depend on the number of response options;
bmm() stops when a row’s values make the branches of a tree
not sum to 1. Response categories a tree cannot produce, such as
distractor responses on trials without distractors, are declared with
impossible in mpt_tree() and in the importers.
dmpt() and rmpt() take covariate values too.
See ?mpt_tree (#255). Thanks to @GidonFrischkornmpt_from_string() and
mpt_from_eqn() build an mpt() model from
MPTinR-style model text (a string or readLines() output) or
from an EQN file (multiTree, MPTinR, TreeBUGS); both take
restrictions as their second argument.
mpt_from_eqn() renames and reports names mpt()
cannot use, such as D_o; it takes restrictions in the
file’s names, maps per-tree category labels onto shared categories with
categories, and numeric tree labels with
tree_names. Unlike MPTinR and TreeBUGS, bmm reads an EQN
file’s first line unless it is the equation count, so start a title line
with # or delete it. See ?mpt_from_eqn (#255).
Thanks to @GidonFrischkornmpt_data_reshape() reshapes the wide
frequency tables of MPTinR and TreeBUGS, one row per participant and one
column per category, into the one-row-per-participant-and-tree layout
bmm() expects, with 0 for the categories a tree cannot
produce. Columns that are no counts, such as a group factor, are carried
to every row; columns = "mptinr" reads the unnamed, sorted
columns of MPTinR data. See ?mpt_data_reshape. Thanks to
@GidonFrischkornplot() draws the processing-tree diagram of an
mpt_tree(), or of every tree of an mpt()
model, from the branch expressions, and marks restricted parameters on
their edges. See ?plot.mpt_tree (#255). Thanks to @GidonFrischkornmpt() models,
constraining parameters, design-fixed covariates, a fit to real data,
non-linear parameters, importing model files and migrating from MPTinR
or TreeBUGS (#255). Thanks to @GidonFrischkornbroeder_schuetz_2009_e3, binary
old/new recognition data from Broeder & Schuetz (2009, Exp. 3), with
five base-rate conditions from 40 subjects. See
?broeder_schuetz_2009_e3.meyer_grant_jakob_2025, ranking
data from Meyer-Grant & Jakob (2025): 60 subjects ranking 3, 4 or 5
faces, aggregated to the target’s rank counts per subject and set size.
See ?meyer_grant_jakob_2025.ppp() computes the goodness-of-fit
statistics T1 (category frequencies) and T2 (their covariances across
participants) of Klauer (2010) with their posterior predictive p-values,
the checks TreeBUGS’ PPP() reports, for mpt(),
m3() and the SDT count models.
level = "participant" gives one T1 per participant. See
?ppp (#497).m3() accepts choice_rule = "gaussian", a
Thurstonian rule: every candidate gets independent normal noise and the
strongest one is chosen ("softmax" is the same with Gumbel
noise). Activation estimates are on a smaller scale than under
"softmax", and conclusions about c against
a, or about conditions that change the number of candidates
such as set size, can differ between the rules. Fits take much longer
than with "softmax". The Gaussian rule requires whole
numbers of options. See choice_rule in ?m3.
Thanks to @GidonFrischkornbmm_setup() checks whether your machine
can fit models: the C++ toolchain, cmdstanr with CmdStan,
rstan, and the backend bmm() will use. It then
compiles and samples a small test model and gives one fix for every
check that failed. When bmm() fails because the compiler or
the Stan backend is missing or broken, the error now points to
bmm_setup() (#431).roc_sdt(), roc_observed() and
auc_sdt() return the model-implied and observed ROC and the
area under it for sdt_yn and sdt_rating;
latent_sdt() the latent evidence distributions of all four
models; sdt_thresholds() the confidence thresholds of
sdt_rating; and sdt_sensitivity() the
sensitivity on the noise, signal or root-mean-square SD scale. All but
roc_observed() print a summary; plot() draws
the ROC with the observed points, the AUC and the latent distributions;
summary() says when d is d_a. The new
article Signal Detection Theory models walks through all four
models and these functions (#370). Thanks to @GidonFrischkornbmm(file_refit = "on_change") now works instead of
warning and falling back to "never". The cached fit is
returned only while the Stan code, the Stan data, the factor levels of
the model variables and the algorithm are unchanged; any change refits.
As in brms, sampler settings are not compared: rerunning
with a higher adapt_delta, more iter, another
seed or save_pars(all = TRUE) returns the
cached fit unchanged. Use file_refit = "always" in those
cases (#411).bmm_options(file_refit = ) accepts the same values as
bmm(). It previously required a logical, so the string
forms could be set only through options(bmm.file_refit = )
(#411).update() gained the file and
file_compress arguments. file writes the
updated fit in the same order bmm() does, i.e. after the
bmm postprocessing (#411).prior_info() reports, for each parameter
of a fitted model, its link function, the prior actually used on the
sampling scale, and whether it was a bmm default, a brms default, or
user-specified; parameters left with improper flat priors are flagged.
format = "text" produces methods-section-ready sentences.
Parameters that exist only because the family machinery requires them
are omitted, so the report lists the model’s own vocabulary: the
mu brms forces on the response-time custom families, the
mu2/kappa2 second component of a
brms::mixture() family, and the theta2
mixture-weight reference the sampler holds at zero (#391).model_citation() returns the references of
a model or a fitted model, one per element, for your reference list.
Fits saved with an older version of bmm get the current references. The
model references now include their DOIs, and several were corrected,
e.g. the year of Miller et al. for cswald() is 2018, not
2017. If you copied a reference from a help page, check it again. The
help pages of ezdm() and sdt_mafc() list their
two references as separate items (#476).links declaration in a
.model_*() constructor, or through a single softmax group
that is active on every row of the data, so most new models are
supported without writing a method.
native_transform.non_targets() ships as the worked example
of a design-dependent method (#388).bmm_data_check() prints a report on your
data before fitting, without compiling the model. It runs the same
checks as bmm(), collects all errors, warnings and
messages, and summarizes the response variables with the coding the
model expects, the data columns each parameter formula uses, the number
of observations per design cell, and common data mistakes, such as
circular responses in degrees or misplaced NA values in
nt_features. Developers can add model-specific checks with
data_check_findings() methods and
data_check_finding() (#389).student_t(3, 0, 2.5) default
of brms, which on a log link lets individual parameters
vary by a factor of 12 around the group value. The priors are
exponential() on the link scale, with rates from 1 to 4
depending on the parameter; prior_info() shows them for a
fit. Estimates from models with random effects can differ from earlier
versions. To change a prior, address the parameter with
dpar or nlpar,
e.g. set_prior("exponential(2)", class = "sd", nlpar = "kappa")
(#342).lkj(2)
default prior instead of the uniform lkj(1) of
brms. For two correlated effects this lowers the prior
probability of a correlation beyond ±.9 from 10% to 1.45%, so estimated
correlations shrink slightly towards zero. The default applies only to
models that estimate a correlation matrix, not to (1 | ID)
or (x || ID). To return to the previous behaviour, pass
prior = set_prior("lkj(1)", class = "cor").
prior_info() shows the prior as lkj(2), class
cor, as written in set_prior() (#417).bmm() now starts every model from tight initial values
for its random effects, and mixture2p,
mixture3p and imm start their
population-level parameters near the centre of their default priors;
these models and m3 previously used
init = 1. This reduces the
lkj_corr_cholesky_lpdf: Random variable[k] is 0,
von_mises_lpdf: Scale parameter is inf and
Rejecting initial value messages at the start of warmup.
Posteriors are unaffected. A user-supplied init still
replaces the bmm default.bmm() and update() now start Stan’s
step-size search at 0.01 instead of 1, which further reduces the
lkj_corr_cholesky_lpdf: Random variable[k] is 0 and
von_mises_lpdf: Scale parameter is inf messages at the
start of warmup. The adapted step size and the posterior do not change.
A step_size (or stepsize) in your own
control list wins, and
bmm_options(step_size = FALSE) restores Stan’s default for
new fits; update() keeps the step size a fit was run
with.pp_check() can now check every observable of a model’s
likelihood, not just the primary response. brms’s
pp_check() only plots the brms Y variable, so
the ddm and cswald responses and the
ezdm RT variance and accuracy went unchecked. The new
resp_var argument selects the observable
(e.g. pp_check(fit, resp_var = "response")), including
derived ones ("signed_rt" for the RT models;
"mean_pc", the proportion of upper-boundary responses, for
ezdm), and resp_var = "all" returns a panel of
all checks drawn from one shared joint simulation.
pp_check_vars(fit) lists the available checks, the plot
type each uses by default, and the brms standata slots it reads; model
authors declare observables via the
pp_observables()/pp_simulate() S3 generics.
The ezdm checks show how each statistic is distributed
across design cells: density overlays for the RT statistics and a
histogram for the proportion of upper responses (the new
type = "bars_binned"); type = "intervals"
compares each cell with its own predictive interval instead. Where an
observable is undefined for some cells (an ezdm
boundary reached by fewer than two responses), observations are dropped
only when the observed value is undefined; undefined
simulated values are absorbed by dropping those posterior
draws, so the number of observations checked does not depend on
ndraws (#401).bmm() says that a function passed as data
is not coercible to a data frame also while the plyr namespace is loaded
(a pp_check() loads it); it used to say the data has no
observations.brms::kfold() and reloo() now run on
m3, sdt_rating,
sdt_ranking, sdt_cdp,
sdt_mafc (with m a column),
mixture3p and imm fits, and so does
update(fit, newdata = fit$data[rows, ]). They stopped
before the first refit with
Response columns ... missing in the data,
The response variable(s) ... missing in the data or a
complaint about set_size, because update()
checked the fit’s stored data as if you had passed them. No refit and no
change to your calls is needed.set_prior("normal(0, 2)", class = "b", nlpar = "kappa"),
did not reach its intercept in mixture2p,
mixture3p, imm and
m3: the intercept kept the bmm default, and
brms warned that your prior would not be used. Your prior
now applies to the intercept too. A prior with
coef = "Intercept" still sets the intercept alone. Refit
models that used such a prior (#526).update() with a formula. that adds
predictors to a parameter fitted with an intercept only gave the new
coefficients a flat prior instead of the bmm default, in models such as
mixture2p, mixture3p,
imm and m3. With
0 + predictor, the whole parameter was left without a
prior. They now get the priors bmm() gives them. Rerun such
updates.file = in
bmm() or update() or with
saveRDS(), held extra copies of the data and of the fitted
model. The file could be several times larger than needed, and more so
for a data frame with many columns the model does not use. New fits are
saved at their own size. A fit saved by an earlier version keeps its
size until you refit it with bmm() or with
update(recompile = TRUE); update() alone may
carry the copies over (#518).conditional_effects() no longer fails with
Number of trials must be non-negative integers on
m3 fits whose rows differ in their number of trials
(#510).conditional_effects() on m3 fits
ignored conditions, int_conditions,
surface, spaghetti, draw_ids and
re_formula = NULL without a warning, and returned a wrong
interval for probs. They now apply, as they do for
sdt_rating, sdt_cdp and
sdt_ranking. The estimate is the posterior median and
se__ its MAD, as in brms; m3
used to report the mean and standard deviation, and
robust = FALSE returns them (#512).conditional_effects(spaghetti = TRUE) drew its lines on
a different scale than the curve for some parameters: on the sampling
scale at the default scale = "native"
(e.g. kappa and thetat of
mixture3p) and on the native scale at
scale = "sampling" (e.g. kappa and
c of sdm). The lines now follow
scale. Redraw plots made with spaghetti = TRUE
(#512).conditional_effects() now stops with a message naming the
argument if you pass categorical, ordinal,
select_points, transform or
method = "posterior_predict", which do not apply to their
latent parameters. m3 used to ignore them.
plot(points = TRUE) on m3 failed with
Column resp__ not found and now works (#512).conditional_effects() on the mixture3p
parameters thetat and thetant at the default
scale = "native" returned probs with lower
above upper, ignored draw_ids and
re_formula = NULL, and with ndraws combined
draws that did not belong together. All four now work. Default output is
unchanged (#512).summary() of m3,
sdt_rating, sdt_ranking and
sdt_cdp fits left the name of the data blank
(Data: (Number of observations: 120)). It now names the
data passed to bmm(). Refit to get the name into the
summary; fits saved earlier keep the blank.native_parameters() failed with
Cannot build a prediction grid: variable(s) ... are not columns of the model data
whenever a parameter had a non-linear formula,
e.g. bmf(c ~ exp(nlc), nlc ~ 1 + set_size). It now returns
the grid over the predictors of every formula involved, as it does for
linear formulas.native_parameters() failed with
Cannot build a prediction grid: variable(s) 'p' are not columns of the model data
for a fit whose formula links random effects across parameters,
e.g. (1 |p| id), and for a nested grouping such as
(1 | g1/g2). For m3 fits,
conditional_effects() also listed the variables of a nested
grouping as effects. bmm_data_check() reported the ID
p as a missing data column. All three work now.bmm() and default_prior() no longer warn
that flat priors will be used when a parameter that has no bmm default
prior gets a non-linear formula. The warning now appears only when a
formula replaces a default prior. The parameters inside such a formula
still get flat priors unless you set them: in m3 with
version = "ss", b ~ exp(nlb) leaves
nlb flat.posterior_epred() failed with
object 'posterior_epred_ddm' not found (or the name of
another model) on ddm, cswald,
ezdm, sdt_yn,
sdt_mafc and sdm fits, and printed the
same message inside performance::model_performance() and
report::report(). It now returns the expected response of
these models, also for fits saved by earlier versions;
?posterior_epred.bmmfit says what that is for each model.
For sdm, mixture2p,
mixture3p and imm it and
fitted() stop with an error pointing to
native_parameters(), because the mean of a circular
response error is not useful (#475).links argument that names no
parameter of that model is now an error instead of being added:
sdm(resp_error = "y", links = list(kapa = "identity"))
reported a parameter kapa while kappa kept its
default link. A name one edit from a parameter is read as that
parameter, with a warning; a link allowing values the default excludes
warns once. A link the model cannot apply is refused too, naming those
it can: links = list(bound = "loglog") used to fail with
argument is of length zero. sdm,
mixture2p, mixture3p and
imm take none (#420).mu is predicted without an intercept
(Initial values for vectors are only specified for b-coefficients, sd and z parameters).
sdm, ddm, ezdm and
cswald no longer fail with the rstan backend when a
parameter has exactly one slope (no more scalars to read),
nor when their random effects use gr(..., by = ).prior_info() no longer fails for a fit whose formula
needs data2, e.g. (1 | gr(ID, cov = A))
(Object 'A' was not found in 'data2').prior_info() now names every kind of correlation prior
the way set_prior() documents it, not only the one on
group-level effects. A fit with two or more me() terms was
reported as class Lme with
lkj_corr_cholesky(1), brms’s internal spelling, instead of
corme with lkj(1). The same applied to
rescor, lncor and cortime.update() with newdata or a new
formula. no longer fails before sampling for a model that
starts from bmm’s initial values (no more scalars to read
on rstan, Fitting failed on cmdstanr). The initial values
are now built for the data and formula the update fits; an
init passed to update() still wins
(#415).mo() or s() terms no longer
start with a partial init list, which made cmdstanr print
Init values were only set for a subset of parameters.
extract_parameter_dimensions() now reads a declaration
sized by an element of a data array, such as
simplex[Jmo_c[1]], and simplex parameters get an initial
value.file_refit no longer accepts a logical that is not a
single TRUE/FALSE. NA,
logical(0) and multi-element logicals were coerced to
"never", so file_refit = cfg$refit with a
missing or NA config key silently returned the cached fit
instead of erroring as it did before "on_change" was added
(#411).file_refit = "on_change", a cached fit that
restructure() cannot bring forward to the current bmm
version is now refitted rather than aborting bmm() with
“Unable to restructure the object… Please refit” — that is the
one thing "on_change" exists to do. A cached fit with no
algorithm field also no longer aborts on
brms’s bare stopifnot(); the comparison falls
back to the Stan code, Stan data and factor levels (#411).file_refit silently falling back to
"never" for any capitalisation other than all-lowercase.
"Always" passed validation, which lowercases, but the
coercion that followed it did not, so
bmm(..., file = , file_refit = "Always") returned the
cached fit instead of refitting (#411).update(fit, file = ) now saves a bmmfit.
It used to save a plain brmsfit that bmm()
refused to read back
(Object loaded via 'file' is not of class 'bmmfit'). When
the file already existed, update(fit, newdata = , file = ),
as in a script that caches its fits, silently returned the old fit
instead of the updated one. Refit anything produced this way
(#411).options(bmm.default_priors = FALSE) making
bmm() fail for every model instead of fitting with flat
priors. set_default_prior() returned NULL,
which combine_prior() indexed unconditionally
(second argument must be a list);
set_default_prior() now returns an empty prior, and
combine_prior() passes a NULL argument through
(#391).update() now configures the likelihood for the
threading spec that will actually be used.
brms::update.brmsfit falls back to the original fit’s
threads when the argument is not passed, but bmm only
inspected the new request, so updating a threaded fit without repeating
threads emitted the serial likelihood chunk into threaded
Stan code — for the sdm model a compile error
(Identifier 'COSN' not in scope), and for any
loop = FALSE custom family a silently mis-sliced
likelihood. The fallback follows brms in distinguishing an absent
threads argument from an explicit
threads = NULL, which turns threading off.update() no longer lets a global
options(brms.threads = ) reach the likelihood
configuration. Updating an unthreaded fit under a session-wide threading
option emitted the sliced likelihood chunk while brms generated serial
Stan code (Identifier 'start' not in scope); the spec of
the fit being updated now always wins, as it does in brms.update() now re-resolves every parameter whose constant
the new formula changes. update() never called
check_model(), so the constant was never resolved again and
the original fit’s prior overrode the freshly configured one:
update(fit, bmf(..., mu ~ 1)) returned a model in which
mu was still pinned,
update(fit, bmf(..., kappa = 5)) one that reported
kappa = 5 while Stan estimated kappa freely,
and update(fit, bmf(..., mu = 0.5)) one that reported
mu = 0.5 while Stan kept mu at the original
value — all three with no error or warning.ezdm(version = "4par") no longer drops cells in which a
boundary was reached fewer than twice or has no RT summaries.
brms excluded the whole row, response counts included, so
the cells with the most extreme accuracy were missing.
bmm() now keeps them and warns how many there are; refit
such models. newdata for log_lik() or
predict() now needs the columns rt_used_upper
and rt_used_lower, as in fit$data. Where some
cells have no usable RT summaries at the upper boundary,
pp_check() without resp_var leaves those cells
out and no longer accepts newdata or the loo_*
types (#430).dezdm(version = "4par") now returns the density without
a boundary’s response-time terms where that boundary’s summaries are
NA, instead of NA (#430).ezdm_summary_stats(method = "mixture") now returns
n_upper and n_trials for the responses its
mean_rt/var_rt are based on. It previously
returned the raw counts beside cleaned moments, so every cell told
ezdm it rested on more responses than it had and its
posterior came out too narrow. Summary statistics change and fits are
not reproduced; recompute them and refit. Use guess_rate to
set the accuracy expected of a contaminant response
(version = "3par" only). A cell whose accuracy
guess_rate cannot explain is corrected without it, and
warns. adjust_ezdm_accuracy() is deprecated — applying it
now removes the same contaminants twice (#423).bound, came out too narrow. Existing
ezdm fits are not reproduced: in our simulations,
credible intervals for subject-level bound estimates widen
by 20% to 75%, and fitting takes longer (see ?ezdm_dist). A
fit cached with bmm(file = ) is reused unless
file_refit = "on_change". rezdm() no longer
truncates mean_rt at ndt, so with very few
trials it can return mean_rt <= 0, which
bmm() rejects (#407).NaN in
dezdm(), rezdm(), log_lik() and
pp_check(), and both models could return a log-likelihood
of -Inf when the predicted accuracy was very close to 1 but
a cell contained errors. Near zero drift, the likelihood jumped where
the code switched between formulas, and the response counts said nothing
about the direction of drift. dezdm() now rejects counts
that are not whole numbers (#407).thetat (mixture2p)
and thetat/thetant
(mixture3p) had no effects prior, so any
regression coefficient on them was flat. They now get
normal(0, 2.5) on the logit/softmax scale (#305,
#466).mu (sdm)
and mu1 (mixture2p,
mixture3p, imm) had a
student_t(1, 0, 1) intercept prior, uniform over the circle
under the tan_half link, and no effects prior,
which gave non-reference levels a bimodal prior on the native scale.
They now get normal(0, 0.5) on the intercept (95% of the
prior bias within 89 degrees of the target),
normal(0, 0.25) on effects (a condition difference with SD
24 degrees) and exponential(4) on random-effects SDs.
mu/mu1 stay fixed at 0 by default, so this
only affects models that free them (#306).NaN
there, which the sampler reported as “Log probability evaluates to
log(0)” or as divergent transitions, and could bias the posterior
for data with long tails. Fits of such data change; refit to benefit
(#387).threads = NULL while
options(brms.threads = ) is set no longer produces Stan
code that fails to compile
(Identifier 'start' not in scope) for the
sdm and cswald models.
threads = NULL now turns parallelization off, as it does in
brms.rsdm() and rmixture2p() now draw each
value from its own parameter values when a parameter has one value per
draw. Before, every value came from the mix of all parameter values, so
posterior_predict() and pp_check() for
sdm fits were too wide. Fits are unaffected; rerun
posterior_predict() or pp_check().
rsdm() no longer fails for large c and
kappa, and rmixture2p() accepts vectors of
kappa and p_mem.
rejection_sampling() takes arguments of length
n in ... per draw and requires a whole-number
n. The same seed now gives different draws from
rsdm(), rmixture2p(),
rmixture3p() and rimm() (#445).update() without newdata no longer fails
for m3 fits (The response variable(s) corr, other,
dist, npl missing in the data) or for mixture3p
and imm fits whose formulas do not use
set_size (The set_size variable ‘set_size’ must be
either a variable in your data or a single numeric value), and no
longer warns for sdt_yn fits that the reserved column
dist_type will be overwritten (#429).update() without newdata no longer fails
for mixture3p and imm fits in which
every response at the largest set size is NA (The
number of columns for non-target values in the argument ‘nt_features’
should equal max(set_size)-1). Fits saved before this fix can be
updated as they are; the workaround of passing the original data as
newdata is no longer needed (#459).num_options when
its numbers are named after the response categories, as in
num_options = c(corr = 1, other = 4, dist = 5, npl = 5).
Each category’s activation was combined with itself instead of its
number of options, with no error or warning. Such names are now matched
to the categories, in any order; names already used by a data column or
a parameter are an error. If fit$formula$formula shows a
category twice, as in log(corr * corr), refit the model,
e.g. with update(fit) (#449).num_options with the wrong response categories when they
are named after the categories in another order than
resp_cats, as in
num_options = c(other = "n_other", corr = "n_corr", dist = "n_dist", npl = "n_npl").
Each category silently got another category’s column. Such names are now
matched by name; m3() refuses partly or repeatedly named
column names and NA among numeric counts. If
fit$formula$formula shows the wrong column,
e.g. log(other * n_corr), refit with bmm() on
your original data; update(fit) without
newdata stops when it cannot rebuild such a fit
(#457).dm3() and rm3() no longer mix up the
response categories when the activation formulas are not listed in the
order of resp_cats. Each number of options was applied to
whichever activation stood at its position, so densities and simulated
counts were wrong; rerun such calls. Fitting with bmm() was
not affected. The columns of rm3() and the order of
x in dm3() now follow
resp_cats.version = "custom" now stops
when a parameter in an activation formula has no formula of its own,
such as c in corr ~ b + a + c without
c ~ 1. Before, such a parameter was fitted with only an
intercept, announced only by the message “No formula for parameter c
provided”. Scripts that relied on this now fail; add the missing
formula, e.g. c ~ 1. Numeric num_options can
be used by name, n_opt_<category> unless you named
them, and nTrials and Idx_<category>
work in non-linear activations. Earlier fits are unaffected (#495).NA) in a row where its category has options, and says how
many were counted as 0. Missing counts where the category has no
options, as for dist in
oberauer_lewandowsky_2019_e1, are still read as 0 without a
warning. Data columns named Y, nTrials or
Idx_<category> are now refused, not silently
replaced; rename them. Response categories may still be called
Y or nTrials. Missing values in
num_options columns now give an error naming those columns
instead of “missing value where TRUE/FALSE needed”; enter 0
where there were no options (#496).log_lik() for cswald fits no longer
returns NaN for a posterior draw whose ndt is
at or above an observation’s RT, which happens with newdata
such as held-out folds. Such draws now give -Inf, or
0 for errors in the "simple" version, matching
the likelihood the model was fitted with. kfold() and
loo::elpd() no longer break on the common case of a few
such draws; an observation whose RT is below every posterior draw of
ndt can still return a non-finite estimate, which reports a
real mismatch with the data, not this bug. Rerun affected computations
(#453).log or softplus link on drift
(Variable 'Intercept_drift' has NA values) (#460).imm(), sdm() and ezdm() now
refuse an unknown version when the model is created, with
an error that lists the valid ones. Before, imm() accepted
it and bmm() then failed with
no applicable method for 'configure_model',
ezdm() failed only inside bmm(), and
sdm() silently fitted the "simple" version, so
such sdm() fits need no rerun. These three and
m3() also accept an unambiguous abbreviation, such as
version = "f" for "full" (#467).brms::combine_models() now merges a fit with a refit
made by update(), e.g. update(fit, seed = 2)
to rerun chains that failed. Before, it stopped with “Models 1 and 2
have different formulas”. Both fits must come from the same bmm
version, so rerun update() refits made with an earlier
version (#464).tryCatch(message("hi"), message = identity). Printing it
failed with argument 1 (type ‘list’) cannot be handled by ‘cat’
(#481).adjust_ezdm_accuracy() adjusts one cell at a time.
Passing vectors failed with an unrelated R error; it now stops with a
message that points to ezdm_summary_stats(), which handles
grouped data. The function remains deprecated (#409).pp_check() for the RT models silently producing a
misleading plot with negative_rt = TRUE: brms forwarded the
argument to posterior_predict() (signed predicted RTs)
while the observed response times stayed unsigned, so the plot looked
like severe misfit. pp_check() now checks the
"signed_rt" observable (with a message) so both halves are
signed, and errors for models without signed RTs (#401).dv argument of the internal
ezdm posterior_predict functions and its
documentation. posterior_predict(fit, dv = "var_rt")
silently returned mean_rt: brms forwards
posterior_predict() dots to
prepare_predictions() only, never to the family’s
prediction function, so dv was dropped without a warning
and has never worked. Use pp_check(fit, resp_var = ...)
instead (#401).rezdm(version = "4par") crashing (recycling errors)
when some but not all simulated cells produced fewer than 2 responses at
the upper boundary — the upper-boundary branch did not subset its
moments and non-decision time by the affected cells, while the
lower-boundary branch did. The bug was invisible whenever every cell
shared the same parameters (the unindexed vectors are then constant),
and is reachable through rezdm() directly; fixing it is a
prerequisite for the multi-observable pp_check(), which
calls rezdm() with per-draw posterior parameters
(#401).pp_check(group = ) (e.g. dens_overlay_grouped)
being silently dropped when type was not supplied, so that
brms fell back to the ungrouped type and bayesplot warned about an
unrecognized group argument.parameters() is deprecated and will be removed in bmm
1.6.0. Use parameter_info() instead: it takes the same
arguments and returns the same table. Until then
parameters() keeps working and warns. The
parameters package exports a function of the same name,
so with both packages attached, parameters() calls the
function from whichever package is attached last (#474).supported_models() is deprecated and will be removed in
bmm 1.6.0. Use bmm_models() instead. It takes no arguments
and returns the model names, which print as the grouped list.
supported_models(print_call = FALSE) becomes
bmm_models(). Until then supported_models()
keeps working and warns. The insight and
parameters packages export a function of the same name,
so with bmm and either of them attached, supported_models()
calls the one attached last (#481).?conditional_effects.bmmfit now says that
se__ is the spread of the draws on the scale
brms or bmm summarises them on, so with a link other than
identity it can sit on a different scale than estimate__.
Use lower__ and upper__ for uncertainty on the
scale you asked for. A new section covers models with a multinomial
family (#512).hypothesis(), comparing a model with and
without a predictor, and comparing different models fitted to the same
data, with leave-one-out cross-validation and Bayes factors (#308).?ddm, ?ezdm and ?cswald now
give the scale of zr: ddm() and
cswald(version = "crisk") fix it at 0 on its logit scale,
an unbiased 0.5, and ?ddm explains why response-coded data
need zr estimated. ?cswald says that
drift has a log link in "simple" and an
identity link in "crisk", and that the
"simple" bound is half the boundary
separation. ?ezdm_summary_stats says that
method = "robust" gives larger drift and smaller boundary
estimates than "mixture" (#540).ezdm_summary_stats() and
flag_contaminant_rts() now run up to 500 EM iterations
(maxit) instead of 100. Cells with almost no contaminants
can need more than 100; they fell back to robust moments in
ezdm_summary_stats() and to NA probabilities
in flag_contaminant_rts(), which a filter such as
contam_prob <= 0.5 drops entirely. Such cells now get
mixture-based values, so summary statistics and flags from earlier runs
can differ for them. The warning now says when the EM stopped at
maxit. Pass maxit = 100 to reproduce earlier
results (#540).bmm_models() (formerly supported_models())
now lists the models under four task groups (continuous reproduction;
categorical recall and n-AFC decisions; detection, recognition and
confidence judgments; choices and response times), one line per model
with its constructor and full name. The argument lists are gone from the
printout; ?modelname has them (#440).bmm_setup(), find the model for your task, including a
table of the signal detection models by response format and noise
distribution, and fit a first model to
broeder_schuetz_2009_e3 step by step (#440).use_model_template() now follows the package’s current
conventions. document() runs on it without warnings, the
new model passes the website reference check, its default priors include
sd, and with custom_family = TRUE
posterior_epred() works once the generated stubs are filled
in. The file header lists the registries and test fixtures a new model
must also update (#490).bmm(..., threads = 2) now works for cswald
as it does for sdm. The posterior is unchanged
(#387).a, c) so that the
simple and softmax choice rules imply a
comparable, broad prior-predictive range of average performance, and so
that the softmax defaults place equal prior means on
general (a) and context (c) activation —
centering the implied c - a prior at zero for fair
comparisons under cell-means coding. The mis-scaled
normal(0, 2) effect prior on c is replaced by
the shared normal(0, 0.5). Because the simple
rule requires c > a to predict accurate recall, its
defaults remain asymmetric; direct comparisons of context and general
activation should use the softmax choice rule (#364).threads argument
(e.g. bmm(..., threads = 2)), reducing fitting time by up
to ~45% in benchmarks (#374).softplus as an opt-in link function for
positively-bounded parameters, as an alternative to the default
log link. softplus(x) = log(1 + exp(x)) keeps
parameters positive while growing linearly for large values, avoiding
the numerical blow-up of exp() and giving predictor effects
an additive (rather than multiplicative) interpretation on the natural
scale. Enable it per parameter via the model’s links list,
e.g. m3(...)$links <- list(c = "softplus", a = "softplus")
or ddm(rt, response, links = list(bound = "softplus"))
(#363).init returned by configure_model() was
silently overwritten by create_initfun(). This caused the
m3 model’s intended init = 0 (needed for
stable sampling with the simple choice rule and an
identity link) to be lost, and left dead init
code in the sdm model. create_initfun() is
now the single source of truth for initial values, with model-specific
behaviour expressed through S3 methods (#375)..pwald() returning
NaN/-Inf in the upper tail of the shifted-Wald
survival function, which propagated to dcswald() (and
therefore log_lik/posterior_predict) for the
cswald model at extreme reaction times. The R-side
survival now uses the stable log_diff_exp form already used
by the Stan likelihood (swald_lccdf) (#376).print() for model summaries selecting
regression-coefficient rows by an unanchored substring match, so a
parameter such as a could pull in rows of another parameter
like kappa (e.g. in the imm model). Rows
are now matched on the exact parameter prefix. This also fixes a crash
when only a single coefficient row is shown (#379, #369).create_initfun() now matches Stan parameters to model
parameters with a word-boundary regex ((^|_)param(_|$))
instead of a substring match, preventing collisions in models with short
parameter names (e.g. s, c, a)
that are substrings of longer ones (sim,
correct, activation); the longest (most
specific) match is selected when several apply (#354, #355).create_initfun() now resolves initialization terms from
nlpars when a model parameter is not a distributional
parameter, so models built as non-linear brms formulas
(e.g. native-multinomial models whose parameters live in
bterms$nlpars) no longer error with
no applicable method for 'has_intercept' applied to an object of class "NULL"
(#362).threading(n, force = TRUE), matching the unsliced Stan code
brms generates in that case. Previously the threaded chunk was emitted
and the model failed to compile.bmm() now warns when a predictor in the formula shares
its name with both a predicted parameter and a column in the data. Such
a predictor was silently treated as a non-linear term (emitted via
nlf() instead of lf()), changing the
likelihood without any error. Short parameter names (c,
a, s, b) collide naturally with
condition codes or columns like
accuracy/stimulus (#378).configure_prior(): if a model’s
fixed_parameters includes a parameter that its
configure_model() never wires into the formula (neither a
dpar nor an nlpar), bmm now fails with a clear model-definition error
instead of letting a malformed b_Intercept ~ constant()
prior reach brm(). This is a safety net for model
development; it cannot be reached through the normal bmm()
interface, where an unrecognized parameter is already caught earlier by
check_formula() (#377).print.bmmodel() now also displays the required response
variables (one per line, annotated with the expected coding —
e.g. radians in [-pi, pi] for the circular models, seconds and 0/1
responses for the trial-wise RT models) and the default parameter links,
answering “what does my data frame need to look like?” directly at the
console (#392).https://github.com/popov-lab/bmm.use_model_template() now scaffolds the current
model-specification patterns. It generates a flat
.{model}_defaults block for unversioned models (like
ddm) or, with the new versions argument, a
.{model}_version_table block for versioned models (like
cswald). The generated constructor spells out every field
of the model object inline (referencing the defaults/version table for
parameters, links,
fixed_parameters, default_priors, and
init_ranges), and versioned aliases validate
version with match.arg() (#350).void_mu field from
all model definitions and the template. It was assigned but never read
anywhere — response-mean suppression is already handled via
fixed_parameters and the family’s dpars
(#350).swald_lccdf() returning incorrect log-survival
probability when response time equals non-decision time in the
cswald model. Previously returned -Inf
instead of 0 (log of survival = 1) (#348).cmdstanr
and rstan backends. Previously, cmdstanr was
required.ddm)
for speeded decision-making tasks with trial-level RT and response data.
The model estimates drift rate, boundary separation, non-decision time,
and (optionally) relative starting point. Includes distribution
functions dddm() and rddm() (#280).ezdm) for
speeded decision-making tasks. The model estimates drift rate, boundary
separation, and non-decision time from aggregated summary statistics
(mean RT, variance of RT, accuracy) using the closed-form equations
derived by Wagenmakers et al. (2007). Supports both 3-parameter
(symmetric starting point) and 4-parameter (asymmetric starting point)
versions based on Srivastava et al. (2016). Implements Bayesian
hierarchical estimation following Chavez & Vandekerckhove (2025).
Includes distribution functions dezdm() and
rezdm() (#281).cswald) for choice reaction time tasks with two response
boundaries. The model estimates drift rate, boundary separation, and
non-decision time from trial-level RT and response data. Implements two
versions: simple (treats errors as censored correct
responses, appropriate for high-accuracy tasks) and
crisk (competing risks version with separate
accumulators for each response, suitable for balanced accuracy).
Includes distribution functions dcswald(),
pcswald(), qcswald(), and
rcswald(). Thanks to @GidonFrischkornbmmfit objects. Provides an intuitive interface for
visualizing predictor effects on model parameters, with automatic
routing between distributional and non-linear parameters, inverse link
transformations to show parameters on their natural scale
(scale = "native"), softmax handling for mixture3p weight
parameters, and filtering of internal model variables (#203).bmmfit objects. Users can now call
emmeans(fit, ~ condition, dpar = "kappa") for any bmmodel
(#323).m3). Since brms::pp_check() does not support
the multinomial family, bmm now provides a custom method
that compares observed and predicted response proportions in the
ppc_bars style from bayesplot. The method
supports faceting by experimental conditions via group,
configurable credible intervals via probs, and
population-level predictions via re_formula = NA. For
non-multinomial models, pp_check() delegates to
brms::pp_check() and auto-selects the grouped plot variant
when group is specified (#324).bmmodel or bmmfit object with descriptions,
link functions, and fixed values (#329).%>% with the
native pipe |> (#341).bmm website (#237). Thanks to @GidonFrischkorn and @chenyu-psybmf2bf S3 methods for more flexible
translation of bmmformulas into brmsformulas
(#227).file_refit argument of the bmm
function now accepts character strings like brms. A warning
is given when “on_change” is specified, as this is not currently
implemented for bmmodels (#228). (The warning was removed
and “on_change” implemented in the development version, see #411.)formula(NULL)) to be added to a
bmmformula for consistentcy with brms (#264)bmmodels of the circular
model class (#229, #233).k2sd() function for improved performanceFirst version of the package on published on CRAN!
fit$bmm$fit_args list.fit$version$bmm (#88)A major restructuring of the package to support stable and generalizable development of future models (#41).