cogmod 0.3.1
New features
cogmod_invgaussian()gainssigmandt, the between-trial range of the non-decision time (st0): each trial’s non-decision time is drawn fromUniform(ndt, ndt + sigmandt), sondtbecomes its lower bound, exactly as incogmod_ddm(). Spreading the shift turns the Wald density into a difference of two CDFs and the CDF into a difference of two integrated CDFs, both closed form at a fixed drift, so the parameter costs a few normal CDFs per observation, works withcens()unchanged, and rides the existing drift quadrature whensigmadrift > 0too. It is on aloglink withcogmod_ddm()’s prior for the same quantity. It is hard to estimate and should be fixed at zero for most applications (sigmandt = 0inbf()); it shares the leading edge of the distribution withndtandpoutlier, and should only be freed with a lot of data, a strong prior, or both. As withsigmadrift, leaving it out ofbf()estimates it, so existing Wald formulas that do not mention it now fit one more parameter unless they addsigmandt = 0, and fits made before this version cannot be post-processed with it; the vignette models were refit.rcogmod_invgaussian(),dcogmod_invgaussian()andpcogmod_invgaussian()takesigmandtright aftersigmadrift, so apoutlierpassed by position moves along one.-
Censored reaction times:
brms’scens()works on the RT-only families.bf(rt | cens(error) ~ ...)scores an error trial as a right-censored correct response: its RT is a lower bound on when the correct process would have finished, so it contributes that process’s survival rather than its density. Oncogmod_invgaussian()this is the censored shifted Wald of Miller et al. (2018), thecswaldmodel ofbmm. Here it is not a family but a construction, so the same formula works on every RT-only family with a closed-form CDF:cogmod_lognormal(),cogmod_logstudent(),cogmod_gamma(),cogmod_invgamma(),cogmod_weibull(),cogmod_invweibull(),cogmod_logweibull(),cogmod_bisa(),cogmod_exgaussian()andcogmod_geg(). Left- and interval-censoring come with it, andlog_lik()- henceloo()- honours all three, whichbrmsleaves to a custom family’s own method.posterior_predict()predicts the latent, uncensored RT, asbrmsdoes for its own families.cogmod_priors()andcogmod_stanvars()refusecens()on the families that cannot take it, andcogmod_priors()warns above 20% censored trials. What the model is for, what it assumes and the one check to run before using it are in?rcogmod_invgaussianand the Censored Shifted Wald section ofvignette("rt_models").Under the hood every censorable family gets a
<family>_lcdfand a<family>_lccdfbeside its_lpdf, generated from two new registry slots so a family cannot drift out of step with itself. The survivals are written as survivals - never aslog(1 - exp(lcdf))- and the half Normal outlier’s throughstd_normal_lcdf(-z)rather thanstd_normal_lccdf(z), which is-inffrom 1.66 s on: the two placesbmm’s implementation broke. Withsigmadrift > 0the Wald CDF has no closed form and is taken by 64-point Gauss-Legendre quadrature over the drift, in R and Stan alike off one node table. pcogmod_*()for every censorable family.pcogmod_lognormal(),pcogmod_logstudent(),pcogmod_gamma(),pcogmod_invgamma(),pcogmod_weibull(),pcogmod_invweibull(),pcogmod_logweibull(),pcogmod_bisa()andpcogmod_exgaussian()joinpcogmod_invgaussian(), withlower.tailandlog.p. The upper tail is computed as the upper tail rather than as1 - CDF; these are the R side of the Stan_lcdf/_lccdfpair, and the tests hold the two to each other.-
cogmod_priors()now checks the response before returning. Everything in this package is stated in seconds and none of it is unit-equivariant - thendtprior means 170-300 ms,.POUTLIER_SCALEis 0.2 s - butbrmsfills its own defaults from the data, so a column of milliseconds produces a model whose two halves silently describe different quantities. It compiles, it samples, it converges, and the estimates are meaningless. The check catches that and the handful of other mistakes with the same character.It stops where the offending rows would make the fit impossible or wrong in a way
Stancannot report: a non-positive reaction time under a family that places no density belowndt; a response outside[0, 1]forcogmod_choco()orcogmod_betagate(); a non-integer rating forcogmod_betadiscrete(); a non-numeric response; and a third level indec(), which the choice families would otherwise fold silently into option 1, since their Stan code testsdec == 0and takes the else branch for everything else.It warns about the rest: a median implying milliseconds,
NAs, and either tail running past whatpoutliercan absorb. The tails are judged as proportions rather than counts, because the outlier component is supposed to produce the occasional fast response -rcogmod_lognormal(200, ndt = 0.2, poutlier = 0.02)puts one at 81 ms - and a count-based test fires on the package’s own generator. Over 20000 draws the component sends 0.8% of responses below 0.1 s atpoutlier = 0.02and 1.9% at 0.05, the top of the default prior, so the warning sits at 5%.Families with neither
ndtnorpoutlier-cogmod_exgaussian()andcogmod_geg()- are exempted from the tail checks, and a non-positive response is a warning rather than an error there, their support being the whole real line. A formula or family the check cannot read is passed through untouched, sobrms’s own error is what the user sees.
Performance
-
The R-side DDM density no longer goes through
brms::dwiener().dcogmod_ddm(), and with itlog_lik(),loo(),p_outlier()and every other post-processing method that evaluates the likelihood in R, now use a vectorised Navarro and Fuss (2009) series written in log space. The 4-parameter density is about eight times cheaper per element and agrees withbrms::dwiener()to1e-12on the log scale. The 7-parameter density, which evaluates that series 625 times per observation under Gauss-Legendre quadrature, goes from about 5 ms to about 0.6 ms per draw-observation - a LOO over 4000 draws of 500 trials drops from close to three hours to about twenty minutes. Both now return a finite log-density in the far tails wheredwiener()returnslog(0). The Stan likelihood is unchanged.RWieneris still needed by the test suite, which usesdwiener()as the reference.
Breaking changes
-
cogmod_lba2()now truncates each drift rate at zero, the convention ofrtdists(posdrift = TRUE),DMC,EMC2andggdmc. Previously the pair of drifts was conditioned on at least one being positive and a losing accumulator was allowed a negative rate, which it kept forever. The two are different models of the same race wherever a drift is small relative to its SD: densities up to about 40-50% apart in the tails, choice probabilities a few percentage points apart. The change makescogmodLBA estimates directly comparable with those packages and with the literature built on them, anddcogmod_lba2()now reproducesrtdists::dLBA()at the same parameter values. It also simplifies the sampler, which draws each drift from its truncated Normal rather than splitting the conditional law into cases. Fits made with earlier versions cannot be post-processed with this one, and their estimates are not comparable with new ones at low drift rates; the vignette model was refit. The loser’s survival is computed asP(v > 0, unfinished) / P(v > 0)from whichever tail keeps its digits, so the density stays accurate for a loser with a strongly negative drift, where both quantities are tiny, and in the far tail, where1 - CDFwould cancel.cogmod_lba1()is unaffected: with one accumulator the two conventions coincide.The truncation has one cost, and
cogmod_priors()now covers it: once an accumulator rarely wins, itsdriftandsigmaare identified only through|drift| / sigma^2(the truncated Normal converges to an Exponential along that ray), so a flat prior lets the drift run off - the vignette’s error accumulator sat at-12with an interval of-23to-6.5.cogmod_priors()therefore putsnormal(1, 2)ondriftoneandnormal(0, 1.5)on its slopes, the treatmentcogmod_lnr()’snuonealready had. Existing formulas that leftdriftonetobrmsget this prior on their nextcogmod_priors()call.
cogmod 0.3.0
New features
-
New family
cogmod_geg(), the Generalised Ex-Gaussian of Marmolejo-Ramos et al. (2023): the ex-Gaussian with its CDF raised to a power,F_GEG(x) = F_EG(x)^shape. The construction is Durrans’ alpha-power family, so the density isshape * F_EG(x)^(shape - 1) * f_EG(x)and stays closed form - Stan ships bothexp_mod_normal_lpdfandexp_mod_normal_lcdf, so the whole likelihood is three lines and costs one extra CDF.shape = 1iscogmod_exgaussian()exactly, not approximately, in R and in Stan alike, soloo_compare()between the two is like-for-like.What it buys is shape. Sweeping
sigmaandtauacross the values RT data occupy, the ex-Gaussian spans skewness 0 to 2 and excess kurtosis 0 to 6; freeingshapewidens that to roughly -0.4 to 4.8 and 0 to 35. In particular the GEG can be negatively skewed, which the ex-Gaussian cannot be at any parameter value.What it costs is interpretability, and specifically the property the ex-Gaussian is normally reported for. The mean is no longer
mu + tau- atmu = 0.4,tau = 0.2it runs 0.31 atshape = 0.2and 1.15 atshape = 20- and it has no closed form, soposterior_epred()integrates numerically and is the one generic materially slower here than for a closed-form family. On a full data set, summariseposterior_predict()draws instead.shapeis also badly confounded withmu: fitted by maximum likelihood to the lexical-decision data used in the vignettes, the two correlate about -0.98 at the optimum, and the other estimates move with it (on one conditionmugoes 0.429 to 0.508,sigma0.051 to 0.037,tau0.119 to 0.162).shapere-slices the same bulk-and-tail split rather than adding an independent axis.cogmod_priors()therefore gives it a deliberately informativenormal(0, 0.5)on theloglink - centred onshape = 1, the ex-Gaussian - andcogmod_inits()starts it there.Use it when fit is the point. When the
mu/taudecomposition is the point,cogmod_exgaussian()is the family to fit; when a better-fitting descriptive family is the point,cogmod_logstudent()andcogmod_loggamma()decouple skew from tail weight with parameters that stay interpretable. New vignette,
vignette("performance"), covering how to speed up sampling for the choice+RT families: approximating a fit first with Pathfinder before committing to full MCMC, chain and within-chain (multithreading) parallelization, spreading short warmup runs across an HPC job array and recombining them withbrms::combine_models(), and amortized inference (e.g. BayesFlow) as a longer-term direction.-
pcogmod_rdm()gains aresponseargument, giving the defective CDFP(RT <= q, choice = response)rather than only the RT distribution marginally over the choice. It does not reach one - its limit is the probability of that response, whichpcogmod_rdm(Inf, response = k)gives - and the two of them sum back to the marginal CDF. This is what a defective-CDF or quantile-probability plot of a race model needs.responsegoes afterpoutlierin the signature, unlike indcogmod_rdm()andpcogmod_ddm(), which put it before.poutlieris a model parameter andresponseis not, so every parameter now comes first and the argument stays where a positional call already expects it - only a positionallower.tailorlog.p, in eighth place or later, is affected.There is no closed form for it, unlike the marginal CDF, where the race survival factorises as
S0 * S1. It is quadrature over the defective density, so it is accurate to about1e-8rather than to machine precision, and about ten times slower per element - 200 points take 0.6 s against 0.07 s for the marginal.lower.tail = FALSEintegrates the upper side directly rather than subtracting, so the defective survival stays accurate into the tail. New
qcogmod_rdm(), the quantile function, invertingpcogmod_rdm()by root-finding - marginally, or per response.scale_p = TRUEreadspas a fraction of the chosen response’s own probability, sop = 0.5is that response’s median; that is the form a quantile-probability plot wants.-
cogmod_rdm()now accepts a start-point range of exactly zero.bias = 0(sigmabiasin thebrmsfamily) is a model, not a degenerate parameter: both accumulators start at0on every trial and the race is between two plain Walds - equation 2 of Tillman et al. (2020), which is the limit the density already took.cogmod_lba1()andcogmod_lba2()have always allowed it and the RDM excluding it was an inconsistency. Accepted in R and in Stan alike; a negative range is still rejected.This does not make the
sigmabiasdirection any better identified. Thesoftpluslink still reaches zero only at minus infinity, so the flatsigmabias -> 0ridge is as long as it ever was andcogmod_priors()still fences it.
Bug fixes
-
A missing, infinite or zero-length reaction time no longer aborts a density. Of the sixteen mixture families, four threw
missing value where TRUE/FALSE neededondcogmod_*(NA_real_)-cogmod_lba1(),cogmod_lba2(),cogmod_rdm()andcogmod_ddm()- and two threw onInf:cogmod_loggamma()andcogmod_lba2(). The rest returned0. The split was not principled: those are the densities whose cores branch on a comparison rather than being arithmetic all the way down, andany(NA)isNAwhileif (NA)is an error. All sixteen now return0, which is what the shared machinery already wrote for those rows and could never reach. A missing parameter, and a missing response, are handled the same way and for the same reason.This matters beyond tidiness: one bad entry used to take the whole vector down with it, so a single
NAin a column of reaction times aborted the call instead of costing that row.dcogmod_exgaussian()still returnsNAforNA. It is a plain density with no outlier component, so it followsdnorm()rather than the mixture convention. Zero-length input gives a zero-length answer.
dcogmod_*(numeric(0))returned a length-1 value - or, in the same four families, threw - because the shared preparation recycled the empty vector up to the parameters andrep_len(numeric(0), 1)isNA. It now returnsnumeric(0), as everyd/p/qfunction in base R does. A zero-length parameter alongside a real quantile is now rejected rather than silently becoming a vector ofNAs.pcogmod_ddm(q, response = k, lower.tail = FALSE)was not a survival. It returned1 - P(RT <= q, choice = k), which isP(RT > q OR choice != k); atq = Infthat gave the probability of the other response rather than zero. It is now the defective survivalP(RT > q, choice = k), so the two tails add to that response’s own probability rather than to one. The marginal (response = NULL) is unchanged, and so is the lower tail in both forms. Same convention aspcogmod_rdm().pcogmod_invgaussian(NA_real_)threw rather than returningNA.pcogmod_rdm(q, lower.tail = TRUE)returnedNaNinstead of0atq <= 0whenpoutlier > 0. Both mixture components are exactlylog(1)there, and the mixture of them landed 2e-17 above zero rather than on it, whichlog(1 - exp(.))cannot take. Reached in practice byqcogmod_rdm(), whose root search starts at zero.pcogmod_rdm(NA)returned1rather thanNA, the mixture helper mapping a missing value onto a log-survival of-Inf.The direct lower-tail branch of
.pwald()had no small-biascase; both of its branches divide by the start-point range. Unreachable before, sincebias = 0was rejected.
Breaking changes
-
The pre-rename names are gone.
rt_lognormal(),lnr(),ddm(),rdm(),choco(),betagate(),betadiscrete(),lba(),rt_lba()and every function derived from them - 140 exports in all - were kept as synonyms through 0.2.1 and are removed here. Use thecogmod_*name:rt_lognormal()iscogmod_lognormal(),rrt_lognormal()isrcogmod_lognormal(),rt_lognormal_stanvars()iscogmod_lognormal_stanvars(), and likewise for the densities, the*_lpdf_expose()and thebrmspost-processing hooks.The synonyms existed so that a model fitted before the rename could still be summarised, since
brmslooks uplog_lik_<family>()by the name stored on the fit. That window closes with the first CRAN release: there is no released version to be compatible with, and a fit made under an old name can be brought forward by settingfit$family$nameto thecogmod_*one. Refitting is the safer route, as several parameterizations changed in 0.2.1 as well. -
cogmod_exgaussian()’smuis now on anidentitylink and is unbounded, where it was onsoftpluswith a lower bound of zero.sigmaandtauare unchanged.muis the location of the Gaussian component, not a scale. The convolution is well defined for any real value - the density integrates to one atmu = 0and below - and the Stanlpdfhas always agreed, checking onlysigmaandtau. The old bound lived in.prepare_exgaussian()and in the family declaration alone, so the R functions were refusing inputs the sampler would happily fit.Two things were wrong with constraining it. Interpretability, which is most of the point of the ex-Gaussian: behind
softplusa coefficient is not in seconds, and the conversion factor moves with the intercept - the local slope is 0.33 atmu = 0.4s, 0.39 at 0.5 s and 0.63 at 1 s, so the same effect reads as a different number depending on where the intercept sits. And fidelity: for fast, heavily-tailed data the Gaussian component genuinely belongs near or below zero withtaucarrying the mass, and forcingmu > 0distorts themu/tausplit in exactly the cases where that decomposition is the quantity being estimated.identityalso matches every other implementation -brms’s ownexgaussian(),retimes, and the estimates in the literature - so fitted values are now directly comparable.What to change. Coefficients on
muare now in seconds and are not comparable to values from an earlier fit; refit rather than reinterpret. Passcogmod_exgaussian(link_mu = "softplus")to keep the old behaviour. cogmod_priors()now sets a prior oncogmod_exgaussian()’smuintercept,normal(0.4, 0.25). It previously leftmutobrms, whosestudent_t(3, 0, 2.5)was a fair statement on the softplus scale (median 0.69 s) but onidentityis centred on zero seconds and rates a Gaussian centre of -2 s as plausible as one of +2 s. The prior deliberately does not exclude negative values. Only the intercept is set; the response’s slopes are the effects being estimated and are left alone.-
The outlier component is now a half Normal with a fixed scale of 0.2 s, where it was a half Student-t with 3 degrees of freedom and a user-supplied scale. Two things changed, for one reason.
The Student-t’s tail was heavier than every decision density in the package, so far-out slow responses were eventually explained better by the outlier component than by the model: against a shifted LogNormal at
poutlier = 0.02, a 5 s response was attributed to it with probability 0.86 and the crossover sat at 3.86 s, withndtpulled up behind it.vignette("outliers")already flagged this as a defect. A Gaussian never gets there - the same responsibility is 0.000 out to 30 s - and it costs nothing where the component is actually needed, becauseexp(-x^2 / 2s^2)kills the far tail at any scale: at 0.2 s it holds 76% of its peak density at 0.15 s and 46% at 0.25 s, against 85% and 66% for the half-t. The slow tail now belongs to the decision family, which is whatcogmod_loggamma()’sshapeandcogmod_invgaussian()’ssigmadriftare for.A welcome side effect: the
poutlier -> 1degenerate mode is now thousands of log-likelihood units below the sensible one rather than hundreds, andndtand the decision parameters no longer drop out of the density there, because a half Normal cannot explain a slow response at all. The mode still has infinite volume inpoutlieritself, socogmod_priors()is still not optional. -
minrtis removed from every family, density, RNG,*_stanvars()and*_lpdf_expose(). The package works in seconds, full stop. The equivarianceminrtbought in the likelihood was already fictional end to end:cogmod_priors()shifted only thendtprior with it, while thesigmandtprior ofcogmod_ddm(), thesigmadriftprior ofcogmod_invgaussian()and themupriors are stated in seconds outright - andcogmod_priors()is not optional. Making the assumption explicit costs one argument from about twenty signatures and removes a whole class of misconfiguration.Calls passing
minrtnow fail with R’s usualunused argumenterror. Data in another unit fails silently, as it always did whenminrtwas left at its default: the outlier component’s log-density atRT = 400is about-2e6, so it contributes nothing, the mixture collapses to the unmixed shifted family,poutliergoes to zero andndtis pinned by the fastest observed response. Nothing errors and the chains still initialise. Divide by 1000 before fitting.cogmod_priors()accordingly givesndta fixednormal(-1.2, 0.2), andcogmod_inits()starts it at 0.1 s. cogmod_invgaussian()gainssigmadrift, the between-trial SD of the drift rate, so the Wald can produce the long right tails empirical RT distributions have. Described in full under 0.2.0 below.-
New function
pcogmod_ddm(), the diffusion’s cumulative distribution function - the package hadpcogmod_rdm()andpcogmod_invgaussian()but no DDM counterpart.response = NULLgives the RT distribution marginally over the choice, andresponse = 0/1the defective CDF that boundary carries.It is the series that
rcogmod_ddm()now inverts to draw from, so it was written and validated anyway; exposing it costs nothing and it is the more accurate of the two available implementations. Against numerical integration ofdcogmod_ddm()over a grid of 360 cells the worst deviation is 1e-15, wherertdists::pdiffusion()is out by up to 5e-4 (andpdiffusion(Inf, ...)by 1.4e-4). It covers the classic 4-parameter DDM plus the outlier component; the between-trial variability parameters would each need their own quadrature layer, so they are not arguments rather than being silently ignored.
Performance
-
cogmod_rdm()samples about 1.5x faster. It was the most expensive likelihood in the package - roughly 5.9 us per observation per gradient, against 2.6 forcogmod_lba2()and 0.8 forcogmod_lnr()- and two thirds of that was avoidable. The Stan survival evaluated the normal CDF atalphaand atbetatwice each, once insidelog_g()and once again for the terms that follow it, and it assembled its six signed terms through a helper returning avector[2], which allocates on the autodiff stack once per term per observation per leapfrog step.log_g()now takeslog Phi(u)from its caller, which brings the survival from six normal-CDF evaluations to four, and the six terms are grouped into the two differences that are individually monotone in the threshold, so each is onelog_diff_exp()of known sign and nothing on the hot path returns a vector. The maths is unchanged and the grouped form cancels less: against the R implementation it agrees to 6e-11 where the term-by-term form reached 5e-9. Gradients are unchanged to the precision finite differences can resolve. -
posterior_predict()is about 2x faster for the choice+RT families. brms calls it once per observation, so anything done per call is paid thousands of times, and for every family exceptcogmod_ddm()the sampling itself was the small part: twodata.frame()constructions and anas.matrix()came to roughly 63% of the call, against 13% for the actual draws. The registry’srngentries now return a barelist(rt, response)and.rchoice()can return a matrix directly, so the prediction path builds no data frame at all. Acbind()costs about a seventeenth of thedata.frame()it replaces.rcogmod_lnr(),rcogmod_rdm(),rcogmod_lba2()andrcogmod_ddm()still return a data frame, with the same draws for the same seed - only the internal path changed. Measured on thevignette("decision_making")models at 20 draws: LNR 41.5 -> 20.7 us per observation-draw, LBA 32.9. -
cogmod_ddm()samples its predictions 4-6x faster, on top of the above. It was the slowest family to predict from by an order of magnitude, becausebrms::rwiener()takes one parameter set per call and roughly 85% of that call is fixed setup - which cannot amortise when every posterior draw carries its own parameters. (rtdists’ marginal cost is 1.4 us per draw against a 772 us fixed cost; RWiener’s sampler does not amortise at all, staying at 55-90 us per draw for any n.)Draws are now taken by inverting the CDF, which vectorises across parameter sets because every step acts on the whole vector at once. The large-time series splits into a part that depends on the time and a part that does not, so the latter is built once and each evaluation is a single
exp()and a column sum; and the density falls out of the sameexp(), which makes a Newton step cost exactly what a bisection step costs. Eight Newton passes leave about 0.6% of draws for a bisection cleanup that cannot fail, since the bracket is valid by construction.Accurate to 1e-13 in log RT against a 60-step bisection, with no Kolmogorov-Smirnov failure against either
brms::rwiener()orrtdists::rdiffusion()over a wide parameter grid. The CDF underneath it agrees with numerical integration to 1e-9, wherertdists::pdiffusion()is out by up to 5e-4. On thevignette("decision_making")models: DDM 560 -> 122 us per observation-draw, DDM-5 483 -> 75.
Documentation
Every help page now documents what each of its functions returns, rather than only the random-generation function it is named after - the
brmsfamily object, thestanvars, and the shape of thelog_lik(),posterior_predict()andposterior_epred()output, including the families whoseposterior_epred()errors because the decision time has no finite mean.Examples that were commented out now run. The plots are live, the
bf()formulas are built, and the*_lpdf_expose()and model-fitting snippets are in\donttest{}behind a check forcmdstanrand a CmdStan installation instead of\dontrun{}, so they execute wherever the toolchain is present.p_outlier(),with_outliers()andcogmod_inits()gained real examples.DESCRIPTIONcites the papers behind the models.
cogmod 0.2.1
New features
-
sigmabias = 0is now allowed incogmod_lba1()andcogmod_lba2(), and in the single-accumulator case it is the recinormal, or LATER, model of Carpenter & Williams (1995): the accumulator starts at zero on every trial, so the decision time isboundary / driftand1 / (RT - ndt)is normally distributed, withmuandsigmathe mean and SD of promptness. Zero was previously rejected as an invalid parameter; it is a nested model, and the bound is now closed for the same reasoncogmod_invgaussian()’ssigmadriftandcogmod_ddm()’s three between-trial variabilities are.bf(rt ~ 1, sigmabias = 0, boundary = 1) # free: mu, sigmaNothing about the density had to change for this to be exact - at
sigmabias = 0the existing Taylor branch evaluates todnorm(b / t, mu, sigma) * b / t^2 / pnorm(mu / sigma)to machine precision, in R and in Stan alike - so the two models share one likelihood andloo_compare()between them is like-for-like.This matters beyond nesting. Estimating
sigmabiasfreely is treacherous precisely because the recinormal limit is smooth: the likelihood goes flat as the start-point range shrinks, andsoftplusreaches zero only at minus infinity, socogmod_priors()has to fence the direction off. Pinning it at zero removes the parameter instead, which is the honest option when the design cannot identify a start-point range.Note that two pins are now needed for the evidence scale, not one: scaling multiplies every member of the scale ray by a common constant and leaves zero at zero, so
sigmabias = 0drops off the ray rather than pinning it.cogmod_stanvars()says so explicitly when that is the only fix present. -
New family
cogmod_exwald()(Schwarz, 2001): the decision time is a Wald convolved with an exponential residual stage of meantau- the mechanistic counterpart ofcogmod_exgaussian(), whose first stage is a descriptive Gaussian instead, withtaumeaning the same thing in both. The mean exists and isndt + boundary / mu + tau, soposterior_epred()returns a number.The density has two branches, both exact. Where
mu^2 > 2 / tauthe convolution collapses to a closed form in the Wald CDF; below that - which is the common regime, since at a drift of 3 and a threshold of 0.5 the closed form needstau > 0.22 s- the same expression continues analytically through the Faddeeva function, givingg * exp(-(boundary - mu * t)^2 / (2 * t)) * Re[w(z)]. The exponent is the Wald’s own, so nothing overflows, and the branches meet exactly atmu^2 = 2 / tau. Across a grid spanning the usual RT region the density integrates to 1 to within 8e-12, the mean is right to 3e-9, and the relative step across the branch seam is 5e-8.Note there is deliberately no
sigmadrift: it andtauboth fatten the right tail and are very hard to tell apart, andcogmod_invgaussian()is where the drift-variability route lives.ndtandtaualso share a ridge - both delay the response, and only the shape of the leading edge separates them - socogmod_priors()givestauthe samenormal(-1.5, 0.7)it givescogmod_exgaussian(). Fixingndt = 0inbf()recovers Schwarz’s own model. -
New family
cogmod_bisa(), the Birnbaum-Saunders or fatigue-life distribution (Birnbaum & Saunders, 1969): a first-passage-time model in which evidence arrives in discrete cycles and only ever towards the boundary - what is random is the size of each increment, never its sign. Summing those increments and applying the CLT, then treating the cycle count as continuous, gives the first-crossing time.It is parameterized mechanistically, as
mu(drift) andboundary(threshold), so it sits directly alongsidecogmod_invgaussian()with the parameters meaning the same thing and only the mode of accumulation differing. Fixing the per-cycle SD at 1 is the same convention that fixes the Wald’s diffusion coefficient, and it makes(mu * t - boundary) / sqrt(t)exactly standard normal - the usual(1 / a) * (sqrt(t / b) - sqrt(b / t))written in these parameters, withb = boundary / muanda = 1 / sqrt(mu * boundary). The map between the two is a bijection, so nothing is given up.Everything is then closed form, and the density is the Wald’s own tilted by
(mu * t + boundary) / (2 * boundary)- one sign apart from it. That tilt makes the family an equal mixture of an inverse Gaussian and its length-biased twin, so at the same(mu, boundary)it is slower and more dispersed than the Wald (mean 0.222 s against 0.167, SD 0.184 against 0.136 atmu = 3, boundary = 0.5), while keeping the same exponential-order right tail.E[T] = ndt + boundary / mu + 1 / (2 * mu^2)is always finite, soposterior_epred()returns a number, and the median is exactlyndt + boundary / mu. There is nosigmadrift: the extra dispersion comes from the mixture structure at no cost in parameters, and drift variability is whatcogmod_invgaussian()is for.It is also the cheapest first-passage density in the package - one log and one square, no branch and no special function - and
rcogmod_bisa()is one normal draw per observation, exact, with no rejection step. -
New family
cogmod_logstudent():log(RT - ndt)follows a Student-t, a robust LogNormal that varies kurtosis wherecogmod_loggamma()varies skew. The heavy tail absorbs slow contaminants into the likelihood rather than into a mixture component, which matters because thepoutliercomponent is a half Normal and by construction cannot explain a slow response. Atdof = 5the probability of a decision time beyond 5 s is about five orders of magnitude larger than the matching LogNormal’s.The degrees of freedom are called
dof, notnu:cogmod_lnr()already spendsnuzero/nuoneon drift rates, andbrmsrecognises the namenuand supplies defaults of its own for it.Two things to know. The mean does not exist for any finite
dof, soposterior_epred()errors rather than returning a number; the median is exact atndt + exp(mu). And the density is unbounded atndt- integrable, so the posterior stays proper, but the likelihood has no maximum, which is one more reasoncogmod_priors()is not optional. A Student-t is also symmetric on the log scale, so a smalldoffattens the fast tail as well as the slow one and competes withpoutlier;cogmod_priors()centresdofat 6 with 95% of its mass between 1.5 and 24 to keep that in check. cogmod_priors()now supportscogmod_exgaussian(), where before it returned thebrmsdefaults with a message.sigmaandtauare both lengths of time in seconds behind asoftpluslink, whichbrmshas no way to know:tauarrives flat, andsigmaarrives with thestudent_t(3, 0, 2.5)thatbrmssupplies because it recognises the name - a Gaussian SD centred on 0.69 s modelled, 1.9 s omitted, wider than most whole RT distributions. They now getnormal(-2.3, 0.7)andnormal(-1.5, 0.7)on the link scale (roughly 25-330 ms forsigma, 55-630 ms fortau), and the matchinglognormalwhen the dpar is left out ofbf()altogether.muis deliberately untouched: it is the response’s own intercept and thebrmsdefault is already proper and reasonable there.-
cogmod_invgaussian()gainssigmadrift, the between-trial SD of the drift rate. Above zero, each trial draws its own drift from aNormal(mu, sigmadrift)truncated at zero, which is what lets the Wald reach the long right tails empirical RT distributions have. Marginalising over that draw is a Gaussian integral, so the density stays closed form and costs two normal CDFs;sigmadrift = 0gives back the previous density exactly, not approximately.The truncation is what keeps the density proper: a single-boundary accumulator handed a negative drift never terminates, so an untruncated Normal would leave up to a third of the mass unaccounted for.
cogmod_ddm()’ssigmadriftneeds no such truncation, a diffusion between two boundaries always absorbing at one of them.It is fixed the same way as the
cogmod_ddm()variability parameters - writingsigmadrift = 0inbf()pins it and recovers the classic Wald, while leaving it out ofbf()estimates it. Fixing it is the better default:sigmadriftandpoutlierboth fatten the right tail and are only weakly distinguishable, andcogmod_priors()givessigmadrifta deliberately informative prior where it is estimated. Note that withsigmadrift > 0the density decays ast^-2and the mean does not exist, soposterior_epred()returnsInf; summariseposterior_predict()draws instead.Two consequences for existing code. The
drift/boundary/ndt/poutlierfunctions gained an argument, sosigmadrift = 0now sits betweenndtandpoutlierin the signatures ofrcogmod_invgaussian(),dcogmod_invgaussian()andpcogmod_invgaussian()(positional calls that passedpoutlierfourth need updating; named calls are unaffected). And a formula that does not mentionsigmadriftat all now estimates it rather than fitting the fixed-drift Wald. -
New
cogmod_stanvars(): the third of the three generics that take the model rather than the family, alongsidecogmod_priors()andcogmod_inits(). It reads the family off the formula and returns that family’s Stan code, so the family is named once - inbf()- instead of three times:f <- bf(RT ~ Condition, ndt ~ Condition, family = cogmod_lognormal(minrt = 0.25)) brm(f, data = df, prior = cogmod_priors(f, df), init = cogmod_inits(f, df), stanvars = cogmod_stanvars(f))This is not only tidier.
minrtis baked into the generated Stan code as a literal, because a Stan function cannot see the data block, socogmod_lognormal(minrt = 0.25)fitted withcogmod_lognormal_stanvars()runs happily against an outlier component the family does not describe.cogmod_stanvars()takesminrtoff the family, and the two cannot disagree. The per-family<family>_stanvars()functions are unchanged. cogmod_inits()now supportscogmod_exgaussian(), whose three parameters are all on the RT scale behind asoftpluslink and so are equally badly served by the default start atlog(2) = 0.69s - which makes the Gaussian SD alone wider than most whole RT distributions.-
cogmod_inits()now returns a value for every parameter the Stan program declares, not only the ones it has an opinion about, so CmdStan no longer printsInit values were only set for a subset of parametersand lists the rest. Regression slopes and standardized group-level effects start at zero, group-level and spline SDs just above zero, Cholesky factors at the identity; all are at least as good a starting point as Stan’s ownU(-2, 2).Two related fixes come with it. Links are now read off the family rather than the registry, so
cogmod_gamma(link_mu = "log")is honoured. And the jitter is applied on the unconstrained scale - additive when free, multiplicative for a positive parameter, on the logit scale for a bounded one - so a jittered start can no longer land outside its own bounds, which it could previously for a dpar left out of the formula and estimated on the natural scale. -
New
cogmod_inits(): starting values for the families that estimatendtdirectly.brmsinitialises on the unconstrained scale, soinit = 0putsndtatexp(0) = 1second - above most sub-second RTs, which leaves every response attributed to the outlier component and the decision parameters with no gradient at all. Forcogmod_gamma()andcogmod_weibull()it also puts the shape atsoftplus(0) = 0.69, below the 1 at which the density becomes unbounded at the shift. No single scalar avoids both, since the two pull in opposite directions.On 1500 simulated Gamma trials (true shape 3, true
ndt0.25),init = 0left the shape stuck at its starting value withRhat2.3 and an ESS of 3 after 306 s;cogmod_inits()recovered shape 3.23 andndt0.227 withRhat1.01 in 28 s. An informative prior on the shape did not rescueinit = 0- a prior cannot move a chain whose gradient is zero.Parameter names are read off
brms::make_stancode()for the model actually being fitted, so0 + Intercept, interactions, group-level terms and smooths are all handled; anything it does not recognise is left to Stan. -
New
cogmod_loggamma()family: a shifted Log-Gamma model for reaction times, equivalently a shifted generalized gamma.log(RT - ndt)follows a location-scale log-gamma with locationmu, scalesigmaand shapeshape, andndt/poutlier/minrtwork exactly as incogmod_lognormal().shapeis unconstrained, withshape = 0in the interior: it recoverscogmod_lognormal()exactly,shape = sigmathe shifted Gamma,shape = 1the shifted Weibull andshape = -1the shifted inverse Weibull. Fitting it is therefore a way of testing whether the LogNormal shape is adequate - an interval forshapecovering 0 says it is. Negativeshapegives a power-law right tail.Note the boundary at
sigma * shape >= 1, where the decision density becomes unbounded atndtand the likelihood with it;cogmod_priors()setsnormal(0, 0.5)on theshapeintercept to keep well clear of it.Fit this family with
init = 0. The prior keeps the posterior clear of that boundary but not the starting point:brmsinitialises fromU(-2, 2)on the unconstrained scale, so about 15% of chains start withsigma * shape >= 1, fall into the spike atndtand never finish.init = 0starts every chain atshape = 0, the LogNormal, and removes the problem. with_outliers(),without_outliers(),p_outlier()andcogmod_priors()now work oncogmod_loggamma()as well ascogmod_lognormal().-
cogmod_stanvars()now warns when the evidence scale is left free forcogmod_lba1()andcogmod_lba2(). Both have a likelihood that is exactly constant along the ray that multiplies the drift rates, their SDs, the start-point range and the threshold offset by a common factor - verified to machine precision, not merely near-flat - so nothing in the data can pick a point on it. The failure is quiet rather than loud: the fit converges,pp_check()looks right, and only the individual parameter estimates are meaningless, being whatever the priors happen to say about that direction.Fixing any one member of the ray in
bf()pins it and silences the warning -sigmazero = 1conventionally, asrtdistsandEMC2both do, butboundary = 1orsigmabias = 0.5work as well. Note that leaving a parameter out ofbf()does not fix it:brmsdeclares it as a free auxiliary parameter and the ray stays exactly as free, which is the case the warning mostly exists to catch.cogmod_rdm()andcogmod_ddm()are quiet by construction, their unit diffusion coefficient having pinned the scale already.
Bug fixes
-
cogmod_ddm()no longer reportsNon-finite gradientduring warmup or a Pathfinder search, and no longer collects the divergent transitions that come with it. Stan’s classic 4-parameterwiener_lpdf()- much the fastest of the three Wiener densities Stan offers, and the one this family used whenever the three between-trial variability parameters were zero - returns-infin two regions, and hands back NaN partial derivatives when it does. A NaN partial is not made harmless by the mixture weight on it being zero: reverse-mode multiplies the (zero) adjoint into the stored partial, and0 * NaNisNaN, so a single trial in one of those regions turns the gradient of the whole model to NaN, and Stan rejects the proposal.The two regions are the alternating small-time series losing its sum to cancellation - which depends only on the rescaled decision time
tau = t / boundary^2, not on the drift or the scale separately, and sets in belowtau = 6.6e-4- and the density underflowing to zero, which a cheap leading-term estimate detects. Those calls now go to Stan’ssv-capable density instead, which works in log space throughout and stays finite, with finite gradients, down to log-densities of-1e7. The two agree to1e-13where the paths meet, so there is no step in the likelihood, and the fast path still handles the overwhelming majority of evaluations.On the 2000-trial fit in
?cogmod_ddm, over three seeds: 16Non-finite gradientreports per Pathfinder run became 0, and 47-420 divergent transitions per NUTS run became 0. Sampling is about 35% slower per iteration and roughly two to a hundred times better per effective sample.The same guard is applied to the general 7-parameter form, used when
sigmabiasorsigmandtis nonzero, which fails the same way. There it returns the-infthat form would have returned anyway, but as a constant, which carries no partial derivatives. -
The test suite runs in a third of the time (2472 s to 968 s on Windows). Every family’s Stan
lpdfnow goes into one model, compiled once per session, instead of nine separate*_lpdf_expose()compilations; the factorial parameter sweeps are thinned to subsets that still cover every level of every factor; and the sixbrms::brm()model fits are behindCOGMOD_TEST_SLOW, which the CI workflow sets. Setting it locally runs them:Sys.setenv(COGMOD_TEST_SLOW = "true"); devtools::test()The shared model also removes a trap the RDM tests had worked around with a cache of their own:
expose_functions()fails on a modelcmdstan_model()returns pre-compiled, so a second call in the same session errors rather than reusing the first. -
cogmod_priors()now coverscogmod_lnr()’snuone,sigmazeroandsigmaone, which it previously left flat. Push an accumulator’s rate down far enough and it stops finishing first ever; the density then depends on it only through the loser’s survival term, which has already saturated at 1. Past aboutnuone = -6the log-likelihood is exactly constant, and that accumulator’ssigmais unidentified along with it. The outlier component makes this reachable rather than hypothetical: it floors the trials the retreating accumulator can no longer explain, so the plateau is there even when both responses are observed.nuonegetsnormal(0.7, 1.5)on its identity link, the two sigmasnormal(0, 1)on softplus (lognormal(-0.7, 0.75)when omitted frombf()and so on the natural scale), and all threenormal(0, 0.5)on slopes.mu- which isnuzero- has the mirror-image plateau, but it is the response’s own intercept andbrmsalready gives it a properstudent_tdefault, so it is left alone; if you model a rarely-chosen option it is worth mirroring thenuoneprior onto it by hand.cogmod_inits()already covered this family and is unchanged. -
cogmod_priors()now coverscogmod_lba1()’ssigmabiasandboundary, which it previously left onbrms’s flat default. As the start-point range approaches zero the LBA converges smoothly to the recinormal, so the likelihood stops depending onsigmabiasaltogether - and asoftpluslink reaches zero only at minus infinity. That is a flat prior over an infinite flat region, the same improper posterior the function already exists to prevent forndtandpoutlier, and it failed just as quietly: on the 4285-trial fit invignette("rt_models"),sigmabiasfor one condition ran off tosoftplus(-10.4) = 3e-05withRhat1.69 and an effective sample size of 6, while every other parameter looked healthy. With the priors in place the same fit givesRhat1.02, an effective sample size of 387, no maximum-treedepth hits, and a finite estimate.boundaryis covered too, sinceb = sigmabias + boundaryputs the two on the same ridge.Both get
normal(0, 1)on the link scale,lognormal(-0.7, 0.75)when the dpar is omitted frombf()and so lives on the natural scale, andnormal(0, 0.5)on slopes - wider than the blanketnormal(0, 0.2)the other dpars take, because the point is to fence off zero rather than to shrink effects. Families can now declare such rows in the registry, so the next one that needs them does not need a special case. -
The
?rcogmod_weibulland?rcogmod_gammanotes about the shape were understated. They said the density is unbounded atndtfor a shape below 1, which is true, but the threshold that matters in practice is 2: below it the derivative of the log-likelihood with respect tondtis unbounded at every observation, so the posterior stays proper while the sampler grinds. On the data invignette("rt_models")the Weibull shape comes out at 1.4,ndtlands inside the dense left edge of the data, the step size collapses to 0.005 against 0.19 forcogmod_lognormal(), and mean treedepth goes from 3.9 to 8.1 - 19x the gradient evaluations and 19x the wall time, withRhat1.18 onndt. The density is cheap; all of the cost is geometry. Both help pages now set out the three regimes and say to prefercogmod_loggamma(), which nests the Weibull atshape = 1, when the shape comes out below 2.No prior is set on those shapes, deliberately, and none is set on
ndteither. Every obvious remedy was tried on that fit and measured:-
normal(2.4, 0.4)on the shape (softplus scale, 95% of its mass above a shape of 1.9) moved the posterior shape by 0.01. The likelihood prefers the low-shape corner by around 100 log units; the prior contributes 5. -
normal(-1.25, 0.05)onndt, centred below the fastest bulk response, left the posterior 6.5 prior SDs away at essentially its unconstrained value. Thendtlikelihood has a posterior SD of 0.003, some fifteen times sharper than that prior. The attempt cost 4% divergent transitions against 0.5%, 16% of iterations at maximum treedepth against 7%,Rhat1.43 against 1.18, and a slightly worseloo. - Fixing
ndtat the fastest observed response does remove the problem, by removing the parameter - and reinstates the min-RT bound this parameterization exists to remove. On these data the fastest response is 71 ms, which is not a decision.
Note what is not wrong:
ndtand the shape are jointly identified, sharply (posterior SD onndtof 3 ms), so this is not two parameters trading off with nothing to separate them and pinning one is not the missing ingredient.The slow sampling and a poor fit turn out to be the same fact. Across the ten families fitted in
vignette("rt_models")the Weibull comes last byloo- 196 elpd (SE 21) behindcogmod_loggamma(), and 95 behind the next worst. What the sampler struggles with is the model contorting itself to represent a left edge it cannot otherwise reach. Where the shape comes out above 2 the family is fine, ascogmod_gamma()is on these same data at 2.2; a shape below 2 is best read as the model asking for a different one.Under the older
ndt = tau * min(RT)parameterization the same singularity was damped by the logit Jacobian vanishing astauapproached 1, which is why it only became visible oncendtwas estimated directly. -
cogmod_priors()no longer leavesbrmswarning that a globalbprior “will not be used in the model as all related coefficients have individual priors already”. It set both the blanket row and the row for the coefficient namedIntercept, which is right when there are slopes beside the intercept but leaves the blanket row covering nothing underndt ~ 0 + Intercept. The pair is now checked in both directions.cogmod_lognormal()andcogmod_loggamma()also carried hand-written R copies of the shared mixture - their ownr*(),d*(),.prepare_*(),log_lik_*(),posterior_predict_*()andposterior_epred_*(), about 420 lines reproducing what the other seven families delegate. All of it now delegates too, verified bit-identical to the code it replaced before the change was made.cogmod_lognormal()andcogmod_loggamma()generated their Stan code from hand-written copies of the shared mixture rather than from the registry every other shifted family uses. The copies had drifted: the LogNormal one described its outlier scale as0.8 * minrtwhen it isminrt, and neither carried the folded normalising constant that makes the outlier term ~1.4x cheaper per gradient evaluation. Both now come from the one generator, verified against the R density to machine precision.-
cogmod_priors()now also covers dpars left out ofbf()entirely.brmsdeclares those as plain auxiliary parameters - class"<name>"with an emptydpar, on the natural scale with no link - so matching ondparalone missed them and they keptbrms’s own defaults. Those defaults are actively wrong here:uniform(0, min_Y)onndtreimposes the very min-RT bound the parameterization exists to remove,gamma(0.01, 0.01)onshapehas support only on the positives and would silently truncate away the inverse-Weibull half of the family, andpoutlierwas left flat over[0, 1]with half its mass above 0.5. All three are now replaced, with priors on the natural scale:lognormal(-1.2, 0.2),normal(0, 0.5)andexponential(100).The
poutlierprior for the omitted case has its mode at zero, unlike its modelled counterpart: leaving it out of the formula is taken to mean the data were trimmed or no outliers are expected. Its median is unchanged.
Breaking changes
-
Every family is renamed to a single
cogmod_*scheme.rt_lognormal()iscogmod_lognormal(),lnr()iscogmod_lnr(),ddm()iscogmod_ddm(), and so on; the two LBAs are told apart by their accumulator count, sort_lba()(no choice) iscogmod_lba1()andlba()(two choices) iscogmod_lba2(). Every derived function follows its family:rrt_lognormal()isrcogmod_lognormal(),rlnr()isrcogmod_lnr(),rt_lognormal_stanvars()iscogmod_lognormal_stanvars(), and likewise for the densities, the*_lpdf_expose()and thebrmspost-processing hooks.Two things were wrong with the old names.
lba(),lnr(),ddm(),rdm()andchoco()are generic enough to collide with anything else attached, and thert_prefix said “reaction time” on families that are not all RT-only. One prefix fixes both, andcogmod_tab-completes the whole package.The old names all still work in this version, as exact synonyms rather than wrappers - the
brmshooks included, so a model fitted before the rename can still be summarised. (They were removed in 0.3.0.) -
The
bsdpar is renamedboundaryincogmod_invgaussian(),cogmod_lba1(),cogmod_lba2(),cogmod_rdm()andcogmod_ddm(). This is not cosmetic:brmsnames the unpenalized spline coefficients of a modelbs, so any of those families combined with a smooth failed to compile withIdentifier "bs" is already in use. There is no alias for a dpar name - updatebs ~ ...toboundary ~ ...in formulas,bs =toboundary =in ther*()/d*()calls, and expectboundary_Interceptwhere you hadbs_Interceptin the output.Fits made before this change cannot be post-processed, because their draws carry the old dpar name; refit them.
cogmod_lnr(link_nuzero = )is nowlink_mu.brmsrequires the first distributional parameter of a custom family to be calledmu, somuis what the formula uses and what the argument now matches;nuzeroremains the name of the quantity, in the prose and inrcogmod_lnr()/dcogmod_lnr().The
r*()andd*()functions of every shifted family now validate their decision parameters, against the same bounds the generated Stan code checks. Onlycogmod_lognormal()andcogmod_loggamma()did this before; the other seven silently returned zero density orNaNdraws for, say, a negativesigma.r*()errors,d*()warns and returns zero - so a single bad posterior draw still cannot abort a whole call.-
cogmod_invgaussian(),cogmod_gamma(),cogmod_invgamma(),cogmod_weibull(),cogmod_invweibull()andcogmod_logweibull()now use the same parameterization ascogmod_lognormal():tauand theminrtdpar are gone, replaced byndtestimated directly (log link) pluspoutlier, withminrtcarried on the family as a constant. Every one of them gains an outlier component, andwith_outliers(),without_outliers(),p_outlier()andcogmod_priors()now work on all eight families.Update models as for
cogmod_lognormal(): replacetau ~ ...withndt ~ ..., dropminrt = min(df$RT), and addpoutlier ~ 1.ndtcoefficients are on the log scale.The
d*()/r*()functions gainpoutlierandminrtarguments, andrcogmod_gamma(),dcogmod_gamma(),rcogmod_invgamma(),dcogmod_invgamma(),rcogmod_weibull(),dcogmod_weibull(),rcogmod_invweibull(),dcogmod_invweibull(),rcogmod_logweibull()anddcogmod_logweibull()are new - those families previously had no R-level density or RNG at all.pcogmod_invgaussian()now returns the mixture CDF.Note that
cogmod_gamma()andcogmod_weibull()have an unbounded density atndtwhenever their shape falls below 1, which the outlier component cannot repair; fit them withinit = 0.cogmod_loggamma()nests both and lets the data choose the shape instead. cogmod_lba1()moves to the same parameterization:tauand theminrtdpar are replaced byndt(log link) pluspoutlier, withminrta family constant.rcogmod_lba1()anddcogmod_lba1()gainpoutlierandminrt.posterior_epred_cogmod_lba1()now explains why there is no expectation rather than calling it prohibitive: the decision time is(b - U(0, A)) / driftwith a drift truncated at zero, whose density is positive at 0, soE[1 / drift]diverges and the mean does not exist.All nine shifted families are now generated from a single internal registry, so the Stan code, the R density, the RNG, the likelihood and the predictions cannot drift apart. Verified family by family: Stan agrees with R to machine precision, each density integrates to 1, and each RNG reproduces its own density.
-
cogmod_lnr()moves to the samendt+poutlierparameterization as the RT-only families.tauand theminrtdpar are gone;ndtis estimated directly, on the log link, andminrtis a constant carried on the family object rather than somethingtauis scaled by.with_outliers(),without_outliers(),p_outlier(),cogmod_priors()andcogmod_inits()all work on it now.Because the LNR produces a choice as well as a time, its outlier component is not quite the RT-only one: the contaminant guesses uniformly over the two response options in addition to drawing an RT from the same half Student-t, so the mixture is `poutlier * (1/2) * g(t) + (1 - poutlier)
- f_k(t - ndt)
. The1/2is what keeps the joint density summing to one over both responses - without it the total comes to1 + poutlier`.
Update models as for
cogmod_lognormal(): replacetau ~ ...withndt ~ ..., dropminrt = min(df$RT)frombf(), and addpoutlier ~ 1. Fit withinit = cogmod_inits(f, df)rather thaninit = 0- on the log link,init = 0startsndtatexp(0) = 1second, above nearly every sub-second RT, which leaves every response attributed to the outlier component and the race parameters with no gradient at all.rcogmod_lnr()anddcogmod_lnr()gainpoutlierandminrtarguments. Verified against the RT-only families’ checklist: the joint density sums to one over both responses and integrates to one over time (with and without the1/Kterm, to confirm it is load-bearing), the Stancogmod_lnr_lpdfagrees with the R density to machine precision, and a simulated fit recoversndtwell above the fastest observed response. - f_k(t - ndt)
-
cogmod_rdm()moves to the samendt+poutlierparameterization, on the same shared machinery ascogmod_lnr().tauand theminrtdpar are gone: the family’s dpars are nowmu,driftone,sigmabias,boundary,ndt,poutlier,ndtis estimated directly on the log link, andminrtis a constant carried on the family object.with_outliers(),without_outliers(),p_outlier(),cogmod_priors(),cogmod_inits()andcogmod_stanvars()all work on it now.Update models as for
cogmod_lnr(): replacetau ~ ...withndt ~ ..., dropminrt = min(df$RT)frombf(), and addpoutlier ~ 1. Fit withinit = cogmod_inits(f, df)rather thaninit = 0orinit = 0.5.cogmod_priors()also supplies thesigmabias/boundarypriors that?cogmod_rdmpreviously told you to write by hand: the two enter the model only through the sumb = boundary + sigmabiasand trade off along a ridge worth a handful of log units, which under a flat prior and asoftpluslink is an improper posterior. The drift rates are left flat on purpose - unlikecogmod_lnr()’snuone, a drift pushed to zero does not produce a plateau, because a driftless accumulator still finishes and still wins sometimes.rcogmod_rdm()anddcogmod_rdm()gainpoutlierandminrtarguments, andpcogmod_rdm()gainspoutlierandminrttoo - its CDF is now the mixture’s, and still keeps the far-tail survival in log space.dcogmod_rdm()keeps itsresponse = NULLmarginal, which is now the sum of the two defective mixture densities. Invalid parameters now warn and return a zero density rather than erroring, matching the other mixture families. A drift of exactly zero is still accepted - it is the one closed lower bound in either registry, because driftless Brownian motion still reaches the threshold with probability one.Verified against the checklist: the joint density sums to one over both responses and integrates to one over time for several parameter sets and
poutliervalues (including as the start-point range shrinks to1e-6), the Stancogmod_rdm_lpdfagrees with the R density across the parameter grid, and a simulated fit recoversndt = 0.256against a true0.25- some 200 times the fastest observed response, which the oldtau * minrtbound could not have expressed. -
cogmod_lba2()moves to the samendt+poutlierparameterization. Its dpars are nowmu,driftone,sigmazero,sigmaone,sigmabias,boundary,ndt,poutlier. Update models as forcogmod_lnr(): replacetau ~ ...withndt ~ ..., dropminrt = min(df$RT)frombf(), and addpoutlier ~ 1.Two long-standing bugs in the density came out with it.
The density was not normalised. A normal drift rate can come out negative, and such an accumulator never reaches the threshold, so a trial on which both drifts are negative produces no response at all.
rcogmod_lba2()has always resampled until at least one is positive - butdcogmod_lba2()never divided by the probability of that event, so the density integrated to the probability rather than to one. At drift rates of0.5and0.2with SDs of1.5it came to0.83, and the simulated choice proportion was0.562against an integral of0.468. Because the shortfall depends on the parameters, it biased estimates rather than merely offsetting the likelihood. Both the R and the Stan densities now condition on the event the process is conditioned on.The
(1 / A)cancellationcogmod_lba1()was fixed for was still here. The defective density divides `drift * (Phi(z2) - Phi(z1)) + sigma * (phi(z1)- phi(z2))
by the start-point range, and both differences vanish linearly in it; the loser's survival was computed as1 - CDF, which cancels the same way. Relative error reached 2.7% atsigmabias = 1e-5and 320% at1e-7. Both now go through the kernelscogmod_lba1()already uses (.lba_dens_over_A(), and a new.lba_surv_raw()that takes the survival directly rather than as1 - CDF`), which the two families now share in R and in Stan.
The
.Machine$double.epsfloor is gone too. It turned every RT below the point where the density becomes representable into a log-density of exactly-36.04- a constant the model never produced, with a gradient of zero. Where the density really has underflowed the log-density is now-Inf, and the outlier component is what keeps the mixture finite there.rcogmod_lba2()now imposes the positive-drift condition exactly, by sampling which accumulator is positive and then the truncated normals, instead of a rejection loop. Itsmax_iterargument is therefore gone, along with the fallback that forced a drift positive withabs()- and so drew from the wrong distribution - whenever the loop ran out.Note that the evidence scale of an LBA is arbitrary: multiply the drifts, their SDs, the start-point range and the threshold by any
c > 0and every finishing time is unchanged, so the likelihood is exactly constant along that ray.cogmod_priors()now fences all four positive parameters, which makes the posterior proper, but only fixing one SD in the formula (sigmazero = 1inbf()) identifies the scale. This was true before and is now documented. - phi(z2))
-
cogmod_ddm()moves to the samendt+poutlierparameterization, andsigmatauis renamedsigmandt. Its dpars are nowmu,boundary,bias,sigmadrift,sigmabias,sigmandt,ndt,poutlier.sigmatauwas the between-trial range of the non-decision time expressed as a fraction ofminrt(st0 = sigmatau * minrt). Withtauand theminrtdpar both gone it was named after a parameter that no longer exists and scaled by a constant the user no longer sees, so it is nowsigmandt, which isst0itself, in the same unit as the data, on a log link -ndtremains the lower bound of the resulting Uniform. Update models by replacingtau ~ ...withndt ~ ..., droppingminrt = min(df$RT), addingpoutlier ~ 1, and rewriting anysigmatauterm assigmandtin seconds.All three between-trial variability parameters remain legitimately zero, and fixing them in the formula (
sigmadrift = 0) still recovers the classic 4-parameter DDM.cogmod_priors()now supplies priors for all three: each has a floor at zero that its link reaches only at minus infinity, and the likelihood stops changing well before then, which underbrms’s flat default is an improper posterior.posterior_epred_cogmod_ddm()keeps its closed form - the DDM is the one choice family here with a usable one - now usingndtdirectly and blending in the outlier component whenpredict_outliersis set, like the RT-only families.rcogmod_ddm()anddcogmod_ddm()no longer take...forbrms::rwiener()/brms::dwiener(), which the shared mixture machinery cannot forward; setoptions(wiener_backend = )instead.Both R and Stan evaluate the decision component at a non-decision time of zero, which
dwiener(),rwiener()andwiener_lpdf()all refuse. Since the Wiener density depends on the time and the non-decision time only through their difference, both offset the pair by the same1e-10, and the Stan literal is generated from the R constant so the two cannot drift apart.All four choice+RT families are now on the direct
ndt+poutlierparameterization, andtau+minrtis gone from the package.
Bug fixes
-
dcogmod_lba1()was wrong for small start-point ranges. Its density is built fromdrift * (Phi(z2) - Phi(z1)) + sigma * (phi(z1) - phi(z2))divided by the start-point rangeA, and both differences vanish linearly inA- so evaluating them directly and then dividing lost every significant digit onceAwas small. The old code also floored the bracket at1e-10, which turned that underflow into a spurious density floor spread over the whole line. The result: the density stopped integrating to one below aboutA = 0.1and was outright divergent belowA = 0.01.Both differences are now computed stably - a Taylor expansion in
delta = A / (sigma * t)below1e-4, and tail-aware differencing above it - and the floor is gone. The density integrates to 1 fromA = 2down toA = 1e-8, and converges to the recinormal (LATER) limit at the expected first-order rate. The same fix is in the Stan code, which matches the R density across 960 parameter combinations.
Bug fixes (parameterization)
posterior_epred_cogmod_logweibull()returnedexp(mu + sigma * 0.5772), which is the geometric mean of the decision time - the exponential ofE[log(RT)]- rather than its mean. It now returnsexp(mu) * gamma(1 - sigma), andInfwheresigma >= 1and no mean exists.posterior_epred_cogmod_invweibull()now returnsInfwhere the Frechet shape is<= 1and the mean does not exist, instead of a finite but meaningless value.-
cogmod_lognormal()no longer usestauandminrt. Non-decision time is now estimated directly asndt, in seconds, through a log link, and the family gainspoutlier, the proportion of trials generated by an outlier process rather than by the decision process.The old parameterization set
ndt = tau * minrtwithtauin(0, 1)andminrtinjected as a constant, which cappedndtat an order statistic of the sample. Because that cap was shared across the whole dataset, a non-decision time larger than the fastest observed response was inexpressible - so any condition or participant whose truendtexceeded the global minimum RT could not be recovered, and the misfit surfaced instead as spurious effects on the other parameters.What makes the direct parameterization tractable is the outlier component: a fixed half Student-t (scale
0.4,3df) mixed in with weightpoutlierkeeps the density positive belowndt, turning the hard min-RT boundary into a finite cost and leaving the log-density smooth. No bound is taken from the data. The half-t is flat at the origin, so the fastest responses are not starved of density; its tails are heavy enough that supplying RT in milliseconds degrades rather than underflowing to zero; and its mean is finite, whichposterior_epred()requires.Models must be updated: replace
tau ~ ...withndt ~ ..., dropminrt = min(df$RT), and addpoutlier ~ 1. Note thatndtcoefficients are on the log scale, soexp()them for seconds. -
The outlier component is scaled by
minrt, the fastest reaction time that could plausibly be a real decision, used directly as the half-t scale with no conversion factor. It defaults to0.3seconds, where the conditional accuracy functions in the Outliers article show responses sitting at chance across three paradigms. 61% of the component falls belowminrtwhatever value is chosen.It is a judgement about the task rather than a statistic: nothing is read off the sample, and
ndtis not bounded by it.This matters because the shifted LogNormal is scale-equivariant on its own - multiply every response by 1000 and
ndtcomes back multiplied by 1000 - and a component pinned to the second would be the one thing breaking that. With millisecond data and a fixed scale, the outlier component sits many orders of magnitude below the decision density everywhere in the data,poutliercollapses toward zero andndtreverts to being pinned by the fastest observed response, silently and without a warning. Settingminrtin the unit of the data makes the likelihood exactly equivariant instead, so millisecond data needminrt = 300.minrtis a constant, never estimated, and is deliberately not a dpar:brmshas no notion of a default for one, so a dpar omitted from the formula is estimated rather than defaulted. It is carried on the family, and the matching Stan constant comes fromcogmod_lognormal_stanvars(), which also accepts the family itself so the two cannot drift apart:fam <- cogmod_lognormal(minrt = 0.3) f <- brms::bf(RT ~ 1, sigma ~ 1, ndt ~ 1, poutlier ~ 1, family = fam) brms::brm(f, data = df, prior = cogmod_priors(f, df), stanvars = cogmod_lognormal_stanvars(fam))rcogmod_lognormal()anddcogmod_lognormal()gain a matchingminrtargument. Models fitted before this change fall back to the default, so their predictions are unaffected. The
wagenmakers2008dataset has been removed. Those data were supplied by the original authors for distribution in thertdistspackage specifically, and no open licence covers them, so redistributing them here was not appropriate. They remain available asrtdists::speed_acc;rtdistsis inSuggests, and the vignettes and paper now reconstruct the same subset from it (!censor & response != "error" & rt <= 2), so previously reported results are unchanged.The experimental confidence signal detection model (
rconf_sdt(),dconf_sdt(),conf_sdt_stanvars(),conf_sdt_custom_family()and itsbrmsmethods) is no longer exported. It was not ready, and the code is commented out inR/conf_sdt.RandR/conf_sdt_brms.Rpending a rework.Priors are now required.
brmsassigns a flat, improper prior to the intercept of any custom-family parameter it does not recognise, which here means bothndtandpoutlier, and the likelihood has two flat directions that a flat prior turns into an improper posterior:poutliertoward 1, where every response is attributed to the outlier component andmu,sigmaandndtdrop out of the density altogether; andndttoward 0, where the model reduces to an unshifted LogNormal and the gradient with respect tolog(ndt)vanishes. The second is inherent to putting a positive shift on a log link and has nothing to do with the mixture, which is why a prior onpoutlieralone is not enough. Symptom: intercepts around1e14,Rhatnear 2 and an effective sample size of about 5, with no error raised.cogmod_priors()fills the gap.
New features
-
cogmod_priors(formula, data)fills in every priorbrmswould otherwise leave flat - forcogmod_lognormal(), thendtandpoutlierrows. It starts frombrms::get_prior()for the model in hand rather than guessing, so0 + Interceptformulas, interactions, group-level terms and smooths are all handled and a prior matching no parameter is impossible by construction. The result is passed throughbrms::validate_prior(), so a malformed specification errors there with the offending row in view, and the return value is the complete prior table with asourcecolumn marking each row asuserordefault- print it to see exactly what the model will be fitted with. To change one, edit the row;c()will not work for a slot the table already covers, becausebrmsrejects two priors for the same slot.The family is read off the formula, so build it with
brms::bf(..., family = cogmod_lognormal()). Any other family, or a formula carrying none, gets a message and thebrmsdefaults unchanged, so the call is always safe to leave in a script. p_outlier()returns the posterior probability that each trial came from the outlier component rather than the decision process - the mixture responsibility, averaged over draws. Responses belowndtcome out at 1 and those in the bulk near 0, but the probability rises again in the far slow tail, where the half-t has heavier tails than the LogNormal; that is the mechanism behind the advice to filter implausibly slow responses before fitting. The responsibility is computed on the log scale, so it stays finite in tails where both components underflow to zero. It returnsrtandp_outlieronly; thefastcolumn was a marginal median split that ignored any grouping in the model and is gone.posterior_predict()andposterior_epred()forcogmod_lognormal()describe the decision process alone by default, as ifpoutlierwere zero. For visualising effects the outlier component is a nuisance that pulls expected values toward its own mean (0.441 s) and adds a spike of implausibly fast draws; it is also a fixed regularizer rather than a claim about how guesses are distributed, so simulating from it means simulating from something the model does not assert. The likelihood is unaffected and is always the full mixture, soposterior_predict()andlog_lik()no longer describe the same distribution - a hand-rolled LOO-PIT check should usewith_outliers().-
with_outliers()restores the fitted mixture for prediction, andwithout_outliers()returns to the default. The main use for the former ispp_check(): on untrimmed data the decision-only predictive has no fast spike to match the one in the data, which reads as misfit. The same flag can be set up front withcogmod_lognormal(predict_outliers = TRUE).It is carried on the family rather than passed as an argument because
brmsdoes not forward extra arguments to a custom family’s prediction methods -posterior_epredreaches the method withprepand nothing else - andinsight,modelbasedandmarginaleffectsinherit that. Carrying it on the object is what makes it work through all of them. New Outliers article validating the outlier-mixture specification against the Illusion Game dataset, cross-checked against the lexical decision data of Wagenmakers et al. (2008) and the brightness discrimination of Ratcliff and Rouder (1998), both from
rtdists. It also derives recommended priors for all four parameters from the rates it measures, and explains why they matter more under this parameterization than the last:ndtis no longer bounded above by construction, andpoutlierhas a degenerate region near 1 wherendtbecomes unidentified.
cogmod 0.1.0
Models for subjective scales
- Beta-Gate (
cogmod_betagate(),rcogmod_betagate(),dcogmod_betagate()), a reparametrised ordered beta model. - Discrete Beta (
cogmod_betadiscrete()and friends) for Likert-type responses. - Choice-Confidence, CHOCO (
cogmod_choco(),rcogmod_choco(),dcogmod_choco()) for bipolar scales. - Signal detection with confidence ratings (
conf_sdt()).
Models for decision making
- Lognormal race (
cogmod_lnr()), linear ballistic accumulator (cogmod_lba2()), drift diffusion with optional across-trial variability (cogmod_ddm()), and the racing diffusion model (cogmod_rdm()).
Models for reaction times alone
- A consistently parametrised set of shifted, right-skewed response distributions:
cogmod_lognormal(),cogmod_invgaussian(),cogmod_gamma(),cogmod_invgamma(),cogmod_weibull(),cogmod_logweibull(),cogmod_invweibull(),cogmod_exgaussian()andcogmod_lba1().