Skip to contents

frmtmb 0.80.0

Fitting

  • BREAKING: frm(quadrature = TRUE) integrates two nested random-effect blocks, such as (1 | school/class). Before, frmtmb refused a model in which a row reads more than one block. The new route integrates the inner block inside each level of the outer block with Gauss-Hermite rules that the Laplace fit centers and scales, and re-anchors the rules at the estimate. Against independent references the log-likelihood error at the default nodes was 9e-11 to 7e-6 on seven designs with standard deviations near 1, and 5e-4 to 6e-4 with an inner standard deviation near 3 or an outer one of 3; on a near-separable response it was 1.7e-2, and the fit warned that its rule did not settle. The Laplace error on the same designs was 0.07 to 1.4. More nodes do not always reduce the error on the hard designs.

  • A nested fit whose rules do not settle at the estimate warns with class frmtmb_quad_unsettled. Profile and root-search intervals (confint(method = "profile"), "uniroot", profile(), hypothesis(method = "profile"), conditional_effects(band = "profile")) and ranef(condVar = TRUE) stop on a nested fit with class frmtmb_quad_nested_unsupported; Wald and bootstrap intervals work.

  • frmtmb still refuses crossed blocks, (1 | g) + (1 | h), and says that their integral does not factor. It also refuses three or more blocks, and two blocks on different predictors, such as (1 | g) in mu and in sigma, each with its reason. (1 + x || g) is one block of two coefficients and is refused; the message says to write it as (1 | g) + (0 + x | g), which the nested route integrates.

  • New frmtmb_control(quad_nodes = ) sets the nodes per level of the nested route (default 11 outer, 15 inner). New frmtmb_control(quad_max_gb = ) (default 4) limits its memory: the fit predicts the memory from small probe tapes and measured costs per family, and stops before the Laplace warm start when the prediction is above the limit.

  • BREAKING: a covariate far from zero against its spread fits the same model as the covariate centered. With y ~ (x + 1e6) + (1 | g) and an sd of 1, the optimizer stopped at its start with code 0. On poisson and Gamma the slope was 6 to 10 standard errors off and the fit 20 to 47 log-likelihood units short, on 5 of 5 data sets each, with no warning on 6 of the 10. Bernoulli was 0.9 to 2.6 standard errors off from x + 1e5, and a gaussian fit without a group term 12 standard errors off at x + 1e6. The optimizer now steps in coordinates in which each ill-conditioned fixed design has orthogonal columns, the ones the standard errors already used. A design with a condition number below 1e3 keeps its fit. On a grid of offsets from 1e3 to 1e8, four families, with and without (1 | g) and 5 data sets each, all 240 fits give the slope, its standard error and the log-likelihood of the centered fit. At x + 1e8, 11 of the 40 fits warn of “false convergence (8)” at the maximum; none below that warns. The same holds for:

    • a cumulative() fit, which was up to 3.6 standard errors off with no warning at 5 x + 1e6;
    • a shifted column in sigma’s formula, in a zi formula, in an nlpar’s formula, beside s(), offset(), weights() or a factor, and its interaction with a factor;
    • a nonlinear body linear in a shifted column, b0 + b1 * x, which was 7.6 to 9.9 standard errors off, one of two with no warning;
    • a prior with lb or ub on class b, which kept the old fit, 6.3 to 9.4 standard errors off, 4 of 5 with no warning. A bound on the intercept of such a design, or one that centering does not condition, keeps the old fit and warns (class frmtmb_ill_design);
    • profile() and confint(method = "profile") or "uniroot" on a shifted coefficient, which were NA with an unclassed warning;
    • frm_allfit()’s bobyqa and NLopt fits.

    On two raw polynomials with (1 | g) whose group sd the optimizer ran to zero, the fit now reaches lme4’s maximum at an sd near 0.005, 1e-4 higher.

  • BREAKING: the frame drops a fixed-effect column by two tests on the design with its columns scaled to the same size. A column whose QR leaves less than 1e-11 of its norm, with every column but the intercept centered, is aliased and is dropped with the message “rank deficient”. It was 1e-7 on the unscaled, uncentered design, as in lm() and lme4, which dropped the slope of x + 1e7 with an sd of 1, and products and powers of shifted columns (w1:w2, I(w^2)) from an offset of 1e4: the fit was 0.43 to 50 log-likelihood units below the model asked for, with a message only. Such columns are now kept, as brms keeps them. Then, in their order, the columns that the optimizer cannot fit (a centered condition number above 1e10, or columns dependent to 1e-11 without centering) are dropped with a warning of class frmtmb_design_capped: the data identify them, so the model fitted is not the one written. Products and powers of columns shifted by 1e6 meet this warning. A raw polynomial far from zero, poly(x, 8, raw = TRUE) on [1000, 1010], keeps 5 columns with finite standard errors. A near alias that the old test dropped and the new one keeps, such as x2 = x1 + 1e-9 * noise, now has standard errors near 1e8, as glm() gives. An exact alias is dropped as before.

  • frmtmb_control(autoscale = TRUE) on a column far from zero warned of “false convergence (8)” at the maximum. From x + 1e3 such a fit is now silent, and its standard errors are those of the centered fit.

  • BREAKING: a nonlinear fit from its default start also fits from each sign pattern of the body’s coefficients and returns the highest point. When that point is higher than the first fit by more than 0.01, a message of class frmtmb_nl_multistart says so. Before, a default fit could stop at a lower maximum at return code 0 with no warning: b1 * (1 - exp(-b2 * x)) under gaussian() stopped at its linear limit, 69 to 75 below the maximum on 5 of 5 data sets, and a bernoulli() b1 * exp(b2 * x) stopped at the other sign of b2.

    • The patterns vary each coefficient that the body does not read as an added term, so a + b * x is not affected. With four such coefficients or fewer, every pattern runs; with more, every pattern of the four that move the body most. The order of the terms does not change the result.
    • A coefficient that moves the body far more or less than 1 per unit starts at the body’s scale: b2 in b1 * exp(b2 * x) with x in the thousands starts near 1e-3.
    • On 450 fits (9 families, 5 bodies, 2 slopes, 5 seeds), 16 stopped more than 1e-4 short of the best of 8 random starts, 11 of them with no warning; now 0 do. On 335 other fits (15 bodies, 6 families, 5 seeds), 97 were short, 38 with no warning; now 11 are, 5 with no warning. They are Michaelis-Menten bodies under bernoulli() and binomial(), a power law under cumulative(), a four-parameter logistic under bernoulli() 2.8e-4 short, and fits that warn. On these 785 fits none is lower than before.
    • The point a pattern reaches can itself be a second maximum. The fit then converges with no warning and is still short: on 1 of the 87 fits that the patterns raised, 1.84 short, where the default start alone stopped 2.29 short with a warning. A start elsewhere can show it.
    • The cost is 1.2 to 2.7 times the fit time with two or three such coefficients, and 3.5 times with four, on fits of 300 rows. On fits of 20000 rows it is 1.5 to 2.8 times with two or three, 5.4 times with four, and 47 times with five, where the fit gained 15906 log-likelihood units.
    • Give start, or set options(frmtmb.nl_sign_probe = FALSE), to skip it. A located prior does not skip it.
  • BREAKING: a nonlinear coefficient that the body reads as an added term is stepped in the units by which it moves the body, where these units are 16 or more times from 1. The curvature of a normal, Student t or logistic prior on it is added; under another prior with a curved density, such as a gamma or lognormal prior, it keeps its old steps. a * x with x near 1e-20 now reaches the maximum of y ~ 0 + x; before, it stopped at its start, -299.33 against -272.50, at return code 0. a + b * x with y near 1e6 now reaches the maximum of y ~ x; before, it stopped at -3045.62 against -1658.76. In the sign patterns’ runs and the refit of their winner, a coefficient inside a curve is stepped in its start size where that is 16 or more times from 1, and keeps its old steps under a prior. b1 * (1 - exp(-b2 * x)) and b1 * exp(b2 * x) with x near 1e-6 now fit as with x near 1 on 40 of 40 data sets.

  • When the refit from the best sign pattern does not stop where that pattern’s run stopped, and raises no warning of its own, frm() warns with class frmtmb_nl_multistart_unsettled.

  • BREAKING: an exact gp() in a parameter of a nonlinear body, such as bf(y ~ a + b * z, a ~ gp(x), b ~ 1, nl = TRUE), starts at the maximum of the marginal of the body made linear at its start. Before, the field had no warm start. frm() also descends from the old start and keeps the better fit, so no fit reads lower than before. On 192 test fits of 12 bodies, frm() stopped short on 56, by up to 290 nats, with return code 0 and no warning. Now 8 stop short, by up to 36.5 nats, also with no warning, and none reads below its old value. 5 of the 8 are low-count Poisson fits, where the linear spelling y ~ z + gp(x) stops at the same value (see the Limits of 0.79.0). On a gaussian body additive in the field’s parameter (a + b * z, a + b * z^2, a ~ gp(x) + (1 | g), a ~ gp(x, by = f)) no fit stops short, which closes the gap of a nonlinear body that the Limits of 0.79.0 name, 98.5 nats on a trend plus a wiggle. With a start of your own the field starts as before. These fits take about 2 to 2.2 times as long.

  • BREAKING: frm() now leaves a ridge that the restart from a saddle did not reach, on a model with two or more nonlinear bodies. The restart moved only the body that was constant; when the restarted fit still warns that it may not have converged, frm() now also moves the other bodies. nlf(mu ~ b1 * exp(b2 * x)) beside nlf(disc ~ exp(d1 * d2 * z)) under cumulative() stopped at the linear limit of exp(), at log-likelihood -377.03 with return code 1, where a start reaches -371.54. On 30 such fits, 20 stopped short by up to 5.3 nats, each with a warning. Now none does. These fits take about 1.4 times as long. A model with one body does not change.

  • BREAKING: frm() fits mi() on the response beside a latent residual when no other density reads the missing values. Before, it refused this model with a reason that is false for it. Those rows leave the objective, and the latent residual of such a row has no observation. On lognormal the log-likelihood is the exact marginal of the observed rows to 1e-8. frm() still refuses the model when another response reads the missing values (bf(w ~ mi(y))).

  • BREAKING: an exact gp() beside ar() on a gaussian response now starts at the maximum of the model’s own marginal. That marginal includes the ar coefficients. Before, the start read a marginal of the response alone.

    brms’s default ar() (cov = FALSE) moves the mean of each row by the earlier residuals of its group. So the field integrates out in closed form beside it. This holds for ar() of any order, with or without gr, and with a free sigma and no se() or weights(). The start is used only where the objective reads the marginal’s value at the marginal’s maximum, to 1e-6.

    On a step-shaped field, n 160, the fit stopped 25.9 nats short with convergence code 0 and no warning. Five more such rows stopped 0.43 to 26.8 nats short. Now all six reach the best value that other starts found, and two read above it.

  • On step-shaped data beside ar(), the maximum now usually has an ar coefficient at or just above 1: a unit root takes the step, and the field is smaller. On the step row above it is 1.008, with sdgp 0.32 against 0.75. This is the likelihood of brms’s cov = FALSE form, whose ar is unconstrained in brms too.

  • The time of a fit beside ar() changes. In one process, against the former start: 0.52 times at 500 positions, where the fit also read 7.8 nats higher; 1.2 to 1.3 times at 300 positions; on panels, with rows of many series on shared positions, 1.5 to 1.7 times on 1000 rows of 40 positions and 2 to 5 times on 600 rows of 50 positions, under about 2 seconds in each.

  • BREAKING: off the gaussian, where the start’s marginal of the response has a second maximum at another length scale, frm() also starts from it and keeps the better fit. This was read on the 150 rows of bernoulli, Poisson, binomial, negative binomial, Gamma, lognormal, student, sigma ~ and ar() fits on which the last release named its limits. Before, 13 stopped short by more than 1e-3 nats, worst 25.9. Now 5 do, worst 11, and no row is lower.

    The second start ran on 40 of those fits and took 13 percent of the time of all 150. On 72 rows of smooth curves it ran on 25 and took 15 percent. A single fit where it runs and loses takes up to about 2.2 times as long: 14.5 s against 8.4 s on a Poisson fit of 240 rows, and 32.9 s against 15.2 s on a bernoulli fit of 240 rows.

  • A constant sigma (bf(y ~ gp(x), sigma = 0.3)) is a known noise in the start of an exact gp(). Before, the marginal read no noise at all: at the fit’s optimum it stood 1.6e12 from the objective, and its maximum was both the start and the value the convergence check reads. Beside ar() a constant sigma takes the working start.

  • The convergence warnings of a fit have their own classes, before frmtmb_warning: frmtmb_conv_code (“Optimizer did not report convergence”), frmtmb_conv_gradient (“Large maximum absolute gradient”) and frmtmb_conv_gradient_nonfinite (“The gradient at the reported optimum is not finite”). The messages do not change. Code that reads class(w)[1] sees the new class; inherits(w, "frmtmb_warning") is still true.

Log-likelihood and priors

  • BREAKING: under a prior with a density (frm(prior = )), logLik() is the log-likelihood of the data at the estimates. It does not include the log prior. The new logLik_penalized() gives the value that the fit maximized, with the log prior, which logLik() gave before. brms’s log_lik() is the data part too; glmmTMB’s logLik() with priors = is the penalized value. For y ~ x + (1 | g) with a normal(0, 100) prior on the slope, logLik() was 5.52 below the fit with no prior at the same estimates. That is the log of the prior’s normalizing constant. Now logLik() is the objective of the fit with no prior at the MAP estimates, to rounding. The split is exact for the Laplace approximation, quadrature = TRUE, importance = and frmtmb_control(profile = TRUE), because a prior there reads only parameters outside the integral. Under REML = TRUE a prior on the fixed effects is inside the integral, so logLik() stops with a message there. The log prior term is what the optimizer adds, with the log-Jacobian of each change of scale and no truncation normalizer, so it is not brms’s lprior; ?logLik_penalized says how they differ.
  • BREAKING: under a prior with a density, the estimates do not maximize the log-likelihood, and a parameter that the prior shrinks is not a full degree of freedom. So logLik() has df = NA, with the reason in attribute df_why, and AIC(), BIC() and extractAIC(), and so step(), stop with a message when they dispatch on such a fit, as anova() and drop1() do. AIC(m0, m1) stops when either frmtmb fit has such a prior. A call that reaches stats’s own methods with the df of logLik(), as AIC(lm_fit, map_fit), BIC(lm_fit, map_fit) or AIC(logLik(map_fit)), gets NA for that fit. A package that counts the parameters itself is not covered: insight::get_loglikelihood() replaces the NA with its own count, so performance can still give an AIC for a fit with a prior. A prior that only sets bounds adds no density, and these methods work; so does a uniform() prior on a coefficient or an nlpar, whose log density is a constant inside its bounds.
  • summary() of a fit with such a prior prints logLik and the log prior in place of AIC and BIC. Under REML with a prior on the fixed effects it prints the maximized value and says why there is no logLik. It marks the method (MAP) only for a prior with a density, not for a prior of bounds alone or a uniform one.
  • anova() of two frm_multiple() fits stops for the likelihood rules ("D3", and "D2" with use = "likelihood") under a prior with a density, for the reason anova() of two fits stops. The Wald rules still work: they are Wald tests at the MAP, consistent with the Wald intervals of summary(), and under a prior their nominal size is not guaranteed.
  • The warning for a discrete log-likelihood above 0 now also checks a fit with a prior.
  • frm_allfit() and the restarts of a nonlinear fit still compare the value that each fit maximized, now through logLik_penalized(). Under a prior, their printout names that value the penalized log-likelihood.
  • logLik_penalized() on a frm_multiple() result stops with a message, as logLik() does.

Predictions and their standard errors

  • BREAKING: fitted() with allow_new_levels = TRUE at a grouping level the fit did not see is the mean of the expected response over that level’s effects, as brms’s posterior_epred(sample_new_levels = "gaussian") is; brms’s default "uncertainty" is still refused. Before, it read the effects at their mean of 0, which is the median: a poisson (1 | g) at sd 1 read low by a factor of 1.647. On a log link that reads the mean the value is the closed form exp(v / 2) times the median. Est.Error holds the effects’ spread, at the estimated sd, and the Q columns are their quantiles. sample_new_levels = "old_levels" and scale = "linear" do not change.

  • BREAKING: conditional_effects()’s se__ is on the displayed scale, as brms’s MAD of its draws is. Before, it was on the link scale: a lognormal mean read 0.027 where the response scale reads 0.045, and a poisson mean 0.045 where it reads 0.065. On a row with a new me() value, a latent or a new level, se__ and the band are fitted()’s. Elsewhere the band does not change.

  • BREAKING: a new row that reads two independent draws, a latent residual on mu beside a me() value on another predictor or two me() calls on two predictors, is integrated over both. Before, the second predictor moved by its regression on the first, and a me() value on another predictor than mu beside a latent was not read. On a hurdle_poisson() row, the draw’s sd read 0.733 against 0.925, and the mean 2.110 against 2.142.

  • BREAKING: a lognormal whose sigma reads a draw (me(), a latent or a new level) has an infinite exact mean over it, from the draw’s far tail; so does a lognormal component’s sigma<k> of a mixture, and a sigma body that reads a nonlinear parameter with a draw. fitted() gives the 11-node Gauss-Hermite mean, which reaches 5.19 sds of the draw as a sampler’s draws do, with one warning per call of class frmtmb_mean_not_finite. On sigma ~ (1 | g) at sd 0.27 it reads 2.0897, as a mean of 4e6 draws does; 0.79.1 read the median, 2.0038. Past a draw sd of about 0.3 on sigma no mean of draws is stable, and the warning gives how far the rule moves. Before, it gave the value at a far node of the rule: 1.7e162 at sdx = 0.8. Any other mean whose rule does not settle as it grows is read the same way, with a warning of class frmtmb_mean_unsettled.

  • A nonlinear body has a standard error: frm_linpred(se.fit = TRUE), fitted()’s Est.Error and its Q columns read the delta method through the body’s taped Jacobian, and its memory grows with the rows, not their square. Before, Est.Error was NA with no message. Under allow_new_levels = TRUE a row of levels the fit saw answers, and a new level that a body reads has no se.fit, refused with class frmtmb_nl_new_level; fitted() integrates over that level’s effects.

  • conditional_effects() on a body whose nonlinear parameter reads me() answers, with the band and se__ that fitted() reads on the row. Before, it stopped.

  • BREAKING: conditional_effects()’s band for a category of a row with a me() value is fitted()’s, the draw’s own quantiles. Category 3 of a cumulative() row at sdx = 1.2 read 0.196 to 0.305; it now reads 0.090 to 0.254, and the draw’s quantiles at the estimates are 0.098 to 0.247.

  • A draw that moves nothing fitted() reads, as a me() value on a Gamma’s shape, costs no quadrature nodes. Past four draws that move it, the four that move it most are integrated, with a warning of class frmtmb_draw_dims.

  • BREAKING: on a latent residual (ar(), ma(), arma(), cosy() or unstr() on a family with no residual), fitted(), residuals() and frm_linpred(se.fit = TRUE) give standard errors for the fitted rows at any number of rows. Above 3000 rows they were NA, with a message. They now read the joint covariance through the sparse Cholesky factor of the joint precision, and each row’s variance through the selected inverse of that factor. On one poisson series with ar(cov = TRUE), fitted() took 4.7 s at 2000 rows and 332 s at 8000 rows. It now takes 0.17 s and 0.42 s, and 1.7 s at 32000 rows. With hu ~ (1 | id) on hurdle_poisson(), or zi ~ (1 | id) on zero_inflated_poisson(), it takes 3.3 s at 32000 rows. The values agree with the dense route to 1.3e-13, relative.

  • BREAKING: beside another group-level block, fitted() on new rows of a latent residual keeps its standard errors above frmtmb.latres_dense_rows rows. They were NA, with a message. A new row reads the estimates and the levels of the other block, and no latent.

  • A fit with a latent residual keeps no dense joint covariance after fitted(). It keeps the sparse factor of its joint precision, which grows with the rows. On one poisson series, a saved fit of 2000 rows grew from 5.1 MB to 36.2 MB after fitted(), and now grows to 5.7 MB. At 8000 rows it grew to 500 MB with both row limits moved; with the default limits its standard errors were NA and it stayed at 9.6 MB. It now grows to 12.3 MB, with every standard error given. Beside a group-level block of 50 levels it grows from 11.6 MB to 15.7 MB at 8000 rows, and from 35.6 MB to 51.9 MB at 32000 rows.

  • options(frmtmb.latres_dense_rows = ) now limits only a dense block of the joint covariance of a latent residual that a function returns or must form: frm_joint_cov(), frm_lp_basis() on the fitted rows, and the standard errors of the category probabilities of an ordinal family’s fitted rows. frm_curve() and conditional_effects() on new rows are not refused. options(frmtmb.latres_se_rows = ) now applies to those category probabilities and to the Laplace check alone. A fit whose covariance of the estimates is held as a factor, which an ill-conditioned design gives, keeps the dense joint covariance and both limits as before.

  • frm_lp_basis() on a fit with a latent residual leaves out of coef_pos every column that no row of A loads: a latent, a level that the rows do not name, and a coefficient whose covariate is 0 on every row. Such a column adds nothing to A V A'. A new row loads no latent.

  • BREAKING: the standard errors of marginaleffects hold the group-level effects on a fit with a group-level term, a smooth, gp() or hsgp(). get_coef() gives the group-level effects at their modes after the coefficients, under frm_lp_basis()’s names (b.<term>.<level>.<coef>), and the sdgp and lscale of an exact gp(), whose field reads them. get_vcov() gives their joint covariance with the coefficients and the variance parameters, which fitted() reads. Before, the delta method held them at their modes, silently wrong on most fits: on 168 fits with (1 | g) or (1 + x | g) on four families, avg_predictions() was more than 25 percent too wide on 77 percent of them (median 2.03 times) and avg_slopes() on 52 percent (median 1.30 times), and the standard error of a level was off on 67 percent. Now the standard errors of the 2016 levels are fitted()’s to within 1e-5, and the averages, slopes and contrasts are within 25 percent of the draws on 164 of the 168 fits; on the other 4 the delta method itself is that far. On a poisson (1 | g) row, 0.4875 where it was 1.4350.

  • avg_slopes() on an exact gp() field reads the field: 0.0308, where it read about 3e-7 on a gaussian field. That is the delta method’s value, as frm_lp_basis() gives it; on the same data brms’s posterior SD of that average slope is 0.154, because the slope is flat in the length scale near its estimate and steep two standard errors above it. Far from its data an exact gp() is also where marginaleffects’ finite-difference Jacobian parts from fitted()’s exact one, by up to 21 percent at the points measured, with numderiv = "fdcenter" no closer; read a level there from fitted().

  • re_formula = NA takes the group-level effects out of get_coef(); a smooth’s coefficients stay. Inside hypotheses() on the fit itself, get_coef() and get_vcov() give the coefficients alone, so "b2 = 0" names the coefficient it named before and joint = TRUE tests the coefficients and no group-level effect. A function given as hypothesis gets the model and keeps the effects in: hypothesis = function(x) avg_predictions(x) reads 0.143, as avg_predictions() alone does, where holding them fixed read 1.774.

  • vcov = a covariance over the outer parameters, such as vcov(fit, full = TRUE) or a cluster sandwich ~ g, is carried into the group-level effects, as the model’s own is. Only the part of their variance that comes from the outer parameters becomes robust; their conditional variance stays model-based. "HC0" and "HC1" are refused on a fit with a group-level term, as before. A matrix over the coefficients alone holds the effects at their modes, with a warning of class frmtmb_interop_se_part.

  • The warning of class frmtmb_interop_se_part names what no parameter holds: the draw of a new row’s me() value or latent residual (and, under cov = TRUE, the fitted latent values it continues from), and the group-level effects of a fit with quadrature = TRUE or importance =. A test of a coefficient no longer gives it. get_predict() gives it once per call, from predictions(), slopes() and comparisons() alike, and under the default vcov or a covariance the user gives, for rows at a grouping level the fit did not see and for an exact gp() at a position the fit did not see, where the kriging variance is more than 1e-4 of the field’s, and counts the rows of each kind.

  • The time of marginaleffects grows with the number of levels, one prediction per parameter: avg_slopes() on a poisson (1 + x | g) fit took 16.3 s at 500 levels of 40 rows, where it took 0.09 s with the effects held fixed. vcov = FALSE and re_formula = NA avoid it.

Residuals

  • BREAKING: residuals(type = "osa") reads each row against its family’s own CDF (osa_method = "cdf") on every core family that has one. Only an untruncated, unweighted gaussian() keeps "fullGaussian". With no group-level term a residual is now the randomized quantile residual of the fitted distribution: qnorm(F(y)) for a continuous response, and for a count qnorm(u) with u uniform on [F(y - 1), F(y)]. On one design per family, the count families matched it to 1.2e-12 or better and the continuous families to 3e-9 or better. The old default, oneStepGeneric, integrated a spline through a profile of the density, and the results were wrong with no warning:

    A residual is read in the tail its row lies in, so it holds to about +-37 and beyond: a poisson count of 60 about a mean of 3 reads 15.24 where TMB’s own formula read Inf.

    With a group-level term, TMB integrates each row by the Laplace approximation. Against the exact one-step predictive the median error was below 0.001 under (1 | g) and 0.006 to 0.016 under a latent ar() on poisson and the zero-inflated and hurdle counts. The largest error was 0.15, on the first row of a group. Over many rows the residuals stay calibrated against N(0, 1).

  • The uniforms of a count’s residual are TMB’s, set.seed(123) unless seed = is passed, so the same fit gives the same residuals.

  • residuals(type = "osa") hands the rows to oneStepPredict() in blocks of about sqrt(n), so that the objective carries the CDF of one block at a time. On (1 | g) fits of 500 and 2000 rows the residuals took 0.05 to 0.31 of the old route’s time on poisson, negbinomial and bernoulli: 59 s against 303 s on a poisson fit of 2000 rows, which took 34 minutes with every row at once.

  • residuals(type = "osa") works on beta_binomial() and on the zero-inflated and hurdle count families: zero_inflated_poisson(), zero_inflated_negbinomial(), zero_inflated_binomial(), zero_inflated_beta_binomial(), hurdle_poisson() and hurdle_negbinomial(). The "cdf" route gives the density the count as data, so a point mass at zero is read as it is in the fit.

  • residuals(type = "osa") on a trunc() count reads each row inside its own window, so bounds that change from row to row work.

  • residuals(type = "osa") on a truncated gaussian() takes the "cdf" route too. With a group-level term the old route left the later rows’ truncation terms in each row’s integral, and on a test of 96 rows the integral failed on every row, so the call stopped with “$ operator is invalid for atomic vectors”.

  • BREAKING: in residuals(type = "osa") a case weight is a frequency, as in the likelihood and in brms: a row of weight w gets the residual of the last of w copies, and later rows are conditioned on all w. A weighted poisson fit with (1 | g) gave the residuals of the fit with the rows written out to 8e-7. A weighted gaussian() now takes the "cdf" route, where it used to read a weight as a precision; with (1 | g) it was 0.0018 from the rows written out, which keep the exact "fullGaussian", and that is the Laplace error of the "cdf" route. A row of weight 0 gets NA, and so does a row of weight between 0 and 1, with a warning. Before, a weight reshaped a count’s own distribution: at weights of 3, 42 of 150 poisson rows fell outside their own step.

  • BREAKING: residuals(type = "osa", osa_method = "oneStepGeneric") on a count needs discreteSupport, the gaussian methods are refused on a count, and tweedie() is refused by name. Before, the first read NaN with no warning, the others gave a gaussian approximation of the count, and tweedie() read NaN. A residual that oneStepPredict() cannot form gives a warning. A fit with quadrature = TRUE is refused with the reason: its objective has integrated the random effects already.

  • bernoulli() and nbinom1() supply a CDF (cdf_log). Neither takes cens() or trunc(), which are still refused, now with “is not supported for this family”.

Performance

  • The standard errors of an exact gp() fit of a gaussian response come from the gradient of the model’s marginal, where that is the objective and its covariance is well conditioned. The gradient of the fit’s own objective solves for the field each time. At 500 positions frm() and summary() together took 34.2 s against 53.0 s, and the Hessian alone 2.1 s against 37.2 s; over three runs at 500 positions and at 2000 rows the Hessian’s time fell by a factor of 14 to 18. At 100 positions the factor is 3.5, and the fit as a whole does not change measurably. On 377 test fits the standard errors moved by at most 2.2e-8 of their value, and no standard error changed between a number and NaN. Where the noise is small against the field, the marginal loses digits, so these fits keep the old route.

Package code and continuous integration

  • The package code does not use the global assignment operator <<-. rOpenSci’s pkgcheck fails a package that does. A closure that keeps state between calls now writes to an environment that it creates, and a function that collects results returns them. The results do not change. The test test-no-superassign.R fails if <<- comes back in core. It also fails if <<- comes back in an extension, but only when the tests run from the source repository.

  • The test “new data are coded from the fitted minimum” compares a prediction on 4 rows with a prediction on 4 rows. On Ubuntu 26.04 with OpenBLAS 0.3.32, one value of a prediction on 3 rows was 1 ulp from the same value on 4 rows, and the test failed. The 3-row prediction must now agree to 64 ulps. A code from the minimum of the new data moves a value by a full category, so the test still finds that defect.

  • The test “the nl body reaches the likelihood” fits the negative binomial families and student() to data with a maximum inside the parameter space. On Poisson or normal data, the shape or nu goes to infinity, so the point where the optimizer stops, and its warning, changed with the platform. A gradient warning on Ubuntu was not in the allowed list.

  • Continuous integration: the check for a skip on a missing Suggests package runs also when the check or the coverage run fails, so one run shows all failures. The weekly ubuntu-next workflow has this check too, and has 150 minutes for its Stan tiers, which compile every program from an empty cache. Its record of the numerical libraries also tries to show the CPU, the OpenBLAS kernel and the OpenBLAS package. The brms-tiers job has 180 minutes and its identity tier 120, because an empty cache and a slow package mirror together can take more than 90.

Limits

  • The limit the last release named beside ar() on a step-shaped field is closed. The one on student() there is not: 11 and 6.07 nats short on two rows. The Student-t density is not log-concave in the field, so the inner solve can find more than one mode, and the log-likelihood at one point depends on where that solve starts: 207.37 from one inner start and 204.77 from another. A second start does not fix that. Two more rows stay short, 1.59 on Poisson and 1.45 on sigma ~ z, as they were.

  • A body that multiplies the field’s parameter is reached only in part. a * exp(b * z) with a ~ gp(x) stops 36.5 nats short on 1 of 12 test fits, and a * exp(b * z) with b ~ gp(x) 5.2 nats on 1 of 12, with return code 0 and no warning, as before. A start for theta is how to check such a fit.

  • A column far from zero against its spread still costs these fits, each with a warning. Beside a mo() term the fit reaches the maximum, but its curvature is not usable and its standard errors are NaN. A category-specific cs() term on it, or a random slope on it, (1 + x | g), stops short of the maximum. Center the column.

  • Under an ordinal family, whose thresholds take the intercept’s place, products and powers of columns shifted by 1e6 (w1 * w2, w1 + I(w1^2)) stop 8.6 to 28.6 log-likelihood units short of the model on the centered columns, each fit with a convergence warning. Center the columns.

  • quadrature = TRUE and importance = run their own objectives in natural coordinates: on bernoulli x + 1e6 a quadrature fit was 1.2e-5 short with no warning, and importance fits warned “false convergence (8)” at the maximum.

  • A nonlinear body that a bound holds runs in natural coordinates, with no warning.

  • A nonlinear body that is curved in a shifted covariate, b0 * exp(b1 * (z + m)), still stops short from m = 100, by up to 39.5 log-likelihood units and 8.7 standard errors, each fit with “false convergence (8)” and a gradient warning.

  • A nonlinear body with an alias inside it, such as b0 + b1 * x + (b2 + b3) * t, has a singular Jacobian and gets no second descent: on a shifted x two of three fits stopped 20 and 36 log-likelihood units short, with a gradient warning only.

  • The rounds of nested quadrature also run in natural coordinates. On a bernoulli (1 | school/class) fit with x + 1e4 or x + 1e6, the fit was 0.0007 to 0.039 below the centered fit and its slope up to 0.06 standard errors off, with a warning that the rule did not settle and one of false convergence. Center the column.

frmtmb 0.79.1

  • residuals(type = "osa") on beta_binomial() always stops with a message. RTMBdist 1.1.0 refused this route. RTMBdist 1.2.0 gives a value, and on a test of 150 rows 21 values were NaN with no warning. The refusal now does not change with the version of RTMBdist.

  • The test “a slope near separation is never silent at few draws” accepts a third result: the optimizer stops and warns. On 61 data seeds at 8 draws, 2 fits stopped with the refusal, 55 were capped and warned, and 4 stopped in the optimizer and warned. None was silent. The result differs by platform.

frmtmb 0.79.0

The main changes:

  • BREAKING: the exact gp() is non-centered, brms’s zgp, at a nugget of 1e-12 * sdgp^2, and starts at the maximum of the model’s marginal.
  • BREAKING: an autocorrelation term on a family with no residual y - mu is brms’s latent residual, as ar(cov = TRUE) and more.
  • BREAKING: a smooth’s standard deviation has brms’s class sds.
  • New: a nonlinear body under cumulative(), sratio(), cratio() and acat(), and on a Gamma() or inverse.gaussian() default link.
  • fitted() on a new row of me() or of a latent residual is the mean over that draw, and Est.Error holds the draw.
  • frm(importance =) reads the Laplace fit’s own scale, in fewer rounds.
  • Update the extensions together with frmtmb. frmtmb.sample 0.26.0, frmtmb.learn 0.7.4 and frmtmb.latent 0.7.2 need frmtmb 0.79.0.

Breaking changes

  • The exact gp() is non-centered: b of the block is z, a standard normal, brms’s zgp. The field is sdgp * L(lscale) * z, with L the Cholesky factor of the kernel plus a nugget of 1e-12 on the correlation. The field was centered at a nugget of 1e-6. Its white noise of variance 1e-6 * sdgp^2 came off sigma^2. It instead moved sdgp and the length scale when positions repeat, sigma is known or the family has none. Every fit that relied on it moves. On a curve x^2 over x from 0 to 10 with sigma 0.3, frm() read sigma 6e-5 and now reads 0.35, the marginal’s maximum. On sin(2 * x) with noise 1e-3 it read sigma 1.3e-7 and now reads 1.1e-3, 1.8 nats higher. At a known se(3e-4) it was 29 nats short. On exp(0.8 * x) with sigma 0.3 the fit was 72 nats short and now reaches the maximum, to 0.0005 and 0.0015 nats on two seeds. The objective rounds at 1e-3, and a warning says so.

  • The nugget is relative to sdgp. So the model of y * c is the model of y scaled. A fit of it is the fit of y scaled to the optimizer’s tolerance. This holds on 5 designs at noise 1e-3 and above, c from 1e-3 to 1e6. The log-likelihood is within 3e-7 nats, sigma within 3e-5, sdgp within 3e-4 and standard errors at new positions within 4e-5. At noise 1e-4, within 3e-4 nats, 4e-3 in sdgp and 3e-4 in the length scale. The same holds for x * c. brms’s nugget is an absolute 1e-12: at y * 1000 its kernel does not factor at the estimates and its chains do not mix (Rhat 3.77), and at y / 1000 it floors sigma. At sdgp near 1 the two are one number.

  • fit$estimates$b, fit$obj, the joint precision and start = list(b = ) read z in an exact gp() block, over a farthest-first order of the positions. ranef(), coef() and getME(fit, "b") still read the field. Read the field off those. The kernel is factored in that order because rounding moves the field least there: at 15 positions 6e-11, against 2e-5 over sorted positions. A fit reads the same in either order, and a chain at true sigma 1e-3 mixes better in this one.

  • An exact gp() starts where the model’s marginal, which integrates the field out, reads its maximum. The fit stays on the z tape: only where the descent begins changes. On a gaussian response with a constant sigma that is the model’s own marginal. It is read from a coarse grid over the length scale and the noise, on the distinct rows of the random design. It starts from the best cell of each of 6 length scales and the best 3 cells overall. Another family, a non-identity link, sigma ~, ar(), mi(), me(), mo() or several responses read the same marginal of the response on its link’s scale. This start is for sdgp and the length scale. A theta in start replaces this, and a beta or betad replaces only itself.

  • What that reaches, on rows no test had seen. A descent from the middle of the range stopped 417 nats short at sigma 1.19 with no warning. This was on a curve with a trend and a wiggle, 2 * sin(3 * x) + x, n 80, noise 1e-3. 0.78.0 reached it there and stopped short elsewhere. Gaussian, on 744 rows of fresh curves: 1 short by more than 1e-3 nats, worst 0.56, conv 0 and no warning. By comparison, 0.78.0 read 40 short and 62 short on the two tables of 192 it was read on. 0 + gp(x), start = list(beta = ), mi(), me() and mo() read 0 short on their row. Negative binomial, Gamma, lognormal, student, sigma ~ x, a log link, ar() and two responses read 0 of 48 short. Against this, it was 32 of 48 on 0.78.0, worst 272.

  • No start is certain: a marginal can have more than one maximum.

  • frm_curve_deriv() on a fit with an exact gp() takes a default step of 5e-4 of the grid’s range at order 2, where it was 1e-4. At the smaller nugget a second difference meets the rounding of the kriging row: at 1e-4 a standard error read up to 20 percent off.

  • class = "sds" is brms’s class for the standard deviation of a smooth’s penalized part, on frm() and on frm_sample(). coef names the term as written ("s(x)", "t2(x, z)", "s(x, by = f)"). A term with several standard deviations (a t2(), a by-smooth) takes the density on each of them. default_prior(), validate_prior() and prior_summary() list a class row with lb = 0 and one row per term, as brms does. Class "sd" no longer reaches a smooth, in brms’s words: class = "sd", group = "s(x)" and a class "sd" prior on a model with no random effect are refused, and sd beside (1 | g) leaves the smooth alone. brms’s log density and frm()’s objective are equal up to one offset, the same at every point tried. The offset is the brms normalizer of a half density and the map’s Jacobian at the first point.

  • On frm(), a missing response value that no density reads leaves the objective. It integrates to 1, so the fit is the complete-case one, exactly, and logLik(), AIC() and BIC() with it. brms’s posterior for every other parameter is that one too. The row stays in the frame and predicts; fit$estimates$miss has no entry for it. This changes every fit of a gaussian or student mi() response no predictor reads that worked before: a gaussian’s by nothing, a student’s by a Laplace error. The error moved nu by up to 33 standard errors on 200 rows with 60 missing and fitted a true nu = 6 as a gaussian. brms’s log_prob on the observed rows equals logLik() to 4e-13 on 13 families.

  • A gaussian response fitted with importance = now returns the exact optimum at any draw count, to about 1e-9 in log-likelihood. It used to move away from it by up to 0.09.

  • The stop that said “ROSE” and blamed an unidentified covariance is gone, because the cause was the frozen proposal: the design it named now settles on the exact maximum. A fit that does come out worse than the correction at the Laplace estimates now stops with a message. The message gives the worst group’s effective sample size. It also gives a larger draw count at which the same fit settled with every group above importance_ess, or names the counts it tried. It tries up to four doublings, up to 8192 draws and 2e6 rows times draws. This is because a tape holds about 1.1 KB per row and draw. So the stop costs more than the fit: 15 times the fit and 3.3 GB at its worst, measured on 244 rows from 512 draws. A larger count that cannot run becomes part of the message.

  • The warning that a correction “moved by the same amount” in every round is gone, with importance_rounds’s text about it. No fit reaches it now: a collapsed variance component settles where the Laplace fit put it.

  • beta_binomial() and zero_inflated_beta_binomial() have a new density form. A fit on data with no overdispersion changes. The old form added raw lgamma() values of the shapes, and those differences lose their digits as phi grows. The true density is flat there, the binomial’s, so the optimizer went on to where the rounding pointed. On one data set the true value is -548.98 at every large phi. The objective read -66752 at log phi 39 and +327680 at 45. frm() returned +94208 with convergence code 0. On another, -541.09 with nothing to mark it, 0.15 above the binomial limit. These fits now reach the binomial limit with code 0, on the five data sets of the test. A fit with overdispersion keeps its log likelihood to 11 digits. The censored and truncated forms use the same mass function. The time of a fit at 3e4 rows is 0.7 times the old time at 5 trials per row. The ratio is 0.9 at 10, 1.9 at 30 and 3.1 at 200.

  • nl = TRUE on cumulative(), sratio(), cratio(), acat() and hurdle_cumulative() now fits the body. Since nonlinear formulas reached the ordinal families, the rule that thresholds replace the intercept also took the intercept of a nonlinear parameter. So b1 ~ 1 left b1 = 0, and frm() and frm_sample() fitted the thresholds alone without a word: on a test data set the log likelihood was -267.12, the intercept-only fit. By contrast, it was -255.52 for the same predictor written linearly and for the maximum of brms 2.23.0. b1 ~ 1 + z lost its intercept in the same way. The log density now equals brms 2.23.0’s Stan program at three points, with and without thres() and disc. Also fitted(), predict(), simulate(), conditional_effects() and the prior table read the body and list the thresholds. On hurdle_cumulative() that holds without disc: with it, brms 2.23.0’s program stops on every top-category row, and frm() fits a model brms cannot.

  • A body that is constant across the rows at a stationary point is no longer returned as a fit. b1 * exp(b2 * x) at b1 = b2 = 0 is such a point on every ordinal family. a + b1 * b2 * x is such a point on any family. The optimizer stopped there with convergence code 0 and “the data do not determine them”, at the log likelihood of the model without the body: -413.56 on one data set, against -371.76 for brms and for a start. frm() now restarts from moved coefficients of every such body and keeps a higher point. A message gives both values and the optimizer’s return code. It looks at the kept fit again, so a second body does not stay there: a sigma body read 16.8 nats short and a mu2 body 65.7. If no restart leaves a body, a warning of class frmtmb_nl_saddle names it. The warning says that the data may simply have no effect there, which is correct for a slope of exactly 0. It also says that a start can test it. A fit from the default start that runs along a flat direction, as the standard errors report it, gets the same restarts: acat() on that data set stopped at -378.10, against -372.23. A point that is a true maximum along the body’s coefficients is left alone, with a message of class frmtmb_nl_local. Every fit that a restart does not beat comes back digit for digit. The restarts do not read the session’s random numbers. optCtrl = list(iter.max = 0) still reads the objective at the start.

  • A fit whose responses are all discrete and whose log likelihood is above 0 now warns, with class frmtmb_discrete_loglik. A probability mass is at most 1, so that value marks a density that lost its digits or a custom family that dropped a constant. The check is one-sided, and a value below 0 can also be wrong. It does not apply under a prior, where logLik() holds the log prior, or under REML.

  • An ordinal fit with a disc body no longer warns that the family’s post$fit_check hook failed. The check of an intercept in disc reads a design, which a body does not have. On a body, the standard errors report a constant term that the likelihood cannot place.

  • frm_compat() gives measured statuses for nl by family, in place of a rule that said refused for every family but gaussian. All core families are “works” except asym_laplace (“conditional”: the optimizer can stop at a kink of the density, on the body and on the linear spelling alike). A family no row measures, such as one from a package loaded later, is “untested”. tweedie and nbinom1 are “works” and say there is no brms reference. thres(), disc, equidistant, sum_to_zero, mixture and mvbf by nl are “works”, and cs_pred() by nl is “refused”. ar(), ma(), arma(), cosy(), unstr() and fcor() by nl are “conditional”, where they were “refused”: they work on gaussian and student, and a family whose term would be a latent residual stays refused.

  • frm_linpred(type = "link") with no dpar returns every predictor of the model as a matrix with one column per predictor, eta1, eta2, and so on. This matches posterior_linpred() of brms 2.23.0, which has one layer per predictor at each draw. An ordinal family with a cs() term has one per threshold, eta + cs_k. A family with several location predictors (categorical(), mixture(), the simplex families) has one per predictor. Before, the method returned the shared eta or the first predictor without a word, where fitted(scale = "linear") already matched brms. dpar = "mu" and dpar = "mu1" return one predictor. se.fit has the matrix’s shape.

  • frm_curve(), frm_curve_deriv(), frm_curve_feature() and frm_lp_basis() read the same layers. A curve holds every grid row once per predictor, with the predictor in a new column .eta. The rows are in the order as.numeric() of brms’s layers at one draw. The band and the covariance are over all of them. Under cs(), the standard error and the derivative read the term’s coefficients: on a sratio() fit the derivative in the cs() variable is bcs_z[k] with its own standard error. Before, it was 0 with a standard error of 0. frm_lp_basis() returns the stacked rows and a new field layers.

  • fitted(dpar = "mu", scale = "linear") of a cs() fit returns the shared part, as brms 2.23.0 does. Before, it returned one layer per threshold.

  • frm_linpred(dpar = "mu", type = "response"), fitted(dpar = "mu"), conditional_effects(dpar = "mu") and frm_curve(dpar = "mu", transform = TRUE) of an ordinal family apply the family’s link inverse, as posterior_epred(dpar = "mu") of brms 2.23.0 does: plogis(mu) under logit. Before, they returned mu unchanged, because the location’s own link slot is the identity. frmtmb.sample’s posterior_epred(dpar = "mu") and posterior_linpred(transform = TRUE) read the same.

  • emmeans() of a cs() fit with no dpar averages the thresholds’ predictors, and ~ x | rep.meas gives each, as brms does. Before, it read the shared part, which dpar = "mu" still reads. At the estimate of an acat() fit with cs(f) the means differed from brms’s by 0.083.

  • frm_curve() under a cov = FALSE autocorrelation term refuses with its reason, that the mean reads the residual of the row before it. Before, it refused with a message about a misread covariance.

  • A new row of a me() term has no latent value, and brms 2.23.0 draws one from normal(x, sdx). predict(), simulate() and conditional_effects(method = "predict") on new data do the same, per replicate, per row or per level under me(gr = ), also under propagate_error = FALSE. This is because the draw is measurement noise in the grid. Before, they read x, and the interval was too narrow: at a coefficient of 1.01 and sdx = 0.8, the predictive sd was 0.55 of brms’s. New data must hold sdx, with positive values and one value per me(gr = ) level. A level the fit did not see needs allow_new_levels = TRUE, as in brms. corme is not read, as in brms. A row with no x or sdx reads NA with a warning of class frmtmb_me_missing, as a plain predictor’s row reads NA. brms reads NaN and warns. So fitted(fit, newdata = ) on the fit’s own data answers when the fit dropped such rows.

  • fitted() on such a row is the mean of the expected response over the draw, by Gauss-Hermite on the response scale. Est.Error holds the draw’s variance beside the coefficients’. On a log link the mean is exp((b sdx)^2 / 2) times the value at x: brms’s mean is 1.41 to 1.76 times that value at sdx = 1.2, and now 0.98 to 1.02 times fitted(). On lognormal, zero-inflated and hurdle families, Est.Error was a constant in sdx and now reads 5.22 where brms’s sd is 5.21. frm_linpred() stays at the observed x, which on type = "link" is exactly the mean. Its se.fit, frm_curve(), conditional_effects() and emmeans() add the draw’s variance, so their standard errors are not coefficient uncertainty alone, as for a new grouping level. frm_lp_basis() reports it in extra_var, exactly on a product of calls. me(x, sx):me(z, sz) now holds (b sx sz)^2.

  • fitted()’s Q columns are the interval on the link’s scale, mapped through its inverse, so they stay in the support of the expected response: 0.51 and 17.3 on a poisson mean at sdx = 1.2, where brms’s quantiles are 0.50 and 17.7 and the Wald interval read -2.26 and 8.24. Before, the interval was symmetric about the estimate on every link. On an identity link it still is. On a me() row they are the draw’s quantiles. Over 200 simulated data sets at sdx = 1 they cover the mean at the drawn latent on 95.5 and 98 percent. By contrast, the old reading covered it on 88 and 91 percent.

  • One me() call is one latent value per row and draw, whichever response or dpar reads it. This diverges from brms 2.23.0, which draws one per predictor on new data, against its own Stan program, which has one Xme_1. At the training rows of a two-response fit, the residuals correlate at 0.734, one latent predicts 0.739 and brms’s replicates give -0.001. The 95 percent width of y1 - y2 at sdx = 2 is 3.18 with one latent and 15.08 with two. Marginals agree either way for two responses. On mu and sigma reading one call, the predictive skew reads 0.33, 0.36, 0.63, where brms reads 0.03, -0.02, -0.14. A mean that is a joint function of the predictors moves too: a categorical() probability reads 0.3336, where brms reads 0.3106. A mixture and a mu with a zi that read one call are the same.

  • frm_curve(contrast = ) reads one draw of a me() value for two rows at one x and one sdx, to all.equal()’s tolerance, so the draw cancels: a contrast in z has the standard error of z’s coefficient, 0.051. It answers for one unit with one unknown x. emmeans() contrasts answer for two new rows, each with its own measurement error, as brms’s draws do, so they carry both draws: 0.97 on that coefficient, against brms’s HPD width of 3.77. For the effect of z, read frm_curve(contrast = ) or the coefficient. For the effect of x, read frm_curve_deriv().

  • marginaleffects’s default vcov gives a warning of class frmtmb_interop_se_part where the standard error of a level from predictions() cannot hold a variance. This is because the delta method has only the coefficients in get_coef(). frm_linpred(se.fit = TRUE) and fitted() carry each of these:

    • a me() term: 0.07 against 1.11. A test of a coefficient is right, and so is a slope or contrast on an identity link, -0.491 with a standard error of 0.0485. On a log link a slope in another term reads 0.067 against 0.312.
    • a smooth, gp() or hsgp() term: 0.31 on every row of a gp(), against 0.087 to 0.695. On s(x) it reads 0.70, 0.12, 1.07, 1.59 against 0.088, 0.068, 0.55, 1.32.
    • group-level effects with re_formula keeping them: 0.209 against 0.104 on a gaussian (1 | g). Under re_formula = NA the two agree, and no warning comes. Over 168 fits on four families with (1 | g) or (1 + x | g), avg_predictions() was too wide by more than 25 percent on 77 percent of them. The median was 2.03 times. avg_slopes() was too wide on 52 percent, with median 1.30 times. A gaussian with no group-level slope reads 0.86 to 1.15.
  • fitted()’s Q columns of a category probability are a Wald interval on its logit, as conditional_effects() builds it. Before, they were a Wald interval on the probability, clamped to 0 and 1. On a new me() row they are the draw’s own quantiles. On a cumulative() row at sdx = 1.2, category 1 read 0 to 0.419 and now reads 0.048 to 0.464. Category 3 reads 0.121 to 0.251. For comparison, brms’s quantiles are 0.048 to 0.469 and 0.135 to 0.244.

  • fitted()’s Q columns on a mixture_mvn() fit are a Wald interval on each mean. Before, they were held between 0 and 1, as for a probability, so they could leave the estimate.

  • conditional_effects()’s se__ on a lognormal or hurdle fit moved from the response scale to the link scale, with no me() term too. This matches the help and a poisson fit. It diverges from brms 2.23.0, whose se__ is on the response scale: 0.068 against 0.519, as a poisson fit reads 0.102 against 0.809.

  • fitted(scale = "linear") of a distributional parameter with no predictor reads its link scale. This is not new, and it diverges from brms 2.23.0, which returns the stored natural value: 2.01 against 7.48 with shape unmodeled, and 2.01 in both with shape ~ 1, the same model. On another fit, 0.776 against 2.17.

  • frm_curve_feature() with no resp on a multivariate fit asks for resp. Before, it stopped with an internal error. insight::get_predicted() on a multivariate fit returns Row, Response, the columns of data and Predicted, as for brms. Before, it returned one unlabeled number per row and response.

  • frm_linpred() with no resp on a multivariate fit returns every response, one column each, as posterior_linpred() stacks them. frm_lp_basis() and frm_curve() refuse it and ask for resp. Before, the three read the first response without a word.

  • A univariate fit does not read resp, as in brms: fitted(), predict(), residuals(), frm_linpred(), frm_lp_basis(), frm_curve(), frm_curve_deriv(), frm_curve_feature(), emmeans(), get_predict(), conditional_effects() and pp_mixture() give the same answer for an empty vector. They give the same answer for a name given twice and an unknown name. Before, the first two gave internal errors and the third a refusal.

  • conditional_effects() of a multivariate fit gives one panel set per response without resp, and takes several, each named <resp>.<resp>_<effect>, in brms’s order: the effects outside, over what any response reads, and the responses inside, so a response that does not read an effect has a flat panel. A panel of one named response has that name too. Before, it gave the first response alone, named <effect>, and failed on several. A response name given twice is refused here and in pp_mixture(), where brms repeats the panel. This is because brms answers that input four different ways across its methods.

  • conditional_effects() of a me(gr = ) fit holds the group at NA, as brms does, so each grid row is a level of its own. frm_curve_deriv() and frm_curve_feature() in that noisy variable refuse with the reason, that a level holds one value.

  • predict(newdata = ) answers under ar(), ma(), arma(), cosy() and unstr() with cov = TRUE, through the draw simulate(newdata = ) takes, as brms does. Before, it refused.

  • conditional_effects() on a nonlinear body whose parameter reads me() now stops, with “frm_lp_basis(): the nonlinear body reaches ‘a’”. It answered on 0.78.0. A gp() in such a parameter stops on both, and a smooth answers on both.

New features

  • gp_nugget_message(), in the sampling API: the new warning’s text, or NULL.

  • gp_tape_scope() and gp_tape_order(), in the sampling API: the tape settings and the parameter order a frame with an exact gp() needs while its tape is built and read.

  • mm() takes an |ID| key, (x | p | mm(g1, g2)) or mm(g1, g2, id = "p"). Terms with one key over the same members merge into one block over the pooled levels, in any predictor or response. They take one set of weights, compared by value, so weights = W beside weights = cbind(w1, w2) of the same numbers fits: brms reads the first term’s for every term, so different weights are refused rather than picked. mm(g1, g2, cov = A) is a known covariance over the pooled levels, with several columns too, and (1 + x || mm(g1, g2, cov = A)) is one block per column, as over gr(cov = A). brms’s log_prob equals frmtmb’s joint density to 4e-9 and 6e-14.

  • gr(cov = A) and mm(cov = A) refuse a matrix that is not symmetric or not positive definite, in brms’s words. A wrong triangle changed the fit by which one the density read, and a matrix that is not positive definite stopped on a NaN gradient.

  • mi() on a response takes every family brms takes it for: skew_normal, exgaussian, gen_extreme_value, von_mises, beta, Gamma, weibull, exponential, frechet, inverse.gaussian, lognormal, asym_laplace, and a mixture(), beside gaussian and student. Every other family is refused in brms’s words, and a response with no observed value by name.

  • A missing value that a density in the objective reads, a predictor’s mi() or mi(sd), stays latent on frm(). A Laplace approximation integrates it out on the chart that holds it in the family’s support: log on the half line, logit on the unit interval, brms’s window (-pi, pi) for von_mises. A reader whose own density has left does not read: z | mi() ~ mi(y) with z missing where y is takes both rows out, along a chain and a fork. A reader’s weight of 0 does not read either. Each of these equals deleting the rows by hand, to 5e-9. A reader’s fitted() at such a row reads the value’s mean. brms’s log_prob equals the joint density to at most 2.3e-8.

  • frm() warns, with class frmtmb_mi_laplace_error, when that approximation moves an estimate by 0.5 standard errors or more. Three inner Hessians over every read value give Tierney-Kadane’s term, a gate that costs 0.1 percent of a fit. At a term of at most 1 nothing else runs. Past the gate, integrate() gives the move. It runs over 10 read values at the estimate and at one standard error above it, per parameter of the read response. Over 144 fits against an exact maximum it warned on 75 of 76 shifts of 1 standard error or more. It warned on 3 of 39 below 0.5. The largest silent shift was 1.01. The warning names the parameter, the size and the remedy: frm_sample(), or deleting the rows if the reader carries little about the value. frmtmb_control(check_mi = ) sets what the check does. On a gaussian value that only gaussian means read, the approximation is exact and the check does not run. Past the gate the check costs about 37 percent of the time of frm() on such a fit. The approximation can move an estimate. The largest shift from an exact maximum, in its standard errors, on 200 rows: 2.74 exgaussian with 60 read by a slope of 0.05, 2.14 student with the same, 0.98 beta with 100 read by a slope of 0.7. 0.96 Gamma with the same, 0.17 exponential with the same, 0.14 exgaussian with 60 read by a slope of 0.7, 0 gaussian. A reader that carries little leaves the row nearly unread, with an unread row’s error: exgaussian‘s parameters sit 2.51 to 3.36 standard errors from the observed rows’ at slopes up to 0.2 with 60 read, and 0.47 at 0.7. Per value, against integrate(), on 80 rows with 8 missing and a gaussian reader: 0 gaussian, -0.114 Gamma, -0.098 exponential, -0.078 weibull, +0.008 frechet, -0.125 beta, -0.079 exgaussian, -0.076 von_mises, -0.037 lognormal, -0.023 skew_normal, +0.021 inverse.gaussian, +0.008 gen_extreme_value. von_mises loses -1 to -4.2 per value at a mean near the window’s end, where the value has two modes. Estimates are off by up to 17 standard errors there. So frm() warns by name, with no integral, when 10 percent or more of read rows have pi - abs(mu) < 2 / sqrt(kappa). At mean 0 it loses -0.068 per value. Its largest shift over 13 replicates at a mean near 0 is 0.74. frm_sample() has none of this. asym_laplace and a mixture() are refused for such a value on frm(): the first has no curvature at its kink, and the second has one mode per component. frm_sample() samples both.

  • frmtmb_family(mi_chart = ) declares the chart of a family’s missing value ("log", "logit", "circle", "identity", "no_mode"). A family with a bounded support must set one.

  • se() on skew_normal, and mi() with se() on one response, as brms writes them. brms’s log_prob equals the joint density to 1e-12.

  • cens() on a mixture() whose components have a CDF is the mixture of the components’ tails, as brms writes it. brms’s log_prob equals the density to 6e-14. A group-level mixture(groups = ) refuses it.

  • gr(g, id = "p", cor = FALSE) is accepted when every term under the key says cor = FALSE: nothing is correlated, so the key does nothing, as in brms. Terms under one key that disagree are refused in both orders, over gr() and over mm(): brms reads the first term’s flag, so swapping them changes its model. id = "p" with cor = FALSE over another group is refused in brms’s words.

  • importance and vcov(cluster = ) run on a fit whose missing rows have all left the objective, and read the observed rows. A value a density reads is still refused: it has no group and no cluster.

  • These stay refused where brms 2.23.0 builds the model, for these reasons. mi() with cens() when a value is latent: brms’s program takes the density at the data, where a missing value is the placeholder Inf. It never reads the latent value, so there is no model to match. With nothing missing mi() takes cens() and trunc(). trunc() on a mixture(): brms truncates each component and then mixes them, and a CDF of the mixture would be a different model. gr(g, by = , cov = ) and mm(by = , cov = ): brms scales each level by the covariance of its by-level and then correlates the levels through A. This is neither block-diagonal over the by-levels nor a Kronecker product, so frmtmb has no block density for it. gr(g, cov = , dist = "student"), mm(dist = "student"): the t’s mixing variable is per level. With levels correlated through A, or a row that loads several levels, the joint density over the field is not a multivariate t. brms samples the construction; the Laplace route has no density to expand.

  • frm_compat() rows, each checked by running both packages: cens() x disc, cens() x equidistant, vint() x sum_to_zero and trials() x cs_pred() are refused, in both packages. s() x prior, t2() x prior, mm() x |ID|, mm() x gr_cov, |ID| x mmc() and gr_cov x mmc() work. mi() x se(), se() x skew_normal, cens() x mixture, thres() x mvbf and mi() x each family above work. mi() x asym_laplace, mi() x mixture, mi() x cens() and mi() x trunc() are conditional on no density reading the value. importance x mi() and quadrature x mi() are conditional in the same way. trunc() x mixture is refused.

  • BREAKING. ar(), ma(), arma(), cosy() and unstr() on a family with no residual y - mu are no longer refused. They fit brms 2.23.0’s latent residual: one latent per row on the linear predictor of mu, with a new parameter sderr and prior class "sderr". The joint density at the conditional modes is an identity with brms’s log_prob at three parameter points, within 4e-10. This holds on poisson, negbinomial, zero-inflated and hurdle poisson, bernoulli, binomial, Gamma, lognormal, beta and cumulative. The Laplace value is within 2e-7 of one computed from brms’s own program. frmtmb integrates the latents by Laplace on sparse coordinates: at 2000 rows in one series, the inner Hessian of ar(cov = TRUE) stores about 4000 entries, where brms’s coordinates would store 2 million.

  • ar() with cov = FALSE on these families is brms’s moving average of independent latents, with each coefficient in (-1, 1). Its sum enters on the link scale where brms has a built-in density on that scale. Elsewhere it enters after the inverse link, as in brms’s program. A mean outside the family’s support has a NaN density, as Stan rejects it.

  • frm() warns, with class frmtmb_latres_laplace, where the Laplace approximation of a latent residual is biased: ordinal rows, or a first-order Laplace correction that moves log sderr or ar[1] by more than 0.15, or cannot be read. Over 84 designs and 3130 fits it warned on 0.865 of the wrong fits and on 0.157 of the right ones. The rate was 0.14 to 0.17 on each set of designs. Above 3000 rows it reads the rows’ share of the latent, as the correction’s dense matrix is the cost that limit avoids. On bernoulli, 20 groups of 10, sderr reads 0.39 and ar 0.77 where the exact maximum likelihood is 1.10 and truth is 0.60. On beta_binomial() it reads 0.26 against 0.65, on zero_one_inflated_beta() 0.28 against 0.59, on Beta() at phi 4 0.26 against 0.42. On negbinomial it is low by 8 to 15 percent, with coverage of log sderr down to 0.66. The fit otherwise reports code 0. Poisson is fine down to a mean of 1, and lognormal does not warn: there the log density is quadratic in the predictor, and Laplace is exact. The condition has no units. The warning is about the estimates, so check_se does not reach it, and suppressWarnings(expr, classes = "frmtmb_latres_laplace") silences it. frm_sample() samples the same model and agrees with brms. ?frmtmb-autocor has the table.

  • BREAKING for a call that fit in the previous development build. frm() refuses cov = FALSE where the AR part adds after the inverse link. This covers Gamma, Beta, Weibull, exponential, inverse Gaussian, beta_binomial(), zero_one_inflated_beta(), any non-canonical link, a cens() or trunc() response. frm_sample() fits it. A latent can carry the mean out of its range, and no number from the fit says whether Laplace reads the likelihood: on benign designs 54 of 120 fits read true. Beta at phi 50 read true at a margin of 6.6. Gamma with a log link at 23.6 read ar -0.86 for 0.47. At low signal, beta_binomial() at 10 trials stops at a nonzero code on 20 of 20 with a gap of 9.0 nats. At low signal, zero_one_inflated_beta() reads sderr 0.28 for 0.38 with code 0. One Gamma fit reported a log likelihood of +3.39e18, and brms’s own chains struggle on these forms, with up to 3507 of 4000 divergent.

  • On a link that does not hold the mean (identity, inverse, 1/mu^2), under cov = TRUE, frm() refuses after the fit. It refuses if the mean stands less than 2 marginal sds of the latent from the link’s edge. There 14 of 150 fits read true against the exact marginal, some inner starts read NaN and coverage of log sderr is near 0. From 2 up there is no NaN, and 0.56 of 530 fits read true, in line with links that hold the mean. Gamma() at shape 50 reads sderr 0.309 against an exact 0.312, and poisson("identity") at a mean of 50 reads 3.79 against 3.73.

  • A dispersion the rows do not ask for no longer reaches an unbounded objective beside a latent residual. Before, a log likelihood read +6.5e17 on beta_binomial(), +1.9e30 on Beta() and +4.1e7 on zero_one_inflated_beta(). A constant dispersion is held at a log of 15, and a dispersion with a predictor reads NaN above a log of 25. A fit at the box warns with class frmtmb_latres_dispersion, naming the term.

  • frm() refuses mi() on the response beside a latent residual: the imputed values and the latents would share one inner problem, which no measurement has read. frm_sample() is not refused by this check.

  • frm() refuses ar(), ma(), arma() and cosy() under cov = TRUE if every group has one row, where brms builds the model. The correlation is not seen in the data, and only its product with sderr enters.

  • frm() refuses asym_laplace() with a latent residual, where brms builds the model. The density has a kink in its mean, so the Laplace approximation has no maximum. frm_sample() samples the model.

  • A fit at a limit of a latent residual says so: a message of class frmtmb_boundary_fit that names the limit, NA in place of a standard error of 0 in summary() and confint_varcorr(). It also gives a row in diagnose(), and brms’s names in place of theta_k. A limit is read from the fit’s numbers: ar[1] at 0.9992 on 2000 rows is well determined and reports its standard error. At ar[1] = 1 with sderr = 0 the message says the data read as cosy() or (1 | group); at ar[1] = -1 it says one size per group, alternating by row.

  • predict() holds sderr and the term’s coefficients at their estimates when it draws the parameters of a latent residual. A draw of log(sderr) with a standard error of 10 multiplied the latents by exp(30). This gave 3.7e304 on a poisson fit whose largest count is 10.

  • brms’s refusals hold on these families, in brms’s words: cov = TRUE above order one, an MA part under cov = FALSE, fcor() and a nonlinear mu. A term on a multi-category family or a mixture says “Explicit covariance terms can only be specified on ‘mu’”.

  • The methods read the latent residual as brms reads err: fitted(), frm_linpred(), residuals(), predict() and simulate() on the fitted rows, under re_formula = NA too. On new rows under cov = TRUE, predict() and simulate() draw new latents; under cov = FALSE, new rows are refused in brms’s words. fitted() on new rows is the mean over the latent at each row’s marginal sd, as brms’s posterior_epred() is, and marginaleffects reads the same mean. frm_linpred() on new rows stays at a latent of 0. conditional_effects() and emmeans() drop the term, as brms’s do.

  • Above 3000 rows fitted(), residuals() and frm_linpred(se.fit = TRUE) with newdata = NULL on a latent residual give NA for the standard error, with a message of class frmtmb_latres_se_skipped. This is because it would solve the joint precision as a dense matrix: 6 to 12 s at 2000 rows, 80 s at 4000, 984 s at 8000. options(frmtmb.latres_se_rows = ) moves the limit.

  • A decision, not a defect. Under cov = FALSE frmtmb’s fitted(), log_lik() and row densities follow the model. In contrast, brms 2.23.0’s posterior_epred() and log_lik() leave out the AR sum of its own Stan program. Summed over the rows, brms’s log_lik() misses its log_prob by -6.78 nats on poisson and -113.48 on Gamma with a log link. This is at ar 0.6 and sderr 0.5. The means differ by up to factors of 1.62 and 5.18. frmtmb’s row densities add up to log_prob, so the two packages differ on this form.

  • default_prior(), prior_summary(), variables(), summary() and confint_varcorr() carry brms’s names and bounds on these terms: ar[1], ma[1], cosy, cortime__<t1>__<t2> and sderr.

  • bf(..., nl = TRUE, autocor = ~ term) and + acformula(~ term) fit a nonlinear model with a residual correlation term, as brms does. The log density equals brms 2.23.0’s Stan program for ar(), ma() and arma() with cov TRUE and FALSE, cosy(), unstr() and fcor(), on both routes. A term written in the body is still refused, and the message now says to use autocor =. bf(y ~ 1, autocor = ~ term) + nlf(mu ~ ...) and bf(y ~ 1) + nlf(mu ~ ...) + acformula(~ term) take the term too.

  • Under cov = FALSE, fitted(), predict() and logLik() of a body follow the model, on the one-step mean, as they do for a linear mu. This diverges from brms 2.23.0 on purpose: brms’s Stan program has the ARMA term. By contrast, its posterior_epred() is the body alone. Its log_lik() is the density at an ARMA coefficient of 0, 34.9 nats from its own log_prob() on one model. brms’s post-processing for a nonlinear formula does not read the term.

  • nl = TRUE on categorical(), multinomial(), dirichlet(), dirichlet2(), logistic_normal(), dirichlet_multinomial() and a mixture() writes the one body into every location. It has one set of nonlinear parameters, as brms does. It was refused. The log density equals brms 2.23.0’s Stan program. fitted() works on these fits and on a body under an ordinal mixture; both were refused.

  • A nonlinear body can call the functions of the Stan language that brms’s bodies call, which brms’s nonlinear vignette does: inv_logit(), logit(), inv_cloglog(), Phi(), square(), cbrt(), exp2(), pow(), inv(), inv_sqrt(), inv_square(), hypot(), log1m(), step(), log1p_exp(), log1m_exp(), log_diff_exp(), log_sum_exp(), log_inv_logit() and log1m_inv_logit(). They have Stan’s values and derivatives, on the tape and off it. A function of the same name found from the formula’s environment comes first, which includes the packages on the search path, except for stats::step().

  • x^b and pow(x, b) in a body, with a data x that is 0 on a row, have Stan’s finite gradient. a * x^b stopped with “NA/NaN gradient evaluation” there, where brms fits it.

  • A nonlinear fit under a link that cannot evaluate a body at 0 now starts. frm(bf(y ~ b0 + b1 * x, b0 + b1 ~ 1, nl = TRUE), family = Gamma()) and the same under inverse.gaussian() stopped with “failed from its default starting values”. frm() now moves one intercept the body reads to where the body equals the response mean, and says so in a message. A body undefined at its start, such as log(a) + b1 * x under Gamma(), keeps the error that asks for a start. A start or a located prior stands. mixture(Gamma(), Gamma()) moves the same way.

  • pp_mixture() takes one response of a multivariate mixture, with brms’s refusal of none or several. Before, it refused every multivariate fit.

  • insight::get_predicted(predict = "link") reads one predictor per row, the shared part of a cs() term, because it returns one number per row.

  • emmeans() of a model with a me() term no longer leaves the term out of the mean. Its design route read the intercept with no b * mean(x). It now takes the grid route, as mo() and mi() do.

  • frm_curve() no longer refuses a grid with a row that has a standard error of exactly 0 on both routes of its covariance check. An example is x = 0 under dpar = "mu" of an ordinal fit. It read the agreement as a disagreement of 1.

  • New sampling-API exports for frmtmb.sample: ce_by_response(), resp_refuse_single() and update_merge_prior().

  • frm_compat(): cs_pred() x frm_curve is now “works”, where it was “untested”; me() x predict and me() x emmeans are now “works”, where they were “conditional”.

  • ?"frmtmb-gp" is a new help topic. It states the nugget, the warning of class frmtmb_gp_nugget, what to do about it, and the order of the field.

  • frmtmb_control(check_mi = c("warning", "ignore", "stop")) sets what frm() does when the Laplace approximation of a read mi() value moves an estimate.

  • options(frmtmb.latres_dense_rows = ) sets the row limit above which a standard error refuses to read the whole joint covariance of a latent residual. The limit is 20000 rows. The size is 16 n^2 bytes: about 200 GB at 100000 rows, where conditional_effects() used 3 GB at 12000 rows.

Bug fixes

  • frm(importance =) no longer stops with “did not converge … the corrected negative log-likelihood ROSE” on the models it exists for. It stopped on 162 of 648 fits over six families, three block shapes and three group sizes at 64 to 2000 draws. Of the others, 204 used every round. Now all 648 fit, and none uses every round. The fit used to freeze the proposal in place while the optimizer moved. A step in an intercept then moved the integrand under draws that stayed put, and the curvature the optimizer saw was a sample statistic: on a binomial y | trials(10) ~ x + (1 | g) of 10 groups of 20, 5 to 70 over seeds at 200 draws, and negative at one, against an exact 34. Where it came out too small, the rounds diverged, with effective sample sizes of 0.998. The proposal’s mode and variance now follow the optimizer to first order around the anchor, in steps measured in units of the predictor. So a fit does not read a column’s center or scale. The standard normal draws stay fixed, so the objective stays smooth, and at a given anchor each parameter value has an unbiased estimate. On that model at 200 draws, one round gives an intercept of 0.036581 and a log-likelihood of -418.1106 (Monte Carlo standard error 0.0049). For comparison, quadrature gives 0.036585 and -418.1121. Laplace gives -418.1233. Over the 648 fits the median round count falls from 4 to 1. The total time is about a third less where the old fit took 3 to 5 rounds. The 162 fits that used to stop now run to the end. The median error against the exact maximum falls from 0.0041 to 0.0002 in the fixed effects. It falls from 0.017 to 0.001 in the log standard deviations. Where both take 2 rounds, a correlated block costs more per evaluation: on 5000 groups, a gradient up to 1.8 times at 3 coefficients and 1.25 times at 2, a tape up to 2 times. A freeze at 40 fixed effects costs 3.6 to 5.6 times, and a whole fit there up to 15 percent more.

  • confint(method = "profile") on an importance = fit gets bounds close to quadrature’s on designs where it used to warn that the proposal had stopped covering the integrand. On a scalar intercept over 40 groups of 4 binary rows at 500 draws, the bounds on the log standard deviation are -0.5115 and 0.7558. By comparison, quadrature’s are -0.5146 and 0.7576. There is no warning.

  • frm(importance =) starts its rounds from where the Laplace route ends. It skipped the restart from a larger standard deviation, so a false collapse of a variance component stayed: every weight is equal there, so nothing left it. On an uncentered ordinal, w = 5 z + 2000, 15 of 40 fits stayed at a log standard deviation of -6.7 to -130. They read 0.15 to 3.33 nats below the Laplace fit.

  • frm(importance =) fits a response in large units. On y x 1e6 a gaussian fit stopped on 6 of 8 data sets with “Report this model”. This is because the prior’s precision was read as a difference that rounded: 0.29 wrong at a standard deviation of 1e7, and 0 at 1e8. It now reads the precision where the density has moved by about one nat.

  • frm(importance =) honors a prior. It used to stop on any prior with “could not reproduce this model’s joint log-density … Report this model”, though frm_compat("importance", "prior") said it works. The prior reads the outer parameters only, so the objective adds it. Against quadrature = TRUE with the same prior, on a binomial (1 | g), the objective agrees within its Monte Carlo error. The estimates agree within 0.05 of their standard errors.

  • A constant distributional parameter, as in bf(y ~ x + (1 | g), nu = 5) with importance =, evaluated the wrong model on the path that reads the weights. It read nu from sigma’s value, so the effective sample sizes and the Monte Carlo standard error described that other model. They read 0.50 and 0.102 where the right values are 0.98 and 0.015. The fit’s own objective was right. The defect dates from importance = itself, in 0.50.0.

  • The numbers that vignette("reinforcement-learning") and vignette("bayesian-cognitive-modeling") quote for importance = are measured again. They came from the frozen proposal. On Exams_2 the Laplace error is 0.0002 nats, not 0.013, and direct numerical integration confirms it. On the 40-subject learning model at 100 trials the correction moves log sd(alpha) by +0.12, not +0.36, and all six datasets complete.

  • frm() refuses a nonlinear body the likelihood does not read: a family whose density does not read the dpar the body computes, or a parameter whose formula has terms and whose design came out empty. Both fitted without the body before. The refusal needs the body’s value to have moved while the objective did not, so a flat or saturated start does not reach it.

  • cs() in a nonlinear body is refused with a sentence that says why. It was R’s “could not find function”. brms 2.23.0’s compile fails on it. A helper called cs() that the formula’s environment finds is the user’s, and it is called.

  • conditional_effects() on an ordinal body draws its Wald band through the body’s Jacobian, and band = "boot" works. Both stopped before.

  • frm_linpred() and fitted() on a nonlinear body under na.exclude read the kept rows of a nonlinear parameter. Before, they mixed padded rows into the body’s data columns.

  • gp(x, k = ) has no nugget, but it is not a remedy by itself: k must be large enough for the field. On sin(8 * x) over 0 to 6 at noise 1e-3, k = 30 stops at frequency 6.5, reads sigma 720 times high and reports convergence, with no warning. k = 60 reads brms’s sigma.

  • A gap of 94 to 1127 nats between frm() and brms on an exact gp(), read earlier, was in the test harness: frmtmb sorts a term’s positions and brms keeps first appearance, and the translator did not reorder them.

  • fitted() on a new row of a latent residual holds the latent’s draw in Est.Error. The Q columns are read from that draw, as on a me() row. On poisson Est.Error read 0.133 where the draws’ standard deviation is 1.005. A row with both a me() value and a latent residual reads one mean and one variance over the two. Beside another group-level block it reads the joint covariance, up to frmtmb.latres_dense_rows rows: 14 seconds and 6.4 GB at 20000 rows. Above that its standard errors are NA, with a message of class frmtmb_latres_se_skipped.

  • (1 | p | gr(g, cor = FALSE)) beside (1 | p | g) builds two uncorrelated blocks, as brms does, in either order. frm() refused it. A key written id = is still refused, since brms reads the first term’s flag there.

  • frm_curve() at its default reads a row of NA as NA, and the other rows answer. Before, it stopped with “infinite or missing values in ‘x’”.

Limits

  • Off the gaussian the start reads a working response, and it is not certain: on 150 fresh rows 13 stopped short by more than 1e-3 nats with no warning, against 51 on 0.78.0 (worst 420). Most are under 2 nats at low information, on bernoulli, Poisson and binomial, and some of those 0.78.0 reached. On a step-shaped field the gaps are larger: beside ar() 26 nats where 0.78.0 reached the maximum, with student() 11 and 6 where 0.78.0 was 1.6 and 2.8 short. A nonlinear body is 98.5 short on both builds on a trend plus a wiggle. A start for theta is how to check a fit that matters.

  • sigma near 1e-6 * sdgp, where a nugget of 1e-12 reaches the noise, is the new limit, three orders below the old one. There sigma can read low. On 200 rows the fit’s sigma was under half the true value on 92. A new warning of class frmtmb_gp_nugget names this: it fired on all 92, on every row of sigma under 1e-6 * sdgp, and on none above 3e-6 * sdgp. On 3 of 79 and 10 of 67 rows it fired with sigma within 0.8 to 1.25 of the truth. There the noise is at the nugget. With se(), or where positions repeat, sdgp and the length scale can read low. On a non-gaussian family it did not fire on 18 rows, up to log mean 16 and 1e7 trials.

  • Below about 1e-4 * sdgp nlminb also stops at false convergence, more often now since it starts at the maximum. fit$opt$convergence and diagnose() keep the code. The warning stays quiet only when a Newton step gains nothing and the stop reads the marginal’s own maximum to grad_tol. Only the warm start of a gaussian field can say so: on 144 rows it quieted 81 of 96, of which 3 were short, worst 0.81, at the marginal’s own second maximum. On 160 rows with a strong trend it quieted 37 of 66, of which 2 were short, worst 1.44. Every other stop still warns.

  • On noise-free sin(2 * x), 60 rows, the fit is 0.003 nats short of the marginal’s maximum, with its warnings. The fitted means at observed positions hold, and at new positions they are 2.6e-7 off. Standard errors at new positions read 1.1 to 2.4 times the plug-in standard error at the maximum.

Costs

  • A gaussian fit with an exact gp() takes longer than the centered one did. frm() took 0.17, 2.1 to 2.2, 28 and 18 seconds. This is from y ~ gp(x) on 60, 200 and 500 distinct positions, and on 1000 rows at 500 positions. Each ran one at a time on an unloaded machine. Before, it took 0.06, 0.64, 7.4 and 4.8: about 3, 3.4, 3.8 and 3.8 times. About half of that at 500 positions is the start, 13 of 28 seconds and 9 of 18, which saves the descent. The rest is the tape’s build, inner solve, Hessian and standard-error check. The inner Hessian is I + sd^2 L' W L, dense in the positions, and TMB’s tape reads it column by column, so the gradient grows faster than the value. Peak memory for frm() and summary() went down, from 0.89 to 0.73 GB at 500 positions. A saved fit grows, from 25 to 36 MB there.

  • On a fit with an exact gp(), the Hessian’s steps for coefficients are in y’s unit. A standard error’s row under its noise floor is flat only if it reads no curvature along its own parameter at two steps. An (Intercept) at y * 1000 read flat and took every fitted standard error with it. Other fits keep their Hessian and their verdicts bit for bit.

frmtmb 0.78.0

The main changes:

  • BREAKING: brms’s prior classes meanme, sdme and corme of a me() term are honored, as in brms 2.23.0. A brms prior table passed to frm() now applies its corme row.
  • BREAKING: in a default_prior(route = "sample") table, a group row prints brms’s (flat), so an edited class row reaches the groups.
  • New sampling-API export predict_rescor_draw(), the joint predictive draw of a set_rescor(TRUE) model, and a resp argument to rescor_row_loglik().
  • Update the extensions together with frmtmb. frmtmb.sample 0.25.0 needs frmtmb 0.78.0.

Breaking changes

  • brms’s prior classes meanme, sdme and corme of a me() term are honored, with brms 2.23.0’s meaning. Before, set_prior() refused the three classes by name, and a brms prior table lost its corme row, which is brms’s lkj(1). Now that row applies. So a brms get_prior() table passed to frm() as prior = gives another MAP fit on a model with correlated me() terms. The frm_compat() row me() x prior is now “works”, where it was “conditional”.

  • In a default_prior(route = "sample") table, a group row reads (flat), as brms prints its "" there: the sd and cor rows with a group, the new corme rows with a group, and a level of thres(gr = ). Before, these rows printed the class row’s default. Passed back as prior =, that printed default applied as an explicit prior, so an edited class row did not reach the groups: a class corme row edited to lkj(4) sampled lkj(1), with a Kolmogorov-Smirnov p-value of 1.3e-32 against the lkj(4) correlation. Now the edit reaches every group, as in brms. The class rows and the coefficient rows do not change.

New features

  • set_prior(class = "meanme") is a density on the latent mean of a me() term. class = "sdme" is a density on the latent standard deviation itself, with the log-Jacobian of the log map, as for class "sd". coef names one me() variable ("mex" for me(x, sdx)). class = "corme" takes lkj(eta) on the correlation matrix of the terms that share a gr =, and group selects that gr = variable. As in brms, the three classes take no resp, dpar or nlpar. meanme and sdme take no group, and corme takes no coef and no bound. A model without me() terms refuses them.

  • default_prior(), validate_prior() and prior_summary() list brms’s rows for these classes. meanme and sdme have a class row and one row per me() variable, and sdme is bounded below by 0. corme has one row, plus one per gr = variable of a correlated set. On five model shapes the rows and bounds are those of brms 2.23.0’s default_prior().

  • default_prior() and validate_prior() answer a me(x) written without its measurement SD, as brms’s default_prior() does. frm() refuses it, as brms refuses it when it reads the data. Before, both tables refused it.

  • With a proper prior on every class, the posterior density of a me() model equals brms 2.23.0’s Stan log_prob up to two terms. These are the Jacobian of the map between the two parameterizations and brms’s log(2) normalization of each half-Student-t. We compared three models at four points each: one me() term, two terms with gr = and a corme prior, and a term beside (1 | h). The constant agrees to 6.4e-12 relative and the gradient to 4.3e-10 relative.

  • predict_rescor_draw() is exported. It is the joint predictive draw of the responses of a set_rescor(TRUE) model, which predict() makes. frmtmb.sample’s posterior_predict() now draws through it, so the two cannot drift apart.

  • rescor_row_loglik() takes resp, the responses whose joint density to give, as brms’s log_lik(resp =) asks. The correlation matrix follows the order of resp.

frmtmb 0.77.0

The main changes:

  • BREAKING: under the covariance form of ar(), ma() and arma(), sigma is the innovation sd, as in brms 2.23.0. A fit saved by 0.76.0 or earlier with such a term is refused where an answer reads sigma.
  • BREAKING: each random-effect parameter and the residual correlation of a multivariate model have one prior slot, as in brms. The named-list prior spelling is refused. set_prior("") is brms’s (flat) on its slot.
  • mo() takes a variable with one or two values, as brms does.
  • marginaleffects reads vcov = and brms’s prediction arguments, and gives finite standard errors on a fit with an empty mo() category.
  • build_objective() reads a prior_only field, for sample_prior = "only" in frmtmb.sample. The sampling-API function autocor_innov_sd() is removed.
  • Update the extensions together with frmtmb. frmtmb.sample 0.24.0 needs frmtmb 0.77.0.

Breaking changes

  • Under the covariance form of ar(), ma() and arma(), sigma is the innovation sd, as in brms 2.23.0. Before, it was the marginal sd of a residual. The model was the same, but the parameter was not brms’s. On an AR(1) with ar 0.93, sigma(fit) was 1.078 and is now 0.406, against a brms posterior mean of 0.432. For an AR(1), the marginal sd is sigma / sqrt(1 - ar^2). At any order, it is sigma times the square root of the process variance at unit innovation variance. The logLik and the other estimates do not change.

  • So sigma(), VarCorr(), summary(), confint(), vcov(), the standard errors and fitted(dpar = "sigma") report the innovation sd. Under sigma ~ x the coefficients of sigma are those of the log innovation sd, as brms’s Intercept_sigma and b_sigma are. Pearson residuals, simulate() and the pointwise log-density read the marginal sd, as before.

  • A prior on class sigma is a density on the innovation sd itself, and its lb and ub bound it, as in brms. Before, the density was read at the marginal sd times a factor of ar and ma. A lb above 0 or a ub was refused. Under sigma ~ x, a prior on the intercept of sigma is now read at the log innovation sd, as brms reads it. Before, it was read at the log marginal sd.

  • The standard error of log(sigma) is the one of the innovation sd: 0.109 against 0.213 for the marginal sd on the AR(1) above. Its correlation with the ar parameter falls from 0.88 to -0.34.

  • ar(p = 2, cov = TRUE) and the other higher orders, which brms does not fit, read sigma as the innovation sd too.

  • conditional_effects(method = "posterior_predict") draws its band at sigma, so under these terms at the innovation sd. brms does the same: its conditional_effects() drops every autocorrelation term. The band is narrower than before by sqrt(1 - ar^2) for an AR(1). On an AR(1) with ar 0.84, its half-width over 1.96 was 0.855 and is now 0.470.

  • cooks.distance(), dfbeta(), dfbetas() and influence() read a coefficient vector that holds the log sd of sigma, which is now the innovation sd. So their values change. On the same AR(1), the sum of Cook’s distances was 0.642 and is now 0.935.

  • A fit saved by frmtmb 0.76.0 or earlier with one of these terms is refused where an answer reads sigma or its coefficients. These refuse: summary(), print(), sigma(), VarCorr(), confint(), vcov(full = TRUE), predict(), simulate(), pearson and deviance residuals(), the influence measures, and hypothesis() on sigma. fitted(), frm_linpred(), emmeans() and conditional_effects() with dpar = "sigma" are refused, and so is conditional_effects(method = "posterior_predict"). Under sigma ~ x, fixef(), coef() and vcov() are refused too. insight::get_sigma(), get_parameters() and get_varcov() are refused as well.

  • Such a fit still answers where only the mean is read, because its model is the same: logLik(), AIC(), fitted(), frm_linpred(), emmeans() on the mean, residuals(type = "response"), and, with a constant sigma, fixef(), coef() and vcov(). update() fits it again. Draws saved by frmtmb.sample 0.23.0 for such a model still report the old marginal sd as sigma in summary().

  • For a current fit, insight::get_sigma() gives insight’s default, as before: sigma() where it answers, and sqrt(deviance / df) under sigma ~ x, where sigma() returns NA. Only a saved fit with one of these terms is refused.

  • The sampling-API function autocor_innov_sd() is removed: no prior needs it now.

  • mo() takes a variable with two values, or with one, as brms 2.23.0 does. Through 0.76.0 both were refused (“mo() needs at least 3 ordered categories”). Two values (brms’s Jmo = 1) give a simplex of one weight, which is fixed at 1, so the term is its coefficient times the code. On 200 rows, y ~ mo(x) + z with x in 1 and 2 has the log-likelihood of lm(y ~ I(x - 1) + z) to 12 digits. summary() reports the weight mox1[1] as 1 with an error of 0, as brms does. A two-level ordered factor is the same model. One value (brms’s Jmo = 0) gives a term that is 0 on every row. Its coefficient is not determined by the data, so its standard error is NaN, with the usual warning. It is not counted in the degrees of freedom of logLik(), AIC() and anova(), as lm() does not count a column it aliases. predict() holds it at its estimate. A group-level term on such a variable, (mo(x) | g), is refused: its column is 0 on every row. brms builds the model but cannot sample it: its Stan program rejects Jmo = 0, and frm_sample() refuses it too. So update(fit, newdata =) on new data where a mo() variable has one value now refits by maximum likelihood, where brms’s update() builds the model.

  • marginaleffects’ vcov = argument reaches the fit. Through 0.76.0 get_vcov() ignored it, so a user’s matrix, "HC3" or a cluster formula gave the model-based standard errors without a message: predictions(fit, vcov = 2 * get_vcov(fit)) gave a ratio of 1 where lm() gives 1.414214. Now a matrix over get_coef(), matched by its names, works. So do a matrix over the whole outer parameter vector, a function, FALSE, "HC0", "HC1" and a one-sided cluster formula. marginaleffects maps "stata" to HC1. The whole-vector matrix is refused for a REML fit, whose vector has no fixed effects. "HC0" and "HC1" equal sandwich::vcovHC() on a gaussian linear model, and ~ g equals sandwich::vcovCL() on a Poisson glm. The bread is the observed information, so for a glm with a non-canonical link the sandwich differs from sandwich’s, which reads the expected information: 1.128 times its standard error of the slope on 300 rows of a Gamma(link = "log") model. The other strings are refused, each with its reason: "HC2" to "HC5" and "robust" need leverages, which frmtmb does not compute, "HAC" and "NeweyWest" a time order, "satterthwaite" and "kenward-roger" denominator degrees of freedom. Under a residual correlation term, such as ar(cov = TRUE), "HC0", "HC1", "stata" and a cluster formula are refused too. Its density is joint over each group. vcov = TRUE works there. See ?frmtmb-marginaleffects.

  • Take a fit whose mo() term has a category that no row is in. There get_coef() carries that simplex as its cumulative weights at the observed categories (zeta1_cum1, …), not as its chart coordinates. So marginaleffects gives NaN only to a prediction at an empty category, with one warning, as fitted() does. In 0.76.0 every standard error of such a fit was NaN. On 200 rows with values 1, 2 and 5, predictions() at 1, 2 and 5 now gives 0.1222210, 0.1135415 and 0.1212624. These are the standard errors of lm() on factor(x) with the maximum-likelihood sigma. With vcov = "HC0" it gives 0.1224166, 0.1103619 and 0.1248376, lm()’s HC0.

  • marginaleffects’ prediction arguments that frmtmb does not take are refused by name: lme4’s re.form and allow.new.levels, brms’s sample_new_levels, ndraws, draw_ids and incl_autocor, and glmmTMB’s zitype. marginaleffects warns that every argument of a frmtmb_fit is “not known to be supported”, so a dropped argument looked the same as one that works: re.form = NA returned the conditional predictions.

  • Each random-effect parameter, and the residual correlation of a multivariate model, has one prior slot, as in brms 2.23.0, which has no class theta. A standard deviation is class sd (a gp() term’s sdgp, and lscale for its length scales). A correlation that an LKJ density fits is class cor, and a residual correlation is class rescor. set_prior(class = "theta", coef = "theta_4") on such a parameter is refused with the class to write, and default_prior() lists no theta row for it. Until now class theta was a second slot on the same parameter, and the one written later replaced the other with no message. On (1 + x + z | g), lkj(2) on class cor and then normal(0, 1) on theta_4 kept the normal on one correlation parameter and no density on the other two. A MAP fit took those two to 559 and 1741. A class sd prior with coef = "x" beside a theta_2 prior was dropped. Class theta keeps the parameters that no brms class names. Examples are a toep() correlation and a parameter of a structure that an extension registers (a car() dependence). With no coef it covers only those. One correlation takes no bound any more. brms has none either: its program for lb on class cor does not parse.

  • A centered intercept has one prior slot, class Intercept. set_prior(class = "b", coef = "Intercept") there is refused, as brms refuses it. It put a second density on the intercept, without the centering. Class Intercept with nlpar is stored as brms’s slot of the same parameter, class b with coef = "Intercept". So the two spellings together are refused as a duplicate. Before, the one written later applied.

  • default_prior(route = "sample") prints brms’s (flat) with lb and ub for a first-order cov = TRUE ar or ma, for cosy and for lagsar, where it printed uniform(-1, 1). The table passed back as prior = gives the same sampled density as the defaults: at 300 points on each of six models the log density agrees to 2.3e-16 relative and the gradient exactly. On frm()’s route the same table is read as written: a (flat) row with lb and ub is a box with no density, as every bounds-only specification is there. A box at the ends of the interval bounds nothing. So frm(prior = default_prior(..., route = "sample")) on such a model moves, and uniform(-1, 1) on ar and (flat) with lb = -1, ub = 1 give different frm() fits. On y ~ 1 + ar(t, g, cov = TRUE) (6 series of 8) the internal ar estimate under the table is 0.8053, where it was 0.7249; maximum likelihood gives 0.8089. The prior object that prior_summary() returns for a frm_sample() run carries the densities. Passed to frm(), it still gives 0.7249, the mode of the sampled density.

  • The named list of prior objects, prior = list(theta_1 = prior_normal(0, 1)), put densities on internal parameters by name. frm(), update(), validate_prior() and frm_simulate() refuse it, and say how to write the same density with set_prior(). brms has no such spelling. It gave a parameter a second prior slot beside its class: on (1 + x + z | g) in frm_sample(), list(theta_4 = prior_normal(0, 1)) removed the default lkj(1) from all three correlations and left two of them with no density. A plain data frame passed as prior = is refused too, as brms refuses one, with a pointer to as.brmsprior(), which reads it. The prior_normal() family of constructors stays: they are densities set_prior() takes. With the list goes the only prior on a single threshold’s internal parameter and on the free directions of sum-to-zero thresholds; brms has neither.

  • set_prior("", class = ...) with no bound is brms’s (flat) on that slot, where it was refused. frm() applies nothing there, so a prior made only of such slots leaves a maximum-likelihood fit. A default_prior() or brms table row with an empty prior and no bound is read the same way, where it was dropped. A default_prior(route = "fit") table keeps that route through a row or column selection and subset(). A priorlist read from it keeps it through +, c() and as.brmsprior(). So frmtmb.sample can refuse it. as.data.frame() drops the route. So as.brmsprior() marks a data frame that holds frmtmb’s printed (flat). frm() drops either mark from the prior it keeps.

  • c() of a prior specification with something that is not one, such as a density object or a named list of them, is refused. The message gives the set_prior() spelling to write. Before, it built a prior that frm() then stopped on with an internal error. c() now reads a default_prior() table or a brms prior as as.brmsprior() reads it, with its route. That holds when a specification or a frmtmb table comes first. brms’s c() combines the rows the same way. With a brms table first, brms’s own c() refuses the frmtmb specification.

New features

  • A prior density can be brms’s (flat). The prior resolver reads it as no density on the parameter a class names, with the Jacobian of the map to the internal parameter. An LKJ density can be flat on the Cholesky factor. frmtmb.sample’s prior = "flat" uses both, so frmtmb.sample 0.24.0 needs frmtmb 0.77.0.

  • build_objective() reads a third field on a copy of the frame, prior_only. With prior_only = TRUE the objective leaves out every response density. It keeps the densities of the group-level coefficients, of the me() latent values and their measurements, and of a mi(sdy) response’s measurements. These are the terms brms 2.23.0 keeps outside if (!prior_only) under brm(sample_prior = "only"). A fit’s own frame never carries it. frmtmb.sample’s frm_sample(sample_prior = "only") reads it, to sample the prior of the whole model. ?frmtmb-sampling-api documents it.

  • plot() of a hypothesis draws the density of its prior draws in the second of colors, as brms does, unless ignore_prior = TRUE. Draws made by frmtmb.sample::frm_sample(sample_prior = "yes") carry them; a maximum-likelihood fit has none, and its plot does not change.

Bug fixes

  • predictions(), comparisons() and slopes() of marginaleffects pass brms’s re_formula, allow_new_levels, dpar (also brms’s nlpar) and resp to the prediction. Through 0.76.0 they were dropped, so predictions(fit, re_formula = NA) returned the predictions conditional on the group-level effects. marginaleffects 1.0.0 still warns that these arguments are “not known to be supported” for this class. It keeps a fixed list of known arguments per class. The arguments have their effect.

  • predictions() on new data with a factor that has levels no row uses returned one row more for each level: 11 rows for 3 rows of a factor with 8 levels. marginaleffects adds a row for each level and removes those rows by their row id, which frmtmb’s get_predict() numbered again.

  • vcov_cluster() gave NaN in every entry on a fit that lost a parameter (a mo() category no row is in, a variance at its boundary). The bread held NaN in the lost rows. The bread is now the covariance conditional on the lost directions, and only the lost rows are NaN.

  • prior = given a list of set_prior() specifications, such as list(sp, sp2), is refused with a pointer to sp + sp2 and c(sp, sp2), as brms combines them. It gave a generic message about a list of length 2.

Documentation

  • New help topic ?frmtmb-marginaleffects: the parameters marginaleffects perturbs, the vcov spellings and brms’s prediction arguments.

frmtmb 0.76.0

The main changes:

  • BREAKING: mo() codes an integer variable from its minimum, as brms 2.23.0 does. Every fit whose integer mo() variable does not start at 0 changes. A fit saved by 0.75.0 or earlier with such a variable is refused.
  • BREAKING: each residual correlation parameter has one prior slot, and the prior tables bound the residual correlation classes as brms does. Under a first-order cov = TRUE ar(), ma() or arma(), a prior on class sigma is read at brms’s innovation sd.
  • The standard error of a prediction includes the uncertainty of the mo() simplex.
  • New in the sampling API: autocor_flat_priors(), autocor_bound_chart(), autocor_innov_sd() and autocor_row_loglik(). New seam slot: frmtmb_register_autocor(jacobian =).
  • Update the extensions together with frmtmb. frmtmb.sample 0.23.0 and frmtmb.spatial 0.3.0 need frmtmb 0.76.0.

Breaking changes

  • mo() codes an integer variable from its minimum, as brms 2.23.0 codes it: values 1 to 4 have a simplex of 3 weights and the codes 0 to 3. Since mo() was added in 0.15.0, frmtmb coded the integers themselves. So the simplex had one weight for each value between 0 and the minimum that no row has. Every fit whose integer mo() variable does not start at 0 changes, which includes every Likert item on a 1..k scale. On such data (300 rows, values 1 to 4) the maximum-likelihood coefficients were not identified: the fit had 6 degrees of freedom where brms’s model has 5, and 3 of 4 predictions had a NaN standard error. frm_sample() sampled that other model, with only ESS warnings: Intercept 0.912 (sd 1.330) against brms’s 2.098 (0.117). Now the fit on 1..4 is the fit on 0..3, with the same log-likelihood, coefficients and finite standard errors. frm_sample() agrees with brms within 2 Monte Carlo standard errors on every parameter. An integer mo() variable with only two values, such as 1 and 2, is now refused (“mo() needs at least 3 ordered categories”). 0.75.0 fitted it with an empty category 0, so its coefficients had no standard errors. brms fits it as a linear term in the code.

  • mo() takes negative integers, as brms does; 0.75.0 refused them.

  • New data for an integer mo() variable are coded from the fitted data’s minimum. A value that is not a whole number is refused, as brms’s validate_newdata() refuses it. So is a value below the fitted minimum or above the fitted maximum. Through 0.75.0 a value between 0 and the fitted minimum was predicted without a message, and a fractional value was rounded to the nearest category: on a 1..4 fit, 0.6 was predicted as 1 and 4.4 as 4. So marginaleffects’ avg_slopes(variables = "k") was 0, because its finite difference never left a category; it is now refused with this reason. avg_comparisons() between two categories works. A missing value, a factor or a character vector in new data is refused with its own reason. mo(mi(x)) is refused at the fit, as brms refuses it.

  • conditional_effects() holds a numeric mo() variable at round(median()) and moderates with round(median +/- mad), as brms does. It held the variable at the mean and the mean +/- sd, which the rounding above then moved to another category. So a display was off by a whole category, and its labels named values the model was not evaluated at. The grid of a mo() effect is the fitted term’s categories, which in a subset() response can be fewer than the data’s.

  • emmeans refuses a reference grid that holds an integer mo() variable at its mean, as brms does, and names the remedy: at = or cov.reduce = list(k = function(v) round(median(v))) for the variable k, which leaves every other covariate at its mean. It reported the mean at the rounded category under the label of the mean.

  • The standard error of a prediction includes the uncertainty of the mo() simplex, in fitted(), predict(), frm_linpred(), conditional_effects(), emmeans and marginaleffects. Since 0.15.0 the delta method left it out. On 200 rows with the values 1, 2 and 5, the standard errors at 1, 2 and 5 were 0.121, 0.0987 and 0.121. They are now 0.122, 0.114 and 0.121, and brms’s posterior sds are 0.124, 0.116 and 0.123. A prediction at a category that no row has, inside the range (3 and 4 here), now has a NaN standard error. The fit’s warning says so. Through 0.75.0 it was finite (0.0859 and 0.0950). marginaleffects reads the simplex through get_coef() and get_vcov(). On 300 rows of y ~ mo(k) + z, predictions() at k = 2 and 3 gave 0.0846 and 0.1035. The exact standard errors are 0.1220 and 0.1142, and it gives those now. On a fit with an empty category marginaleffects cannot leave the undetermined direction out, so its standard errors are NaN there, with a warning. dfbeta() and insight keep the coefficients alone.

  • A fit saved by frmtmb 0.75.0 or earlier is refused when its integer mo() variable does not start at 0. The message says to fit the model again. Every method that reports an estimate, a degree of freedom, a standard error or a comparison refuses it: fixef(), coef(), logLik(), AIC(), BIC(), vcov(), confint(), summary(), anova() and drop1(). So do fitted(), predict(), simulate(), residuals(), conditional_effects() and frm_sample(). update() fits it again. A saved fit on data that start at 0, or on an ordered factor, is the same model and works as before.

  • One residual-correlation parameter has one prior slot, as in brms. default_prior() listed each parameter of an ar(), ma(), arma(), cosy() or unstr() term twice: under its class (ar, ma, cosy, cortime) and as class = "theta", coef = "thetaac_1". validate_prior() and frm() took a prior on each, and the fit applied the one written last. brms 2.23.0 has the class row only. Now the internal row is not listed, and a prior on it is refused with the class to use: “class =”theta”, coef = “thetaac_1” is the internal parameter of ar, which has a class of its own, as in brms. Write the prior with class = “ar”“. A multivariate model’s refusal names the resp too. This is the rule that sar()’s lagsar got in 0.75.0. It is now one rule for core’s classes and for each bounded parameter of a term registered with frmtmb_register_autocor(). class = "theta" still reaches a parameter that no class reaches: a random-effect theta_1, a thetar_1, and a parameter of a registered term that declares no interval. So two kinds of parameter take no box now. The internal parameters of a cov = TRUE ar() or ma() of order two or more take none; brms’s cov = TRUE form is of order one only. A single correlation of an unstr() term takes none either: class = "theta", coef = "thetaac_3" was the one route to it, and class = "cortime" takes lkj() on the whole matrix and no lb/ub, as in brms.

  • The prior table bounds the residual-correlation classes as brms does. default_prior() lists lb = -1 and ub = 1 on a first-order cov = TRUE ar or ma, as brms 2.23.0 does, and no bound under cov = FALSE. For cosy it lists the window frm() fits it on, -1/(d - 1) to 1 for d time points. On route = "sample" it lists brms’s 0 to 1, where frmtmb.sample now samples it.

  • A prior on sigma under a first-order cov = TRUE ar(), ma() or arma() is read on brms’s sigma, the innovation sd. frmtmb’s sigma there is the MARGINAL sd of a residual, and summary(), sigma() and VarCorr() report it. brms’s sigma is the innovation sd, smaller by c = sqrt(1 - ar^2), 1 / sqrt(1 + ma^2) or sqrt((1 - ar^2) / (1 + ma^2 + 2 ar ma)). On an AR(1) of 0.9 that is a posterior mean of 1.16 here against 0.43 in brms, for the same model. Before, set_prior("student_t(3, 0, 2.5)", class = "sigma") was a density on the marginal sd. Now it is brms’s density at sigma * c, with the Jacobian of c, in frm()’s MAP penalty and in frmtmb.sample alike. A lb above 0 or a ub on sigma there is refused. A box on the innovation sd moves with ar, so it is no box on the marginal sd frmtmb holds. brms 2.23.0 takes such a bound on its innovation sd (real<lower=0.2> sigma).

New features

  • Three functions join the interface for a sampling extension (?frmtmb-sampling-api). autocor_flat_priors() gives brms’s flat default on each residual-correlation class that frmtmb holds through a transform. autocor_bound_chart() gives a chart that samples a bounded residual-correlation parameter through one logistic onto its interval. autocor_innov_sd() names the responses whose sigma brms reads as an innovation sd. frmtmb.sample uses all three.

  • New in the sampling API: autocor_row_loglik(fit, dpv, resp), the pointwise log-density of a response with a residual covariance term. These terms are the covariance form of ar(), ma() and arma(), cosy(), unstr(), fcor() and a registered term. Each row is brms 2.23.0’s leave-one-out conditional density given the other rows of its group, read off the precision matrix. On 18 cases (every term, gaussian and student, ragged groups, gaps, sigma ~ x) it equals brms’s log_lik() at the same parameters to 2.0e-15 relative. One draw costs one small inverse per pattern of time points. On 400 groups of 10 rows a draw takes 0.64 ms, against 17.8 ms for brms’s per-group dense inverses. For fcor() on 600 rows it takes 2.6 ms, against 97.5 ms. The values do not sum to the log-likelihood.

  • frmtmb_register_autocor() takes jacobian =, the Jacobian of the term’s whitening map, from which the pointwise log-density reads the precision matrix. A sparse one costs its nonzeros per draw. Without it, frmtmb inverts unwhiten() of the identity, which is exact and dense: 0.5 s and 48 Mb per draw at 1000 rows, 1.7 s and 155 Mb at 2000. Above 1000 rows log_lik() warns once per call, with class frmtmb_autocor_dense_fallback. A given jacobian is checked against the map when the frame is built. The check runs at the start and at a generic point: sigma not 1 and varying by row, and every parameter moved off the start. A wrong one is refused by the term’s name.

Bug fixes

  • A bound at an end of a residual-correlation parameter’s interval is accepted, and bounds nothing, since the parameter never reaches that end. Before, frm() refused it: set_prior("", class = "ar", lb = -1) stopped with “lb = -1 is outside (-1, 1)”. So the default_prior() table of a sar() model, whose lagsar row lists the ends of its interval, did not pass back as prior = in 0.75.0. Past the end, a bound is still refused.

  • ?frmtmb-autocor said that set_prior() cannot target the residual-correlation parameters; it has done so by their classes since 0.49.

frmtmb 0.75.0

The main changes:

  • BREAKING: coefficient names and orders follow brms 2.23.0. A term that calls exp(), or another function named like a covariance structure, has brms’s name (expx, not .frm_expx). default_prior() lists each coefficient by brms’s name (IxE2, polyx21), and the prior tables sort their rows as brms sorts them. fixef() orders nonlinear and multivariate rows as brms does.
  • BREAKING: brms’s bhaz() addition term is in frmtmb.survival. Without that package, bhaz() stops with a message that names it.
  • BREAKING: set_prior() accepts a dirichlet() row for every class. validate_prior() and frm() refuse it when the class is not a simplex of the model.
  • New: brms’s fcor() residual term is in frmtmb, and brms’s sar() term is in frmtmb.spatial.
  • New seams for extension packages: frmtmb_register_aterm(option =) for an addition term that sets an argument of the family, and frmtmb_register_autocor() for a residual correlation term. With them come frmtmb_autocor_pars(), frmtmb_covstruct_info() and frmtmb_register_covstruct(full_rank =).
  • A saved fit that needs extension packages this session cannot load names every one of them in one refusal.
  • Update the extensions together with frmtmb. frmtmb.survival 0.2.0, frmtmb.spatial 0.2.0, frmtmb.sample 0.22.0 and frmtmb.latent 0.7.1 need frmtmb 0.75.0.

Breaking changes

  • A dirichlet() prior on a parameter that is not a simplex is refused at one point, however it is written: where the prior meets the model, in validate_prior() and frm(), before any fitting. Through 0.74.0, set_prior("dirichlet(2)", class = "Intercept", dpar = "sigma") was refused by set_prior(), but class = "sigma" was refused at the fit. Only the model can tell a declared simplex (frmtmb_family(simplex_pars =)) from a distributional parameter. So set_prior() now accepts the row for every class, and the refusal repeats the spelling as written. brms 2.23.0 accepts both spellings in set_prior(), validate_prior() and stancode(), and stops at the Stan compile. The link-scale spelling of a declared simplex, class = "Intercept", dpar = "tw", is refused by name; write class = "tw". A brms prior object with such a row is refused the same way. Before, it was refused as “a brms prior table has 1 row with no faithful frmtmb spelling”. On class = "theta", the refusal adds that brms’s theta of a mixture() is the Dirichlet on its mixing proportions, which frmtmb’s theta is not.

  • brms’s bhaz() addition term is frmtmb.survival’s. Without that package, time | bhaz(df = 6) ~ x stops with “The bhaz() addition term is in the extension package frmtmb.survival, which is not loaded”, as dec() names frmtmb.eam. Before, frmtmb refused bhaz() as a term it does not support and pointed to cox(df =). With frmtmb.survival loaded, bhaz() sets the basis of the cox() baseline, as in brms 2.23.0.

  • A term that calls exp() has brms’s coefficient names. To keep exp(x) from being dropped as a covariance structure, the parser renames the call to .frm_exp(x). Since 0.15.0 that alias was the name of the design column. So the coefficient was .frm_expx in fixef(), vcov(), summary(), coef(), ranef() and VarCorr(). It was b_.frm_expx in variables() and in the draws of frmtmb.sample::frm_sample(), and .frm_exp(x) in confint() and default_prior(). brms 2.23.0 names it expx and b_expx, and set_prior(coef = "expx") and hypothesis(fit, "expx > 0") were refused. The same applies to I(exp(x)), exp(x):z, a group-level (1 + exp(x) | g), and a call of any other function whose name is that of a covariance structure. Now the alias stays in the parser: the coefficient is expx everywhere brms has it, and the confint() row is exp(x). Of 40 formula terms compared with brms 2.23.0, 18 had the alias in their names.

  • default_prior() lists each coefficient by brms’s name, and set_prior(coef =) takes that name. The coef column held the design column, such as log(xp), I(x^2) or poly(x, 2)1, where brms 2.23.0 lists logxp, IxE2 and polyx21. And set_prior() refused brms’s name where the rename is more than dropped parentheses, such as IxE2, polyx21 or expxD2. The design column name still works in set_prior(coef =), for class "b", class "sd" and a cs() term alike. The two spellings name one slot, so writing both is refused as a duplicate. Before, class "sd" and cs() refused coef = "I(x^2)" and "poly(x, 2)2".

  • The rows of default_prior(), validate_prior() and prior_summary() are in brms’s order. default_prior() orders them as brms’s does, by response, distributional parameter, nonlinear parameter, class, group and coef, so b comes before cor, Intercept and sd. validate_prior() and prior_summary() order them as brms’s do, by class, group, response, distributional parameter, nonlinear parameter and coef. frmtmb’s own class "theta", which brms does not have, stays last. Before, Intercept came first and cor after sd, and only the sd rows of validate_prior() were in brms’s order.

  • A nonlinear parameter of a multivariate model has brms’s prefix. brms writes the distributional parameter, then the response, then the nonlinear parameter, so a of the response yn is yn_a: b_yn_a_Intercept and sd_g__yn_a_Intercept in variables() and the draws, yn_a_Intercept in fixef(). frmtmb wrote b_a_yn_Intercept and a_yn_Intercept, and hypothesis() refused brms’s names.

  • fixef(), vcov() and summary() of a fit list the rows in brms’s order in two more cases. A nonlinear parameter’s intercept keeps its place among its coefficients, because brms does not center the formula of a nonlinear parameter: for bf(y ~ a + b * x, a ~ exp(z), b ~ 1 + z, sigma ~ z, nl = TRUE) brms 2.23.0 gives sigma_Intercept, a_Intercept, a_expz, b_Intercept, b_z, sigma_z, where frmtmb listed a_Intercept and b_Intercept first. And a multivariate model lists its rows response by response, as brms declares them: sigma_yn_Intercept before y_Intercept when yn is the first response.

New features

  • frmtmb_register_aterm(option =) registers an addition term that carries no data and sets an argument of the family instead. brms’s bhaz(df = 6), for example, sets the baseline basis of cox(). frmtmb calls the function with the family, the term’s call as written and the formula’s environment, and fits the family that it returns. A family whose frmtmb_family(accepts_aterms =) does not name the term is refused before the function runs, as brms refuses bhaz() for a family other than cox(). frmtmb.survival uses it for bhaz(). frm_compat()’s generic rows for addition terms now say what each kind of term does. A registered data term carries a column to the family’s density. An option term adds nothing to the likelihood and sets an argument of the family of its own response.

  • fcor(M), brms’s fixed residual correlation term, is supported. As in brms 2.23.0, the residuals of a gaussian() or student() response are one draw with covariance sigma^2 M. Under a predicted sigma ~ ... the covariance is D M D with D = diag(sigma). M comes from data2, covers every row of the data, and loses the rows the model drops, as in brms; a vector is its diagonal. The log-likelihood equals brms’s Stan program to 7e-15 at frmtmb’s estimates (gaussian, with a brms gradient there below 3e-6). It equals it to 1.4e-14 at set parameter values with nu = 5 (student). A brms fit of 64 rows agrees with the maximum likelihood fit to within 0.33 standard errors in every parameter. fitted(), residuals() (pearson divides by sigma * sqrt(diag(M))), simulate(), predict() and autocor_matrix() work on the fitted rows. They also take newdata with one row per row of M, read in their order, as brms reads it. So a counterfactual (x + 1 everywhere) works. When the fit dropped rows for missing values, newdata covers them too, and a row with a missing predictor gives NA, as in brms. fitted() there equals brms’s posterior_epred() at the same parameter values. Other newdata is refused, because M covers the fitted rows only; brms refuses it too unless newdata2 holds a matrix over the new rows. frm_linpred() takes any newdata. frmtmb refused fcor() by name before.

  • brms 2.23.0 checks that M is positive definite with min(eigen(M)$values <= 0), which passes a matrix with a negative eigenvalue. Its fit of such an M then fails in Stan, where the Cholesky factor of M is taken. frmtmb refuses every M that is not positive definite, before the fit.

  • brms’s sar() is now supported in frmtmb.spatial. Without that package, sar() in the formula or in bf(autocor = ~ sar(M)) stops with a message that names it, where frmtmb refused it as unsupported before. set_prior() takes class = "lagsar" and class = "errorsar", as brms does.

  • New seam frmtmb_register_autocor(). A package registers a term that correlates the residuals of a gaussian or student response. The term is one affine map from the response to independent innovations, with its inverse. frmtmb writes the density, the mean, the draws, the residual covariance and the guards from the map. fcor() is frmtmb’s own term of this shape, and frmtmb.spatial registers sar() through it. A covariance structure (frmtmb_register_covstruct()) cannot express such a term: it adds a latent vector to the linear predictor, and a lag SAR changes the mean of the response itself. frmtmb_local_registries() saves and restores the new registry.

  • The map gets one sigma when the term sets sigma_varies = FALSE. unwhiten() takes a matrix of innovations, one column each, so the pearson residuals and autocor_matrix() solve the term’s system once for every column. On a 50 x 50 lattice under sar() (2500 rows), they take 0.48 s and 4.3 s. One solve per column took 16 s each. When the model is built, frmtmb checks the map once. unwhiten() must give one value per row and one column per column of innovations. whiten() must give one innovation per row and one finite log-Jacobian. whiten(unwhiten(u)) must give back u to a relative 1e-6. A map that fails is refused with the term’s name.

  • A term’s bounds slot states the interval of a parameter that natural maps by lower + (upper - lower) plogis(theta). set_prior() then takes the parameter’s name as its class, and lb and ub narrow the interval. frmtmb.sample’s default prior on it is flat on the interval, as brms’s on lagsar is. A class prior on a parameter without bounds is refused, with the internal parameter to write it on instead. A parameter with bounds has one prior slot, its class: its internal row is not in default_prior(), and a prior on it is refused. New export frmtmb_autocor_pars() lists the parameters of a fit’s registered terms, with their intervals.

  • A slot of frmtmb_register_autocor() or frmtmb_register_covstruct() can be a function of ....

  • New export frmtmb_covstruct_info(): what the registry knows about one covariance structure (its package, whether it is a field, whether its levels are coupled, whether it is full rank). frmtmb.sample reads it instead of keeping its own list of field structures. A name that core lists as an extension’s ("car", "spde") loads that extension. A residual correlation term’s name, such as "sar", is refused with a pointer to frmtmb_autocor_pars().

  • frmtmb_register_covstruct(full_rank =) states whether an effect a row owns can reach any value with a finite density: a covariance that is positive definite at some parameter value and a density that is positive everywhere. A structure that states nothing leaves the weights check open for it, because its covariance does not tell: a latent with bounded support keeps a weighted model’s maximum. A package whose structure is full rank should state full_rank = TRUE.

  • autoscale leaves the random slopes of every structure but us() and diag() as they are. That holds for frmtmb’s own homdiag() or cs() as much as for a structure another package registers. Nothing tells frmtmb that their parameters scale with the column. It now says so in a message that names the structure and the column. It said nothing before.

Bug fixes

  • A saved fit that needs extension packages this session cannot load names every one of them in one refusal (class frmtmb_extension_missing), with one instruction: “This model needs 2 extension packages …: frmtmb.survival (the cox family of this model) and frmtmb.spatial (the car() structure of this model). Install them and run library(frmtmb.survival); library(frmtmb.spatial)”. Through 0.74.0 a cox() fit with a car() term named one package at a time: summary() named frmtmb.survival and print() named frmtmb.spatial, so the second was found only after the first was installed. Every method of a fit that reads the model’s families, structures or residual correlation terms checks them all first. So hypothesis(), residuals() and drop1() name every package too. A method that reads only stored values, such as logLik(), still answers. The refusal for a cox() fit an older frmtmb built names the other missing packages too. Where its new home, frmtmb.survival, is installed but cannot be loaded, the refusal says to reinstall it.

  • fixef() of a fit with a part from a package this session cannot load refuses, by the package’s name. Through 0.74.0 it answered with Est.Error NA: it read the refusal as “no covariance”. The same was true of a saved cox() fit from frmtmb 0.73.0 with frmtmb.survival installed. Every handler that reads an error as “no answer” now lets this refusal through. So predict() no longer warns first that “the covariance of the estimates could not be recovered from the Hessian”. And cooks.distance() no longer says that “all deletion refits failed”.

  • predict(newdata = ) names the reason for the model at hand. Through 0.74.0 it said “its draws are structured (a hidden state sequence, a group-level latent class, or a correlated residual)” for every model. A mixture_mvn() fit is none of the three. It now names the family that draws its whole response at once, or the residual correlation term, such as ar(time = t, gr = g, cov = TRUE).

  • print() of a curve (frm_curve(), frm_curve_deriv()) or a curve feature (frm_curve_feature()) takes digits =, which reaches the printed rows as in print.data.frame(). Through 0.74.0 it stopped with “print() has no argument digits”.

  • drop1() and step() see a term that calls exp(). With the alias, drop1() found no columns for the term: on y ~ exp(x) + z (200 rows) the .frm_exp(x) row had Df 0 and the full model’s AIC, 607.84, so the term looked free to drop. The row is now exp(x), with Df 1 and AIC 625.69, the AIC of y ~ z refitted. And step() drops z, which lowers the AIC to 605.84; before, it kept both terms.

  • re_formula can name a group-level exp() term. fitted(fit, re_formula = ~ (1 + exp(x) | g)) on y ~ exp(x) + (1 + exp(x) | g) stopped with “matches no group-level term of this fit”, because the fit’s term was (1 + .frm_exp(x) | g). It now gives the full prediction, which is also the prediction of the same model on a column holding exp(x).

  • A smooth’s unpenalized column written in both spellings is one prior slot. set_prior(coef = "s(x).fx1") and set_prior(coef = "sx_1") name the same coefficient. But in 0.74.0 the pair was taken as two specifications, and the later one replaced the earlier without a word: with normal(3, 0.001) on the first and normal(-3, 0.001) on the second, validate_prior() listed only the second and the fit put the coefficient at -3. The pair is now refused as a duplicate, as two specifications for one slot always are.

  • A multivariate model whose second response’s nonlinear formula calls exp() fits. In 0.74.0 such a model, for example mvbf(f1, f2) with f1 = bf(yn ~ a * exp(b * x), a ~ 1, b ~ 1, nl = TRUE) and f2 = bf(y ~ c + d * x, c ~ exp(z), d ~ 1, nl = TRUE), stopped with ‘could not find function “.frm_exp”’.

  • The weights check refuses a weighted likelihood with no maximum. It named its full-rank structures in a list, so it failed open for every structure another package registers. It now reads each structure’s full_rank. Take a Gamma model with weights 1.5 and a registered iid block on one level per row that states full_rank = TRUE. It is refused now, as the same model with (1 | obs) is. Before, it fitted to a logLik of 849 with a shape of 5e14 and two warnings. A registered structure that is not full rank, or states nothing, is not refused.

  • The Student-t latents of gr(dist = "student") were not full rank for the weights check, so it failed open for them. A Gamma model with weights 1.5 and (1 | gr(obs, dist = "student")) fitted to a logLik of 4.7e7 with a shape of 8e19 and two warnings. It is refused now, as the same model with (1 | obs) is, and the refusal names the term as written.

Other changes

  • frmtmb’s R CMD check (four platforms) and its coverage job fail when a test skips because a suggested package is missing or cannot be loaded. The extension jobs have done so since 0.74.0. Every job with this gate also fails when testthat’s output has no summary line. No package is allowed to be absent. The GEV density test reads extraDistr, a suggested package, in place of evd, which frmtmb does not declare. So it runs on every platform. Through 0.74.0 it skipped on all four R CMD check jobs and in coverage.

  • The pkgdown job installs every extension. It also runs the saved-fit test of cox(), car() and sar() with neither extension loadable, and fails if any part of it skips.

frmtmb 0.74.0

The main changes:

  • Four features moved to extension packages. Install the package and load it to use them: Without the package, family = "cox", brmsfamily("cox"), car() and spde() stop with a message that names the package. The README shows how to install every extension at once.
  • frmtmb.spline is retired. Curve inference, frm_curve(), frm_curve_deriv() and frm_curve_feature(), is in frmtmb now. Remove an installed copy with remove.packages("frmtmb.spline"): it still loads with frmtmb 0.74.0, masks these three functions and replaces their print methods.
  • New seams for extension packages: frmtmb_register_covstruct(), frmtmb_family(simplex_pars =, aterm_data =), the family hook post$curve_check, us_sigma() and us_theta(), and frmtmb_local_registries().
  • Every brms family, addition term and formula special resolves in core, resolves in a named extension, or has a refusal that says why.
  • Update the extensions together with frmtmb. frmtmb.latent 0.7.0, frmtmb.survival 0.1.0, frmtmb.spatial 0.1.0 and frmtmb.sample 0.21.1 need frmtmb 0.74.0, and frmtmb.latent 0.6.2 does not load with frmtmb 0.74.0.

Breaking changes

  • cox() and cox_baseline() are in the new extension package frmtmb.survival. Load it with library(frmtmb.survival). brms has cox in its core, so family = "cox", brmsfamily("cox") and family = cox() resolve as before with frmtmb.survival loaded. Without it, family = "cox" and brmsfamily("cox") stop with “The cox family is in the extension package frmtmb.survival, which is not loaded”. A fit is the same fit. The tests have eight cox() models: censored, a frailty, df = 6 with the identity link, truncated, a sbhaz prior, and the three spellings. On each, the log likelihood, the estimates and the standard errors are identical() to frmtmb 0.73.0’s, and get_prior() gives the same table. A cox() fit saved by frmtmb 0.73.0 or earlier must be refitted: frmtmb refuses it by name and says to refit it with frmtmb.survival loaded. Every cox pair of frm_compat() keeps its status and note.

  • frm_curve(), frm_curve_deriv() and frm_curve_feature() are in frmtmb now, with their print methods and the article “How to report a fitted curve”. They were in frmtmb.spline, which is retired. Load nothing extra: they apply to any fitted curve, not only to splines. On the curves, derivatives, features and difference curves the frmtmb.spline tests read, their output is identical() to frmtmb.spline 0.10.2’s. Their print() methods now refuse an argument they do not take, as every frmtmb print method does; before, one was silently ignored.

  • set_prior() no longer refuses a dirichlet() on a class that is not one of frmtmb’s own, such as class = "sigma", when the row is written: such a name can be a simplex that a family declares, which only the fit can tell. frm() refuses the row instead, and names the simplex classes, as set_prior() did. validate_prior() refuses it too. A dirichlet() on one of frmtmb’s own classes, such as class = "b", is still refused by set_prior().

  • mixture_mvn() moved to the extension package frmtmb.latent. To fit a multivariate normal mixture, install frmtmb.latent and run library(frmtmb.latent). Without it, mixture_mvn() is an unknown function. The family, its arguments and its fits did not change. On the 59 fits of its tests, these are identical to the last bit on the same machine: the log-likelihood, the estimates, the standard errors, predict(), fitted(), mixture_probs() and simulate(). Its rows of frm_compat() come with frmtmb.latent, and with it loaded the table holds the same rows as before. brms has no multivariate normal mixture, so no brms spelling reached the family, and family = "mixture_mvn" was not a supported spelling before the move either. mixture(), mixture_probs() and pp_mixture() stay in core. mclust is no longer in Suggests. Update frmtmb.latent together with frmtmb: an older frmtmb.latent does not load with this frmtmb, because its hmm() rules name the mixture_mvn feature that core no longer has.

  • car() and spde() are in the new extension package frmtmb.spatial. Load it with library(frmtmb.spatial). Without the package, frm() refuses a formula that writes either term and names the package. The fields are the same. On the models of the moved tests, these are identical to frmtmb 0.73.0’s: the log-likelihood, every estimate and standard error, the fitted values with and without an unseen location, the ranef() conditional standard deviations, confint_varcorr() and simulate(). frmtmb.sample::frm_sample() gives the same draws from the same seed. With the package loaded, frm_compat() gives every pair the status it had. fmesher is no longer among frmtmb’s suggested packages.

New features

  • frmtmb_family(simplex_pars =) declares which extra parameters of a family are simplexes, by brms name: c(sbhaz = "sbhaz_raw") is the baseline of frmtmb.survival’s cox(). A declared simplex is read in the chart of a mo() simplex, and searched off a pole and off a face the likelihood rises from. frmtmb.sample draws it in the softmax chart under brms’s dirichlet(1), and names its draws as brms does (sbhaz[k]). It has a prior class of its own name in get_prior() and set_prior(). Until now these behaviors were written for cox() by name. frm() refuses, by name, a declaration that names no extra parameter the family returns, or one with no coordinate. Before, such a typo ended in “not enough degrees of freedom to define contrasts”.

  • Some saved fits have a family that an extension package built. When this session cannot load that package, such a fit now stops at the first method that needs the family’s own functions. These include summary(), vcov(), confint(), fitted(), residuals(), simulate(), update() and frm_curve(). The message names the package: “The cox family of this model is from the extension package frmtmb.survival, which is not installed …”. It stopped with “could not find function” and the name of one of that package’s internals. A family records the package that built it, and a mixture() the packages of its components. logLik(), fixef() and frm_linpred(type = "link") need no function of the family and still answer.

  • frmtmb_family(aterm_data =) builds data the densities read from the observed response, once, at frame assembly, such as a spline basis over the observed times.

  • A family’s post$curve_check(fit, resp) is run by frm_curve() and its two companions before they read a curve, and may refuse. It is how frmtmb.survival’s royston_parmar() refuses a curve off a fit whose cumulative hazard is not monotone.

  • us_sigma(theta, d) and its inverse us_theta(S) are exported. us_sigma() builds the covariance matrix that the parameter vector of a us() block encodes. It is AD-safe, so a family in another package can carry a free covariance matrix as an extra parameter. us_theta() gives the parameter vector of a covariance matrix, for a start. The correlation entries fill the lower triangle by column, which is glmmTMB’s order up to 3 x 3 and not from 4 x 4. us_sigma() refuses a theta of the wrong length and names the length it needs; inside frmtmb a wrong length was recycled with a warning. floored = TRUE now floors the variance of a 1 x 1 block as the 1 x 1 random-effect blocks do, which no caller in frmtmb reached.

  • frmtmb_register_covstruct() lets a package register a covariance structure from its .onLoad(): a bar structure written name(terms | g), or a field written as a predictor special such as car(M, gr = g). Its precision or covariance may depend on parameters and on fixed data that the special names. One entry carries everything that frmtmb and frmtmb.sample read: the objective, the standard errors and their checks, VarCorr(), confint_varcorr(), ranef(), predict() with and without new levels, simulate(), conditional_effects() and vcov_cluster(). The built-in structures are entries of the same shape. The structure joins frm_compat() as a feature of kind "covstruct", where quadrature and importance = refuse it, as frm() does. ?frmtmb-extension-api has the steps. The call checks the entry it builds, and each block is checked against its entry when the frame is built: the length of start, the positions of sd_idx, the dimension and names of vcov, and for a registered structure that lpdf is finite at zero. The call refuses a name that the rest of frmtmb, brms or another extension owns, such as gaussian or dec. It also refuses a function that formulas call as a term, such as poly, ns or log. A name that another package registered is replaced only with replace = TRUE. A fit records the package each structure came from: read where that package is not installed, the fit is refused by the package’s name, with class frmtmb_extension_missing, before a method reads the structure.

  • frmtmb_local_registries() saves every registry a package fills from its .onLoad() and restores it when a test ends, for an extension’s own test suite.

  • frm_compat() resolves the rules that packages contribute in the order of the packages’ names. So the status and note it gives a pair no longer depend on the order of the library() calls.

  • A brms spelling that an extension supplies is refused by name when that extension is not loaded, as the Wiener family already was: y | dec(rt) ~ x names frmtmb.eam and says how to load it. The brms names frmtmb does not support are refused with the reason and the frmtmb spelling: bhaz() (the basis is set in cox(df =, degree =, intercept =) of frmtmb.survival, and bhaz(gr = ) has no equivalent) and brmsfamily("custom") (use custom_family()). Every family, addition term and formula special of brms 2.23.0 now resolves in core, resolves in a named extension, or has such a refusal. This holds for its plainest spelling and for the others that brms documents. Before, custom, bhaz(), dec() and bare gr(g) did not.

Bug fixes

  • A call around a bar term that names no covariance structure is refused, at any depth and in re_formula = too. Through 0.73.0 the call was dropped and the term fitted with the default us structure with no message: foo(1 | g), foo(bar(1 | g)) and us_t(1 | g) all gave the log-likelihood of (1 | g), and re_formula = ~ foo(1 | g) was ignored.

  • (1 | gr(g)), brms’s plainest spelling of a grouping factor, is (1 | g), as it is in brms. Through 0.73.0 it was refused.

  • A gaussian model can have a field with one row per level, such as car() over areas with one observation each. Such a model no longer warns that its variance is confounded with the residual sd. A fixed matrix correlates the levels, as for gr(cov = A), which the check already exempted; through 0.73.0 it named three structures and missed the rest.

Other changes

  • gratia is a suggested package, for the curve tests that compare frm_curve()’s simultaneous band with gratia’s. splines2 is no longer suggested: only the cox() tests used it, and they moved with it.

frmtmb 0.73.1

Bug fixes

  • fitted() of a multinomial() or dirichlet_multinomial() fit holds the interval of each expected count at the row’s trials. Before, the upper bound was the sum of the estimates, which equals the trials only to rounding, so a bound could pass them by a few units in the last place.

frmtmb 0.73.0

The main changes:

  • cens() and trunc() for 23 more families, every one brms 2.23.0 censors, with brms’s reading of a discrete bound.
  • brmsfamily("negbinomial2") and dirichlet_multinomial().
  • The generics of brms’s loo_*() functions, kfold(), reloo(), loo_moment_match() and loo_subsample(), with a refusal on a fit.
  • quadrature = TRUE refuses a row that reads more than one random-effect block, and frm() refuses a weighted likelihood with no maximum.

Breaking changes

  • A discrete response is censored and truncated as brms 2.23.0 reads its bounds. Right censoring at k is P(Y > k), left censoring P(Y <= k), an interval P(k < Y <= k2), trunc(lb = k) P(Y >= k) through F(k - 1), and trunc(lb, ub) the window F(ub) - F(lb - 1). Through 0.72.0 a censoring bound on a count was inclusive (P(Y >= k) for right censoring), so the same data frame now means a different model: 200 poisson draws at lambda = 4 recorded as “6 or more” and right censored at 6 move by 30.2 in log-likelihood and 5.5 percent in lambda. Right censored at 5, they give the old fit exactly. This applies to every discrete family, poisson() and discrete_weibull() included. The once-per-session notice about the inclusive reading is gone.

    • One composition stays frmtmb’s, on every response: a censored row under trunc() is the event inside the window, P(y < Y <= ub) for right censoring and P(lb <= Y <= y) for left, over the window’s mass. brms 2.23.0 divides the event over the whole line, P(Y > y) or P(Y <= y), by the window’s mass, which is not a probability: on a poisson and a negative binomial with trunc(lb = 2, ub = 9) and censored rows the two log-likelihoods differ by 6.16 and 14.85. They agree for right censoring with no upper bound, left censoring with no lower bound, and every interval.
    • trunc(lb = 0), or any lb <= 0 on a count, is no truncation and is accepted, as in brms; it was refused.
    • An interval whose ends agree (y2 == y) is refused on a count, as on a continuous response: (y, y] holds nothing. A fractional trunc() bound on a count is refused, as a fractional cens() bound already was.
  • trunc() refuses a response outside its window, and a window that is empty, with brms’s two messages: “Some responses are outside of the truncation bounds” and “Truncation bounds are invalid: lb >= ub”. Such a fit used to run, and scored a row the truncated distribution gives no mass to.

  • The refusal of cens() and trunc() names the family. It points at frm_compat() for the term that was used, which lists the families that have a CDF. It no longer lists them itself.

  • The predictive draws of dirichlet() and brmsfamily("dirichlet2") change for the same seed. The Dirichlet draw that they and dirichlet_multinomial() share takes its gamma variates on the log scale. On the probability scale every variate of a row underflowed to 0 at a small concentration, and the row came out NaN: 18 rows of 20000 at phi = 0.01 and 9570 at phi = 1e-3, none now. simulate(), predict() and the draws methods of a dirichlet_multinomial fit with a small phi stopped with “NA in probability vector”. The distribution of the draws is unchanged; the means and covariances match the Dirichlet’s within Monte Carlo error at phi from 0.01 to 20. brms 2.23.0’s rdirichlet() has the same defect.

  • quadrature = TRUE refuses a row that reads more than one random-effect block. Nested blocks, (1 | ga/gb), crossed blocks, (1 | g) + (1 | h), and (1 | g) in both mu and sigma used to fit. But the Gauss-Kronrod rule integrated only some of the effects and left the rest to the Laplace approximation. Over (1 | ga/gb) the log-likelihood equaled the Laplace one to 1e-8, and summary() failed with “no ‘dimnames’ attribute for array”. They are now refused before any tape, as lme4 refuses nAGQ > 1 for more than one term, and the message names the blocks. One block per row is unchanged, including the blocks of a by-split gr(g, by = f).

  • frm() refuses a weighted likelihood that has no maximum. The refusal applies when two conditions hold. First, every row with a positive weight has a random effect of its own in mu. Second, the weights sum to more than the number of rows. If the dispersion formula has an intercept, the likelihood grows without bound as the dispersion goes to its limit (sigma to 0, shape to infinity). It grows like the peak height of the density to the power sum(w) - n. Such a fit used to stop at “NA/NaN gradient evaluation” under profile = TRUE and REML = TRUE. Under ML it reported a log-likelihood of 1.4e11 with only convergence warnings. The refusal names the term as written and the weight sum, and names what gives a usable fit: weights that sum to the number of rows or fewer, a random effect that is not observation-level, or a fixed dispersion. It covers gaussian, student, lognormal, Gamma, weibull, Beta, inverse.gaussian, skew_normal, shifted_lognormal, huber, von_mises, frechet, gen_extreme_value, exgaussian and zero_inflated_asym_laplace. A prior exempts the model only when it provably bounds the dispersion. gamma() on shape does, and so does a normal() prior on every coefficient of an intercept-free cell-means formula (sigma ~ 0 + f). A student_t() or gamma(2, 1) on sigma does not, and neither does a prior on the slopes of a dispersion formula with an intercept. A prior that bounds the objective does not make the estimate informative: the mode then sits where the prior and the weight excess balance. A dispersion formula that cannot reach its limit at every row (sigma ~ 0 + x, or an offset alone) is not refused: its likelihood is bounded. The check does not run with se(), residual correlation, mi() or several responses. Under cens() it counts the uncensored rows, because a censored row’s probability is at most 1 and its own effect can bring it to 1. Under the Laplace approximation each censored row takes 1/2 off the excess.

  • asym_laplace() with a random effect of its own on every row is refused under the Laplace approximation, at any weights. Its log density is linear on each side of the mode, so the data give such an effect no curvature. The approximate likelihood grew without bound: log-likelihood 449 with sigma at exp(-7.1) on 80 unweighted rows, where the exact integral gives -56.7. quadrature = TRUE integrates the effect exactly and is refused only when the weights sum to more than the number of rows. zero_inflated_asym_laplace() is refused the same way: on its nonzero rows it is asym_laplace (log-likelihood 278.05 on 80 unweighted rows). With cens() both checks count the uncensored rows.

New features

  • The generics of brms’s loo_epred(), loo_linpred(), loo_predict(), loo_predictive_interval(), loo_R2(), loo_model_weights(), kfold(), reloo(), loo_moment_match() and loo_subsample(), with a refusal on a fit. log_lik(), loo() and bayes_R2() refuse a fit the same way. On a fit the six loo_*() functions name frmtmb.sample::frm_sample(). kfold(), reloo() and loo_moment_match() name frm_bootstrap() and AIC(), because neither frmtmb nor frmtmb.sample refits the sampler, and loo_subsample() names the full loo() of draws. Before, with frmtmb alone a call stopped at “could not find function”, and with rstantools, loo or brms attached at “no applicable method”. loo_moment_match(fit) with loo attached stopped inside loo’s default method, at a missing argument loo. frmtmb.sample registers its draws methods on these generics. Each defers to its owner’s generic while the owner is loaded (rstantools; loo for loo_model_weights(), kfold(), loo_moment_match() and loo_subsample(); brms for reloo()). loo_epred() has two owners, brms 2.23.0 and rstantools 2.7.1, and takes the one nearer the front of the search path.

  • cens() and trunc() take 23 more families, the ones brms 2.23.0 censors and truncates: student(), skew_normal(), exgaussian(), shifted_lognormal(), asym_laplace(), zero_inflated_asym_laplace(), von_mises(), Gamma(), Beta(), binomial(), beta_binomial(), negbinomial(), geometric() and brmsfamily("com_poisson"). The others are the zero-inflated poisson, negative binomial, binomial, beta-binomial and beta, and the hurdle poisson, negative binomial, gamma and lognormal. Each family has both tails of its CDF on the log scale on the AD tape, and a censored model with random effects fits. Each tail is computed directly where it is small, so a right-censored row deep in the upper tail keeps its digits and its gradient. Three are the complement of the other tail in part of the range: com_poisson’s survivor above 1e-3, a hurdle family’s CDF where the survivor is below 1/2, and von Mises tails on the near side of the mean. The references are stats, the closed forms, 300-bit Rmpfr arithmetic and adaptive quadrature, over grids that include both tails and the edges of the support. Against them, the relative error of the log of either tail is 1e-12 or better on 554 of 582 comparisons. It is 3.7e-9 or better on the rest, except two negative-binomial far-tail points where stats::pnbinom() is the one in error. The incomplete gamma’s is 8.04e-14 or better for shapes from 0.01 to 1e7.

    • A censored row is brms’s likelihood. At frmtmb’s estimates, the log density of brms’s Stan program with flat priors equals logLik(), given the same data frame. This holds for every family with all four censoring codes, and for truncated fits. Among those are a zero-inflated count under a constant bound and a right-censored count inside trunc(lb = ). The gradient is not asserted for asym_laplace(), whose fit stops at nlminb’s false convergence on these data with or without censoring. Two exceptions are brms’s. Its inverse-gaussian survivor adds a term it should subtract: -0.466 where the survivor is -0.988 at y = 2, mu = 2, shape = 4. So frmtmb’s right-censored inverse.gaussian fit differs from brms’s. Also, Stan’s gradient of the gamma CDF in the shape is -0.18 where finite differences of its own log density give 0.0002. That was measured at frmtmb’s estimates of one censored hurdle_gamma model. compois() takes the mean where brms’s com_poisson() takes the rate, so the two are different models.
    • von_mises() follows Stan and brms: F(y) = P(-pi <= Y <= y).
    • bernoulli(), xbeta() and zero_one_inflated_beta() still refuse both terms, as brms does, and frm_compat() says so.
    • A count right-censored at or above its trials (binomial(), beta_binomial() and their zero-inflated forms), or an interval that starts there, is refused by name: P(Y > n) is zero. brms accepts such a row, its log density is -Inf, and it draws nothing.
  • A family can give both tails of its CDF on the log scale, through the new cdf_log argument of frmtmb_family(). For such a family every censored row and every trunc() window is formed on the log scale from the tail that keeps its digits: an interval as S(y) - S(y2) where S(y) >= 1/2 and as F(y2) - F(y) below. So an interval in an upper tail keeps its digits, and a left-censored row whose probability is below 1e-308 keeps its log. Every built-in family with a CDF declares it except exponential(), weibull(), cox(), frechet(), gen_extreme_value() and discrete_weibull(), which are read through lcdf and lccdf as before. For every family trunc(lb = ) with no upper bound divides by the log survivor at lb, as brms does.

  • fitted() reports the truncated mean of a trunc() response for student(), Gamma(), binomial(), negbinomial() and geometric(), the families brms has one for. A count’s lower bound is inclusive, as in the likelihood. brms 2.23.0 sums from lb + 1, the mean of a different window than its own likelihood uses: for a Poisson mean of 3 truncated to [2, 6] it reports 3.9588 where the truncated distribution’s mean is 3.3869.

  • dirichlet_multinomial(), brms’s overdispersed multinomial. The category probabilities of each row are Dirichlet with mean softmax(mu) and precision phi, and the counts are multinomial given them: brms’s dirichlet_multinomial_logit2_lpmf(). The response, trials(), refcat, K and fitted() are multinomial()’s, and so is the code: the two families are one implementation. phi takes a log link (link_phi) and its own formula. simulate() draws each row’s probabilities from the Dirichlet, then the counts. On 126 rows (two to five categories, phi from 1e-4 to 1e8) the log density is within 9.1e-12 of an exact reference. brms 2.23.0’s Stan function is within 6.1e-7: its lgamma differences lose digits as phi grows, and these are formed so that they do not. That matters because phi grows without bound on data with no overdispersion: on 7 such fits the plain differences stopped above the multinomial limit, 3 with “false convergence”; formed this way, none does. Fit to the same data as brms, with and without a group term, the maximum-likelihood estimates lie within 0.05 posterior sd of brms’s posterior means. This holds for the coefficients and phi.

  • brmsfamily("negbinomial2"), brms’s negative binomial in sigma = 1 / shape. The variance is mu + sigma * mu^2. It is negbinomial() underneath: the same density, simulator, mean, variance, deviance and addition terms, read at shape = 1 / sigma, so anything negbinomial() gains this family gains with it. sigma takes the positive links (link_sigma) and its own formula; with the log link on both, sigma ~ z is the model shape ~ z with its coefficients negated. As in brms there is no exported constructor. Over 384 points with sigma from 1e-8 to 1e6, its log density is within 6e-12 relative of an exact reference. brms 2.23.0’s neg_binomial_2_log_lpmf() is within 1.4e-10.

  • Both families are in frm_compat(), reach every post-fit path of negbinomial() and multinomial(), and take brms’s link sets.

Bug fixes

  • simulate() of a trunc() fit stopped on a valid model when a row kept only a few percent of its fitted mass inside the window: rejection sampling left 1 of 120 rows of a weibull fit with a diag() term unfilled after 100 passes. A row that rejection has not filled after 20 passes is now drawn by inverting its CDF, for each family of this package with one.

  • trunc(lb = ) without an upper bound divided by 1 - F(lb) on the probability scale, which is 0 once F(lb) rounds to one: an infinite likelihood the optimizer could step into. A gaussian fit with its mean far below the bound had an objective of -Inf. A weibull trunc(lb = 0.5) fit with a diag() term stopped at a NaN gradient, and now fits at a log-likelihood of -145.97.

  • An interval-censored row lost its digits above the median and at a point mass: as log(F2 - F1) both terms round toward one in an upper tail, and a hurdle’s (0, y2] was (hu + tiny) - hu. The objective was then rounding noise. At points away from the optimum its gradient was 21 and 45 percent off for skew_normal() and von_mises(), and eight times its size for hurdle_gamma().

  • trunc(lb = -Inf) or trunc(ub = Inf) stopped on a NaN gradient from Inf * 0 in the derivative of the normalizer. An infinite bound is now no bound.

  • The exgaussian() density was a gaussian approximation from sigma / beta of about 20, the one RTMBdist::dexgauss() uses: on 400 gaussian rows the log-likelihood was 0.63 off at sigma / beta = 50, and near-gaussian fits stopped at sigma / beta = 20 with false convergence, below the maximum. The density is now the closed form, through Mills’s ratio, and so is the CDF, whose derivatives were wrong from sigma / beta of about 1e3.

  • inverse.gaussian()’s CDF was NaN once shape / mu passed 355, where exp(2 shape / mu) overflows, so a censored or truncated fit there had a NaN likelihood. Both tails are now formed in logs: at mu = 1, shape = 1000 the CDF at 0.9 is 4.534060403e-04, as statmod::pinvgauss() gives.

  • poisson() and inverse.gaussian() score a right-censored row with the log survivor itself. It was log(1 - F), which is -Inf once F rounds to one. A Poisson row right-censored at 700 with mean 5 now scores -2773.28. A Poisson fit right censored at 75000, which failed at -Inf, reaches the maximum of an independent fit on stats::ppois().

frmtmb 0.72.0

The main changes:

  • brms’s categorical() and multinomial() signatures, refcat, fitted() and posterior_linpred() of every location predictor, and logistic_normal() in mvbf().
  • Per-coefficient sd priors, brms’s labels and order of nested and multi-membership groups, and one Student-t latent scale for the terms of one grouping factor.
  • uniform() and lognormal() priors, and support guards on the tape.
  • Every addition term on newdata, coded as the fit coded it.
  • Accurate standard errors for Cook’s distance, D1 and D2, REML in the design’s coordinates, and bounded Newton steps.
  • The exact Hessian for every fit without random effects.
  • drop1(), step(), update() and influence() refit on the data the fit holds.

Breaking changes

  • multinomial() takes brms’s arguments, (link, refcat, K), and reads its categories from the response matrix. multinomial() with no arguments now fits, as brms spells it; the dpars are named by the columns, cbind(y1, y2, y3) giving muy2 and muy3, as in brms. multinomial(K = 3) still builds the family away from the data, with the positional names mu2, mu3. multinomial(3) is refused: the first argument is the link, and the refusal names K =.

  • A categorical() dpar name is brms’s make_stan_names(). Dots and underscores in a level are dropped, so the level "p.q" gives mupq where it gave mup.q.

  • fitted(scale = "linear") on a family with several location predictors returns brms’s n x 4 x P array. That is every mu<cat> of categorical(), multinomial() and the simplex families, layers eta1, eta2, ..., and every component’s mu<k> of a mixture, layers unnamed. It returned the first predictor’s n x 4 matrix.

  • categorical() takes refcat as its second argument, as brms does; levels and K follow it.

  • Nested and multi-membership groups carry brms’s labels. (x | g1/g2) is (x | g1) + (x | g1:g2), as brms 2.23.0 expands it. The groups are g1 and g1:g2. reformulas wrote g2:g1. The group of (x | mm(m1, m2)) is mmm1m2, whatever its weights. These labels are in set_prior(group = ), default_prior(), prior_summary(), variables() (sd_g1:g2__x), ranef(), coef(), VarCorr(), ngrps(), summary(), re_formula and the new-level messages. The old spellings "g2:g1" and "mm(m1, m2)" are refused, as brms refuses them. A re_formula term now names its group exactly as the fit writes it.

  • The groups of each predictor are in brms’s order, sorted by name (brms’s terms_re()). This order is in variables(), ranef(), VarCorr(), ngrps(), summary() and the order of the theta parameters. (1 | h) + (1 | g) now lists g first. Terms on one group keep their written order.

  • ranef() and coef() name the levels of an interaction group as brms does. The parts are joined with _: p1_q1 for a level of g1:g2 (was p1:q1). A : inside a part stays: c:d_f.

  • variables() lists every group-level standard deviation, then every correlation, then a smooth’s sds_, in brms’s order. Before, each block’s correlations followed its own standard deviations.

  • Student-t terms of one grouping factor share one latent scale per level, as in brms. Examples are `(1 | gr(g, dist = “student”)) + (0

    • x | gr(g, dist = “student”)), and a term inmubeside a term insigma. At each level, their coefficients are one multivariate t. So the first example is the model(1 + x || gr(g, dist = “student”)). Agr(g, by = f)term beside a plain term ongshares the scale at each level ofg. Agr(g, by = f)term alone is the model it was. Terms that share a scale take onedist_nu.simulate(re_formula = )keeps or redraws them together, andquadrature = TRUErefuses them. See?frmtmb-student-re`.
  • A prior density with a bounded support is NaN outside it on the tape too. beta(), gamma(), inv_gamma(), exponential() and lognormal() each add a guard. The guard is NaN outside the support and exactly 0 on it, its edges included as in Stan. beta() and gamma() relied on a log() that the tape drops where it is multiplied by a constant 0. So beta(1, 1) on a coefficient was flat over the whole line, and gamma(1, r) was finite below 0. exponential() had no guard at all and was finite below 0 on doubles too. Inside the support nothing changes.

  • A bounded density on a parameter with no such bound of its own warns, in brms’s words. Examples are uniform() or beta() on a coefficient and gamma() on an intercept. It warns when neither its row nor the class row above it gives lb or ub, as brms 2.23.0 does. The warning comes from the call that uses the prior (frm(), validate_prior(), update(), frmtmb.sample::frm_sample()). Without the bound the parameter is not constrained, and the density is a wall a sampler meets: give lb and ub, which frm_sample() passes to Stan as a transform.

  • predict(newdata =) reads every addition term from newdata, and a binomial-type family refuses a missing trials(). Before, the predictive draws read only trunc(), rate() and thres(gr = ) from newdata, and the simulator was given no other term. A binomial with y | trials(N) and newdata N = 1000 drew 0 or 1 on every draw (column mean 0.555; simulate() and fitted() gave 563). A gaussian with se(s) drew at sd 0.029 where newdata had s = 5. A custom family’s vint() and vreal() and frmtmb.eam’s dec() failed. The defect is in every release from 0.61.0, when predict() became a predictive summary. Now predict() reads the terms as simulate() and fitted() read them: binomial gives 563.3 and 1.08 (brms 2.23.0: 562.7 and 1.15), and se() gives sd 0.059 and 4.77 (brms: 0.061 and 4.95). The same fix reaches the newdata fill of a cov = FALSE ARMA term, and multinomial() predict(newdata =), which was refused, now draws newdata’s trials.

    What changes for a caller: newdata without the trials() or se() variable is refused, naming the term, as brms refuses it. Before, it drew one trial per row or ignored the known standard error. The refusal ends “supply the variable”; it used to offer type = "conditional", which predict() does not take.

  • A binomial-type family no longer reads a missing trials() as one trial. binomial(), zero_inflated_binomial(), beta_binomial() and zero_inflated_beta_binomial() refuse it by name. A model of these families always has the term, so only a caller that drops it meets the refusal.

  • A custom family’s declared required_aterms are checked after the fit too. sim_response() and response_mean() refuse a term list that lacks one, naming the term. Before, the family’s own code met NULL, which gave “invalid arguments” from rbinom() or an empty answer.

  • An addition term coded by its levels is read on newdata through the fit’s codes. This is a factor for any term, and a factor, character or logical value of a registered term such as frmtmb.eam’s dec(). Before, newdata was coded by its own levels. A releveled or character dec() swapped the two boundaries in silence. One value alone (a one-row newdata, or a logical dec() given TRUE) was refused for holding one level. A value the fit did not see is now refused by name. It used to be coded as if it were one of the fit’s. A coercion’s own refusal is reported in its own words, not as “could not be evaluated on newdata”.

  • conditional_effects() holds the variables of thres(), vint() and registered terms at round(median()), as brms holds its whole-valued terms, and says so in a message. They were held at the mean: a vint() count of 9.985 drew NA, and a numeric dec() at 0.573 was no boundary.

  • The conditional_effects() grid refuses a row without its thres(gr = ) group (method = "predict"), where it used to draw the row without it. The default grid holds the group, so the refusal meets only a grid that cannot evaluate it.

  • REML and profile = TRUE fits on an ill-conditioned fixed design are taped in the design’s coordinates. There beta is integrated with the group effects. Its inner Hessian has the design’s condition number squared, and the REML objective lost its digits.

    • On a raw polynomial of degree 7 with (1 | g), the fit stopped short with a convergence warning. Its coefficient standard errors were 1.5 to 1.9 times the exact ones. At degree 8 every standard error was NaN, or the optimizer failed.
    • Now the objective is taped in gamma, with beta = T gamma and T from the design’s QR. Under REML the tape also carries -log |det T|, so logLik(), AIC and BIC are those of beta.
    • At degrees 3 to 7 the REML log-likelihood equals the exact restricted log-likelihood to 6.6e-10. At degrees 5 to 8 the fit reaches the REML optimum to 2e-12. The covariance is within 6.1e-9 of the exact one on the scale of the correlation.
    • The joint covariance is held as a factor, as on an ML fit. Every route that propagates it reads the factor. vcov() warns (class frmtmb_cov_products) where a product of the whole matrix loses its digits, and marginaleffects::get_vcov() returns NaN.
    • frmtmb’s REML vcov() adds the uncertainty of the variance parameters to the conditional covariance, as glmmTMB does. lme4’s vcov() is the conditional part alone. On these designs it is 2.2 to 3.3 percent off its own exact value at degree 7. At degree 8 lme4 stops with an error.
  • A log standard deviation that the optimizer leaves near zero below a small interior maximum now moves. The log-likelihood is flat toward zero in a log sd. So nlminb can stop at an sd near zero and call the stop converged.

    • Take a raw polynomial of degree 7 with (1 | g) and no group effect (seed 4). The stop was at an sd of 1.9e-6. The maximum is at 0.033 times sigma, 1.2e-3 higher. The fit warned, and every coefficient lost its standard error.
    • When a larger sd gains more than grad_tol with the rest held, the optimizer restarts from it. That fit now reaches the exact maximum to 8e-9. Its covariance is within 5.6e-6 of the exact one.
    • Among 80 such fits, three were more than grad_tol short. All three now reach the maximum. One of them had called the sd “flat”.
    • A caller who asks for an earlier stop keeps it. That is an iter.max or eval.max below 1000, a looser rel.tol, x.tol, abs.tol or xf.tol than nlminb’s, or another optimizer. Higher caps and tighter tolerances keep the restart.
  • Newton steps from the stop respect the bounds a prior sets. Every full step that crossed a bound was refused. A step now holds the coordinates it would push out at their bounds.

    • Take an upper bound one standard error below the estimate of a raw polynomial’s top coefficient. On 0.71.0, 10 of 20 fits reached the constrained maximum at degrees 5 to 8. Two of the others stopped 0.12 and 0.60 short with no warning. Their standard errors were 5.8 to 39 times the right ones. Now 17 of the 20 reach it.
    • A parameter on its bound that the Newton step would push out is held by that bound, whatever the sign of its gradient. On an ill-conditioned design that sign is rounding.
    • The other coefficients’ covariance is taken in the coordinates of the design without the held one.
  • Nonlinear formulas take the coordinates of their Jacobian. A nonlinear body has no design, but its Jacobian with respect to the coefficients of its parameters serves as one. This changes the standard errors of such fits.

    • A degree-8 raw polynomial written as a body, one parameter per coefficient, now reaches the maximum of lm(). Its covariance is within 4.7e-6 of the exact one. 0.71.0 stopped 0.047 to 0.41 short and gave no standard errors.
    • A degree-6 raw polynomial beside A * sin(w * z) had a covariance up to 3e-3 from the exact one, with no warning. Now it is within 1.8e-8.
    • The Hessian in these coordinates must be symmetric to 1e-3 of its largest entry. A body that is nearly unidentified along a nonlinear parameter fails that test and keeps the covariance of 0.71.0.
  • Fits with up to 400 outer parameters take the design’s coordinates. The limit was 100. This changes the standard errors of such fits.

    • A degree-7 raw polynomial beside 150 or 292 covariates now reaches the maximum of lm(). Its standard errors are within 5.3e-6. On 0.71.0 these fits stopped 0.5 to 2.2 short, with a warning, and their standard errors were NaN or wrong.
    • Newton steps in natural units still stop at 100 parameters.
  • On an identity-link gaussian design the Hessian in the design’s coordinates costs half the gradients. There the gradient is linear, so a central difference is exact. Every other column keeps the Richardson extrapolation. This moves such fits’ standard errors in their last digits.

    • With 100 outer parameters and n = 1e4, a gaussian fit takes 0.87 of the time it took on 0.71.0. With (1 | g) it takes 0.89 of that time.
    • With n = 1e5, both fits take 0.88 of the time they took on 0.71.0.
    • Non-gaussian and nonlinear fits keep the cost and the accuracy of 0.71.0.
  • Standard errors of a fit without random effects come from the exact Hessian. The covariance was the inverse of central differences of the gradient with an absolute step of 1e-3, the step stats::optimHess() takes. Where that step moves a linear predictor by much and the gradient is not linear along it, the standard errors were wrong and nothing warned. A poisson GLM with x ~ N(500, 100) had standard errors 6.2 percent off. Take ten poisson and bernoulli designs with covariates of very different scales, against an exact covariance formed in 256 bits. The error was up to 21 percent. It is now below 1e-11 on 18 of the 20 fits, and 2.6e-8 on the other two (a covariate on [1000, 1001]). y ~ a * exp(b * x) with x on 20 + [0, 1] had a correlation 0.133 off, and is now within 5e-7. The covariance is now the inverse of the AD Hessian, which the standard-error check already read as exact. Every fit without random effects moves. On 27 ordinary, well-scaled fits the standard errors moved by at most 5.2e-7 relative, and the correlations by at most 1.1e-6. All moved toward the exact values. The fit already holds this Hessian, so the first vcov() no longer takes one: on a bernoulli GLM with 50 coefficients and 1e5 rows it went from 20.4 s to 2.2 s. Under frmtmb_control(check_se = "ignore") the fit does not hold it, and the first vcov() builds it. That is faster than the differences, but it takes more memory. On a bernoulli GLM with 1e4 rows the peak memory rose by 350 MB at 200 coefficients and by 870 MB at 500. The change includes quadrature = TRUE and importance = fits, whose outer objective has no inner random effects, and there it is the largest. Take a poisson y ~ x + (1 | g) fit by importance sampling with x ~ N(500, 100). Its intercept standard error was 0.498. The same model on x / 100, an exact reparameterization, gives 0.270, so the first was 85 percent off. It now gives 0.270 on both. The quadrature fit of the same model was 0.27 percent off.

  • With random effects, the outer Hessian’s step follows the design. Where the root mean square of a coefficient’s design column is above one, the step is 1e-3 divided by it. So a step moves the linear predictor by about 1e-3 in root mean square over the rows. A row far out in a heavy tail moves by more. Take a lognormal column with a root mean square of 50 and a largest value of 836: that row moves by 1.7e-2. The standard errors are within 7e-6 of a Richardson reference, where 0.71.0 was 1.6e-2 off. A poisson GLMM with x ~ N(500, 100) and (1 | g) had standard errors 0.37 percent off and a correlation 0.10 off. Its group standard deviation lost its standard error as “flat”, with a warning. It now keeps it, and the covariance is within 5.3e-7 of a Richardson reference. A design whose columns are all at unit scale or below keeps the steps of 0.71.0, so its covariance does not change. A nonlinear parameter, a mo() term and a variance parameter keep the step of 1e-3.

  • A variance that is zero, negative or not finite is shown as NaN, never as itself. asym_laplace()’s log density is piecewise linear in its location, so its exact Hessian has no curvature there. Inverting it gave standard errors of exactly 0 and confint() intervals of zero width. Such a parameter now has a NaN row and column in vcov(), and summary() and diagnose() give the reason. No asym_laplace() fit has a valid Hessian standard error, in this version or in 0.71.0, whose differences across the kinks gave noise.

  • update() takes what it does not replace from the fit, as brms’s update() does: the data, the formula, the family, the control, the start and the flags. Since update() was added (0.6.0) it evaluated the symbols of the stored call again. So the refit used the objects of that name at that time. With x2 changed in the workspace, update(fit) gave a log-likelihood of -3597.28 for a fit of -3536.91. With 10 rows deleted it had 1990 observations for the fit’s 2000. It stopped with “object ‘d’ not found” (or 'form', 'fam') in three cases. The data were removed, the fit was read back with readRDS() in a new session, or it was made inside a function. A global form of the same name as the call’s argument became the model without a word. The data the fit holds are the model frame and the variables the formula reads through a transform. They are on the rows the fit used, after na.action. The fit’s bases go with them. So update(fit) with nothing changed gives the fit’s log-likelihood, also when na.action removed rows and the formula has splines::ns(), poly() or scale(). Rows that na.action removed stay out of the refit, and under na.exclude the refit pads its residuals to the fit’s rows. The new fit’s call names everything as the stored call did.

  • A variable the fit does not hold is read only from data that are still the fit’s. update(fit, ~ . + z) with a new z reads the stored call’s data, as stats::update() and add1() need, when those data hold the fit’s rows and values. Otherwise it is refused with brms’s words, “New variables found: ‘z’ Please supply your data again via argument ‘newdata’.”, and a reason. It used to read every variable again from the changed object.

  • drop1(), and so step(), and anova() refuse a fit whose prior adds a log density to the objective. Its logLik() is the log-likelihood plus the log prior (since MAP fits, 0.10.0), so the difference of two is not a likelihood ratio: on one probe it was -1.56, with p = 1. A prior that only sets bounds adds no density and is not refused. The message points to a refit without the prior, or to frmtmb.sample’s loo() and bayes_factor(). brms has no drop1() and compares models with those two.

  • drop1() keeps the fit’s bases. A submodel keeps the bases that the fit computed from every row before na.action: the knots of ns(), the coefficients of poly(), and the center and scale of scale(). It keeps their coefficient names too, as stats::drop1() keeps the fit’s design columns. With three missing values in another variable, the likelihood-ratio statistics now equal stats::drop1()’s on lm() and glm(). The AIC of a submodel equals that of update(fit, ~ . - x3).

  • influence() keeps the fit’s bases in every deletion refit, as lme4’s influence.merMod() does. On z ~ poly(x2, 2) + x + (1 | g) the deleted-group coefficients differed from lme4’s by 1.3 times the largest displacement. Each deletion computed the basis again from its own rows (since 0.15.0). They now agree to 4.6e-6 of it.

  • A brms categorical(refcat = "b") object passed as family = keeps its refcat. 0.71.0 dropped it and fitted reference a under the names mub and muc. The same call now fits brms’s parameterization, with brms’s names.

  • validate_prior() and prior_summary() list the class "sd" rows in brms’s order: the class-wide rows, then group by group, with the responses inside each group.

  • conditional_effects(categorical = TRUE) on categorical() draws a Wald band from fitted() by default, where it asked for band = "boot".

  • When the standard-error check removes nothing, a log sd with a small curvature is interior. The covariance is then taken in the design’s coordinates, which moves the standard errors of such fits. Before, solve() of the natural Hessian kept it 1.4e-3 from the exact one at degree 7.

New features

  • categorical(refcat = ) and multinomial(refcat = ) name the reference category, as in brms: NULL is the first, a name must be one of the categories, and NA gives every category a predictor. The dpar names, default_prior() rows and post-fit shapes follow the reference. brmsfamily("categorical", refcat = ) and brmsfamily("multinomial", refcat = ) take it too. Under refcat = NA a maximum-likelihood fit determines only the category probabilities. Its standard-error warning names the common shift of every category’s predictor. A prior on each predictor determines the coefficients. logLik(), AIC(), BIC(), anova() and drop1() count the degrees of freedom of the same model with a reference category, which has the same likelihood. With a category formula of its own the count is not defined: the df is NA, and anova() and drop1() say why. The same holds for dirichlet(refcat = NA). brms 2.23.0’s own fit of that model with its default priors does not converge (R-hat 1.71). A family’s print() shows its reference category.

  • fitted() on multinomial() returns brms’s n x 4 x K array of expected counts, layers P(Y = <category>), with delta-method standard errors; 0.71.0 refused it. frm_linpred(type = "response") gives the n x K counts.

  • logistic_normal() fits inside mvbf(). Its correlations are a block of its response, set with set_prior(class = "lncor", resp = ). The summary reports them as lncor_<resp>(b,c), and variables() and hypothesis() as lncor_<resp>__b__c. <resp> is brms’s Stan name of the response (Ya for Y_a). brms 2.23.0 cannot compile this model; its model block, repaired, agrees with frmtmb’s likelihood at frmtmb’s estimates. dirichlet() in mvbf() worked and is now tested against brms.

  • A class "sd" prior can name one coefficient, as in brms. set_prior("normal(0, 0.5)", class = "sd", group = "g", coef = "x") puts the density on sd_g__x alone. This works in frm(), validate_prior() and frmtmb.sample::frm_sample(). default_prior() lists a row per coefficient below each group row, in brms’s order. A brms prior table with such a row translates. A coef without a group is refused by name. So is a coef on a structure whose columns share one standard deviation (homdiag(), ar1(), homcs()). See “Group-level standard deviations” in ?set_prior.

  • gr(g, id = ) is brms’s correlation id. (1 | gr(g, id = "a")) + (0 + x | gr(g, id = "a")) is (1 | a | g) + (0 + x | a | g).

  • re_formula = ~ diag(x | g) names the term it wraps, as ~ (x | g) does. It named no term before.

  • A refused class "cor" prior names a || or gr(cor = FALSE) term as it was written, (1 + f || g), not as 1 + f | g [diag].

  • set_prior() takes brms’s uniform(lb, ub) and lognormal(mu, sd). frmtmb.sample needs both for brms’s defaults on a non-decision time without a formula and on an ordinal disc under its identity link. The uniform density is NaN outside its interval. That rejects a value there; it does not keep a sampler inside, which lb and ub do.

  • A natural-scale prior on a logit-linked distributional parameter (set_prior("beta(1, 1)", class = "zi"), and frmtmb.sample’s defaults) has a finite Jacobian for every value of its coefficient. Past a coefficient of about 37 it was minus infinity, so a mode there gave an infinite objective.

  • A prior on the nu that the Student-t responses of a set_rescor(TRUE) model share takes no resp, as brms’s row has none: set_prior("gamma(2, 0.1)", class = "nu"). It was refused with and without resp.

  • The sampling API exports ord_lp_blocks(), the threshold blocks of an ordinal model with the dpar class "Intercept" addresses each by.

  • cooks.distance() and the Wald rules of anova() on frm_multiple() fits read the factor of the covariance. These are D1, and D2 with use = "wald". On 0.71.0 they inverted the whole matrix.

    • At degrees 7 and 8 they stopped with “system is computationally singular”. At degree 6 Cook’s distances were up to 5.8e-4 off.
    • Now they are within 2.3e-6 of the exact ones at degree 8.
    • Where the factor’s rows are dependent to working precision, they give NaN with a warning.

Bug fixes

  • cumulative() and hurdle_cumulative() gave a log probability of NaN on doubles for an end category whose neighboring thresholds are tied. The category has a positive probability there. This held under every link with a log-odds form (logit, probit, probit_approx, cloglog). Fits were not affected, because the tape drops the masked term; log_lik() of draws was. The taped value and gradient are unchanged bit for bit at distinct thresholds. A hurdle_cumulative() zero row at such a tie was NaN too, and is log(hu) now.

  • conditional_effects() on multinomial() draws one curve per category, as brms does with categorical = TRUE, and as for the simplex families. dharma_residuals() on it is refused by name.

  • set_prior(resp = ) takes brms’s Stan name of a response (Ya for a response Y_a) as well as frmtmb’s own.

  • confint(parm = ) takes the name of a correlation that is one internal parameter: rescor__y1__y2 of two responses and lncor__b__c of a three-category logistic_normal(). The refusal for a wider block advised hypothesis(method = "profile"), which fails there (“not linear”); it now names hypothesis(class = NULL) and method = "boot".

  • drop1() on categorical(), multinomial(), the simplex families and mixtures was refused (“No linear predictor for dpar ‘mu’”). A dropped term now leaves every location predictor that reads the main formula.

  • A mixture() draws each component at its own rows’ addition terms. The simulator gave each component the whole term list. So the j-th row of a class read the trials() of the j-th row of the data. Take a binomial mixture with N of 2 and 50. In simulate(), predict() and frmtmb.sample’s posterior_predict(), 25% of the draws exceeded N, and the N = 2 rows averaged 11.8 where fitted() says 0.98. mixture(groups =) had the same defect (21%). Present in every release with mixtures of trials families.

  • predict(newdata =) and predict(re_formula =) hand the simulator each distributional parameter on its natural scale. They handed it the reporting scale, which for a mixture’s theta is already the softmax probability, and the simulator applied the softmax again: the mixing weights were wrong on every draw away from the in-sample fast path (443 against fitted() 484 on a binomial mixture at N = 1000). The defect is in every release from 0.61.0 (862032e3), where these draws first read the dpars that way.

  • conditional_effects(method = "predict") codes a registered addition term, such as frmtmb.eam’s dec(), with the coercion its package registered, and thres(gr = ) as the fit’s group codes. A dec() factor reached the simulator as its level codes, so each level was drawn at the other boundary.

  • drop1() refits a term that reads a raw variable. These stopped with “The model uses x2, which is not a column of data” (since 0.49.0): z ~ x + log(x2), scale(x), I(x^2), splines::ns(x2, 3), poly(x, 2) beside another term, x * log(x2), a transformed response, and drop1(scope = ). The refits read the model frame, which holds log(x2) and not x2. The likelihood-ratio statistics and degrees of freedom equal those of stats::drop1() on lm() and glm() and of lme4’s drop1() on lmer(REML = FALSE). brms has no drop1().

  • drop1() refits a model with an interval cens(cc, y2). The upper bound is not a column of the model frame, so every refit stopped (since 0.49.0).

  • drop1() takes the family and the control from the fit. It evaluated them again from the formula’s environment, so family = fam from a function’s frame was “object ‘fam’ not found” (since 0.49.0).

  • step() removes a term, on the fit’s data. It passes its update to update(evaluate = FALSE) as text, "~ . - x3", which update() did not read. So every step that removed a term stopped with “object ‘change’ not found” (since 0.21.0, when drop1() was added). The call it evaluates now holds the fit’s data and formula by value. So step() gives the same model when the data object is changed or removed, or in a new session after readRDS(). Its path equals stats::step()’s on lm() and glm().

  • influence() reads the raw variables by default. Its default data, the model frame, made every deletion refit of a model with a transformed term fail (since 0.15.0).

  • fitted(fit, re_formula = ~ (1 + log(x2) | g)), the fit’s own term, was refused as matching no group-level term (since 0.61.0). make_conditions(fit, "x2") refused x2 of log(x2) (since 0.65.0). Both read the model frame.

  • A fit on a tibble with a transformed term no longer warns “Setting row names on a tibble is deprecated”. A fit on a named list keeps the raw variables a refit reads. Both were defects since 0.66.0.

Documentation

  • ?vcov.frmtmb_fit says what the REML covariance of the coefficients is: the conditional part plus what the variance parameters’ uncertainty adds. It names how that differs from lme4’s.

Internal

  • Bin 2 of the ported brms suite reaches 0.71.0’s families. The thin adapter maps brms’s per-category predictors onto categorical(), dirichlet(), brmsfamily("dirichlet2"), logistic_normal() and multinomial(). It shims brms’s Frechet, generalized extreme value and categorical functions onto frmtmb’s families, and reaches frmtmb.eam’s wiener(). Some of brms’s checks read only the shape of a log density or a CDF. A value check now compares such a value with brms’s own function on the same arguments. No package code changed.

frmtmb 0.71.0

Seven lanes, each with an adversarial review and fix rounds:

  • dblbar: every || term read as brms reads it.
  • famsimplex: brms’s simplex families, dirichlet() and logistic_normal().
  • famuni: frechet(), gen_extreme_value() and discrete_weibull(), and frmtmb.eam’s wiener() links.
  • postfix: the post-processing gaps of the brms port. These are frm_multiple() methods, pp_check(resp = ), and the default-start error of a nonlinear fit.
  • optstart: least-squares starting values for location-scale fits, and a restart that neither gains nor moves.
  • illse: accurate standard errors on ill-conditioned designs.
  • remeasure: the vignette port and bin 2 of brms’s suite, measured, with no package code.

Each lane’s dev/<lane>-findings.md has the validation, the numbers and the scripts, and dev/reviews/2026-10-13-<lane>.md has the review. dev/round-20261014.md is the round’s record.

Breaking changes

  • Every (x || g) term is read as brms reads it. It is (x | gr(g, cor = FALSE)): one term, coded with the formula’s own contrasts. Each column has its own standard deviation, and there is no correlation.

  • A factor in a || term has the columns of the | term. (1 + f || g) with three levels of f now has sd_g__Intercept, sd_g__fb and sd_g__fc. It had lme4’s column for every level (fa, fb and fc beside the intercept). That is one more standard deviation than brms has, and a different model. (1 + z + f || g) had 5 standard deviations and now has brms’s 4. Fits of a factor in a || term change. This reverses the earlier reading that (f || g) gives a column per level. lme4 still expands || term by term (lme4#818); frmtmb follows brms.

  • The random-effect design of 29 || spellings is brms 2.23.0’s, column for column (standata()). The sd_ names are brms’s, and there is no cor_. The spellings cover numeric and factor terms, interactions, poly(), gr(by =), g1:g2, g1/g2 and mm(). They also cover |ID| terms beside || terms, sigma, nonlinear parameters, mo(), cmc = FALSE and mvbf(). Their sd rows of default_prior() are brms’s too. The exception is the group label of a nested or multi-membership term, which differs for | terms too.

  • A || term is one block. (1 + x || g) was two blocks, 1 | g and 0 + x | g. It is now one block with independent standard deviations, as in brms. So ranef(fit)$g and VarCorr(fit)$g address it by the factor name. For gaussian group-level effects the likelihood is the same. The estimates of a numeric (1 + x || g) agree with the old fit to 1.5e-12 relative.

  • A || term over gr(g, dist = "student") has one latent scale per level, shared by all its columns, as in brms. The old split gave each piece its own scale, which is a different model. So these fits change, numeric terms included. On one data set, (1 + x || gr(g, dist = "student")) moved from logLik -538.612 to -539.121.

  • An uncorrelated block names no correlation. brms has no cor_ parameter for such a block. frmtmb named one, fixed at zero, for each pair of columns of a diag() or homdiag() block. It also did so for the diag_t block of a || term over gr(dist = "student"). variables(), summary(), confint_varcorr() and hypothesis() list none now. This includes hypothesis() on frmtmb.sample’s draws.

  • A constant on a location of a family with several locations is refused. This covers categorical(), multinomial(), mixture() and the new simplex families. It also covers the extension families with several locations: frmtmb.eam’s lba() and rdm() (v<k>), and frmtmb.latent’s hmm() (mu<k>) and lca() (theta<k>). bf(f ~ x, muc = 0) and bf(y ~ x, mu2 = 0) used to fit, and the location was estimated anyway. The constant had no effect, so the fit was not the model the user wrote. brms 2.23.0 ignores the constant the same way.

  • The refusal gives two routes. muc ~ 1 estimates the location. muc ~ 0 + offset(o), with o a data column, holds it at a known value.

  • fitted() and residuals() of a fit accept brms’s ndraws. In brms it sets how many posterior draws the summary averages. That is the Monte Carlo error of the answer, not the quantity answered. A maximum-likelihood fit answers that quantity at its estimate, with no Monte Carlo error. So ndraws changes nothing, and a message says so; it was refused. draw_ids, sort, summary = FALSE and robust = TRUE ask for particular draws and stay refused. The setup lines of brms’s ported rows brmsfit-methods:314 and :317 now run (dev/postfix-findings.md, item f).

  • A location-scale fit starts mu at least squares and its scale at the spread that mu leaves. Before, mu started at an intercept read off the response, and the scale started at the response’s own spread (sd(y) for gaussian()). Where a covariate explains most of the response, that spread is far larger than the residual one, and a fit could stop short in silence. For example, take y = 1 + 1e4 z1 + b_g + e with n = 101000 and 50 columns. That fit stopped 9107.66 log-likelihood units below lme4 at code 0. Its group sd was at its boundary.

    Now the coefficients of mu start at a least-squares fit on its fixed design. The fit is weighted when the response has weights(), is on the link scale, and takes the offset out. The scale starts at that fit’s residuals, in the family’s own convention:

    • sigma: gaussian(), lognormal() and shifted_lognormal() and hurdle_lognormal() (on the log scale), student() (from the median absolute deviation at nu = 4), skew_normal(), asym_laplace(), zero_inflated_asym_laplace() and huber();
    • sigma and beta: exgaussian(), each at half the residual spread, as the family’s initializer gives each half of sd(y);
    • shape: Gamma(link = “log”), weibull() and negbinomial();
    • precision: Beta().

    The rule does not apply to a family with no scale or not in that list. It does not apply to another link of mu, or to an intercept-only or nonlinear mu. It does not apply to a mu with a smooth or a gp() term. It does not apply to a design whose condition number is above 1e5, or to a mixture. It does not apply to a censored, truncated or se() response, or to a scale held at a constant. There, mu and the scale start as before, bit for bit. The group sds and the units can still differ from 0.70.0’s, because the units of an mi() response and of zero_inflated_asym_laplace() changed (below). A start or a prior does not turn the rule off. The entries start names are the user’s, and the others keep these defaults. The function par_template() shows the start.

    The change was measured on 550 fits of 11 families (dev/optstart-findings.md). Fits short of the maximum went from 108 to 69. Warned fits went from 129 to 91. On high-R^2 and offset designs, student(), skew_normal() and exgaussian() stopped short on 8 to 10 of 10 fits each. They stopped up to 1370 log-likelihood units short. Now none of them stops short. Other changes that a user can see:

    • The gr(g, by = f) fixture of test-se-check.R now reaches its boundary, 1.44e-4 higher. It warns “singular convergence (7)”. 0.70.0 stopped short there, and warned that two standard errors are not available.
    • Fit a negative binomial to Poisson data, and the shape’s maximum is at infinity. Such a fit ends at “false convergence (8)” at the maximum on 4 of 10 seeds. On 0.70.0 it did so on 2 of 10.
    • A gaussian fit capped by iter.max no longer stops short: from the least-squares start it is at its maximum already.
    • exgaussian() with a group sd 30 times sigma still ends at a NaN gradient on 5 of 10 fits. That is as on 0.70.0, and 4 of the 5 seeds are the same. From the true values, with the group effects started at 0, the density fails on 10 of 10. From the true values and the true group effects, it fails on 3 of 10.
  • A restart from the optimum no longer turns a converged fit into a warning. Where the first run converged, the restart from it may stop at a nonzero code without moving. The fit then keeps the first run’s code, iterations and evaluations, if two things hold. The restart’s gain is within the optimizer’s relative function tolerance of the objective. And no coordinate moved by more than a relative step tolerance. The function tolerance is nlminb’s rel.tol, or L-BFGS-B’s factr times machine epsilon for optimizer = "optim". The step tolerance is nlminb’s x.tol (1.5e-8 by default); for optimizer = "optim" it is 1.5e-8. The restart’s own code, message, iterations and evaluations are kept in fit$opt$restart, and diagnose() shows them. Before, the restart’s “false convergence (8)” replaced the converged code and warned. This happened at lme4’s maximum on 2 of 20 fits with a group sd 1000 times the residual sd. It also happened on brms_missings’ fit_imp2.

  • The unit of zero_inflated_asym_laplace() is measured on its nonzero rows. A weighted response’s unit is measured on the residuals of the weighted least-squares fit. An mi() response’s unit leaves out its imputed rows.

  • Standard errors on an ill-conditioned fixed design. A raw polynomial of high degree, a covariate far from zero or a near-collinear pair can make the outer Hessian too ill-conditioned. Then it cannot be inverted in the coefficients’ own coordinates. frmtmb now takes the Hessian by differences of the gradient along new coordinates. These are the columns of the inverse of each design’s QR factor. In these coordinates the Hessian is close to the identity. The covariance is then mapped back exactly, as lm() does through its QR. This applies when the Hessian has a condition number above 1e8 at unit diagonal. Its weak directions must also lie in the coefficients of the linear predictors.

    • The designs measured: raw polynomials of degree 5 to 8 on [1, 2]. The families are gaussian with and without (1 | g), bernoulli, poisson and cumulative. Every entry of vcov(full = TRUE) is within 2e-5 of the exact covariance at the fit’s own point. That is on the scale of the correlation, against a 256-bit reference. fitted() standard errors agree with the exact ones to four digits. Where a reference uses poly(), they agree with it to four digits too.
    • On the same designs, 0.70.0 gave no standard error to the nine coefficients of a degree-8 polynomial (“the design is ill-conditioned”). With (1 | g) under bernoulli, poisson and cumulative it lost three to six of them as “flat”. At degrees 6 and 7 it was up to 20 percent off. It gave the category probabilities of a cumulative fit standard errors up to 270000 times the true ones. It gave no warning.
    • These fits keep the verdicts of 0.70.0. A design that its QR finds rank deficient is one; the frame drops such a column, as lm() does at degree 9. A nonlinear formula is another. So are a fit that stopped short of the maximum, and a fit with more than 100 outer parameters.
    • Some parameters outside the designs have almost no curvature. A group standard deviation at zero is an example. The SE check judges such a parameter as on 0.70.0: “boundary”, “short of the maximum” or “not finite”. When the check removes it, the covariance of the other parameters is taken in the design’s coordinates. That covariance is conditional on the removed parameter.
    • The test set: raw polynomials of degree 5 to 8 with (1 | g) and no group effect, 20 seeds each. The sd gets the verdict of 0.70.0 on 77 of the 80 fits. On the 16 degree-8 fits with the sd at zero, the coefficients get the standard errors of lm() to 3e-5. 0.70.0 gave those 16 fits no standard error for any coefficient.
    • The extra cost is about two Hessians in the design’s coordinates, at 4 p gradients each. One is for a step from the stop. The other is for the standard errors at the maximum. With n = 1e5 and 100 parameters, a fit took 1.45 to 1.66 times as long as on 0.70.0 in two runs. With (1 | g) it took 1.47 to 1.52 times as long. On 0.70.0 these fits stopped short of the maximum. Now they reach it.
  • Every route that propagates the covariance reads it through a factor on such a fit. No double-precision matrix can be multiplied by a row of that design without losing every digit. At degree 8, a' V a from the exact covariance rounded to double is 2.8 times the true variance of a fitted value.

    • These routes use the factor: fitted(), predict() and its parameter draws, frm_linpred(), and the ordinal and categorical category probabilities. So do conditional_effects(), conditional_smooths(), hypothesis(), VarCorr() and the pooled hypotheses of frm_multiple(). emmeans now takes the grid route on these fits, which also uses it.
    • frm_lp_basis() returns the factor as V_root and V_rest, with V = V_rest + V_root V_root'. frm_joint_cov() returns it as root and Vrest.
    • vcov_cluster() forms the sandwich through the factor. It returns the factor of the sandwich as the attribute root, and hypothesis(vcov = ) reads it. On a degree-8 fit with (1 | g), the sandwich from the whole bread was off by 2.3 to 12 on the scale of the correlation. A fitted value’s standard error through it was 0 to 1511 times the exact one. Through the factor every entry is within 2e-5, and that standard error is within 1e-6.
    • These routes read the whole matrix: a product that a caller forms from vcov() or vcov_cluster(), car and lmtest. They are not accurate on such a fit. cooks.distance() and the D1 and D2 tests of anova() on frm_multiple() fits invert the whole matrix. They can stop with an error.
    • A product formed from the whole matrix can keep fewer than about four digits. That happens at a condition number above 1e12. Then vcov(), vcov_cluster() and frm_joint_cov() warn once per fit (class frmtmb_cov_products). The warning names the routes that are right.
    • marginaleffects forms J V J' itself from get_vcov(), with J from finite differences. On such a fit the cancellation in that product multiplies the error of J. On a binomial with mixed scales (condition number 4e11), predictions() standard errors were 4.8 to 15.1 times the exact ones, with no warning. So get_vcov() now returns NaN, with a warning, on every fit whose covariance is held as a factor.
    • ggeffects, modelbased and parameters read insight::get_varcov(). It returns NaN, with a warning, where a product loses its digits.
    • hypothesis(vcov = ) can get a matrix that does not carry its factor. Then each hypothesis that combines parameters gets NaN, with a warning.
  • Newton steps from the stop and the gradient verdict use the same coordinates. On these designs the natural Hessian has no Cholesky factor. So a degree-8 fit stopped up to 1.2 below the maximum. Its gradient warning measured the remaining headroom from a matrix with no digits along the weakest direction. Both now use the design’s coordinates, and the degree-8 fits reach the maximum of lm().

    • A log standard deviation whose Hessian row is empty, such as a group sd at zero, cannot show its headroom through the Hessian. So the headroom asks the likelihood what larger values of that sd gain, the rest held. That gain is added to the headroom of the other parameters, and only then compared with grad_tol.
  • The standard-error check judges a fit at its maximum whatever nlminb’s code. It skipped every fit with a convergence warning. It showed the standard errors of sdreport() unchecked. So on a ridge, rounding decided whether a fit’s standard errors were judged. Take the xbeta fit of lane ci691 from 25 starts a few ulps apart. The 8 fits with code 0 lost all three standard errors as flat. The 17 fits at “false convergence” showed finite ones.

    • The “did not report convergence” warning now says what a Newton step from the stop gains. Within grad_tol, the estimates are at the maximum. The check judges them as it judges a converged fit, with the same messages. 23 of the 25 xbeta fits now lose the three standard errors. The two that keep them stopped where the curvature is above the check’s threshold, whatever their code.
    • Above grad_tol, the warning says that the estimates and standard errors are those of a point short of the maximum. The check does not judge them, because a curvature verdict there is not about the data.
    • The eight test suites have 129 fits that nlminb did not call converged. A restart from the stop showed 104 of them at the maximum.
    • Newton steps in the design’s coordinates also start from such a stop. Take y ~ x with x on [1e5, 1e5 + 1]. It stopped at “false convergence” 157 below the log-likelihood of lm(). Now it reaches it.

New features

  • gr(g, cor = FALSE) is read as in brms: it is the || term. mm(g1, g2, cor = FALSE) is the || term over the membership grouping. cor = TRUE is the default. Under ||, a cor argument is ignored, as brms ignores it. gr(cor = ) and mm(cor = ) were refused.

  • dirichlet() and logistic_normal(), brms’s simplex families. The response is an n x K matrix of proportions. Write it as cbind(y1, y2, y3) or as a matrix column of the data. Each row must sum to one, and the column names are the categories.

  • The first category is the reference unless refcat names another, as in brms. Each other category k gets its own predictor mu<k>. The main formula applies to every category. bf(Y ~ x, muc ~ z) gives one category its own formula. (1 | ID | g) correlates the group effects across categories.

  • dirichlet() has the precision phi. logistic_normal() has one sigma<k> per category, and each takes a formula. It also has one correlation matrix, lncor. The summary reports it as lncor(b,c), and the new prior class "lncor" takes an lkj() prior.

  • The log densities agree with brms 2.23.0’s Stan functions to a relative 1.5e-11 on 7200 grid points. brms’s Stan program at frmtmb’s estimates gives the same log density to 6e-13 on six models. 500 simulated fits recover their parameters with no warning (dev/famsimplex-findings.md).

  • brmsfamily("dirichlet2") reaches brms’s unexported Dirichlet family, as in brms. It has one log-link predictor per category for that category’s concentration.

  • fitted() on a dirichlet() fit returns brms’s n x 4 x K array of category means, with layers named P(Y = k). predict(), simulate(), posterior_predict() and log_lik() work on both families. Their draws are rows on the simplex.

  • conditional_effects() draws one curve per category, with a Wald band from fitted(). As in brms, fitted(), conditional_effects() and posterior_epred() refuse logistic_normal(), whose mean has no closed form. residuals(), dharma_residuals() and pp_check() refuse both families.

  • The response check holds each row to 1.5e-8 of one. brms tests the MEAN departure over the rows, so one row off the simplex passes there in a large data set. dirichlet() then fails at sampling, and logistic_normal() fits a row that is not a composition.

  • Three brms families that frm() refused: frechet(), gen_extreme_value() and discrete_weibull(), with brms 2.23.0’s parameters, links, link options and defaults (see ?frmtmb-extreme-families). They take random effects, distributional formulas, weights(), cens() and trunc(), and they can be components of a mixture(), as in brms. fitted(), predict(), simulate(), residuals() (including type = "pearson" and "osa"), conditional_effects(), pp_check() and default_prior() cover them. Their log densities agree with a 256-bit reference to about 1e-13 on a grid that includes the far tails. They agree with brms’s Stan functions where those are accurate. brms’s are not everywhere. Its Frechet log survivor is off by up to 3.8e-3 and is -Inf in the far right tail. Its GEV density loses digits near xi = 0, 6.8e-8 at xi = 1e-7 (dev/famuni-findings.md).

    • frechet(): mu is the mean, as in brms, and the scale is mu / gamma(1 - 1 / nu). Under link_nu = "identity" a nu at or below 1 has no likelihood.
    • gen_extreme_value(): the support depends on the parameters, and the density is 0 outside it, with a log that falls steeply rather than a NaN. The optimizer is turned back, and in a mixture() a component that does not reach a row adds nothing to it. brms’s density is NaN there. xi = 0 is the Gumbel model, exactly, so bf(y ~ x, xi = 0) fits one. fitted() returns Inf where the mean is infinite (xi >= 1). brms’s posterior_epred() returns NaN at xi = 0 and xi = 1 and a finite, wrong number above 1.
    • An xi without a formula takes the log1p link, which brms uses for a predicted xi. brms scales such an xi into the support the data leave. That is the same maximum, but its range reaches below xi = -1, where the likelihood is unbounded. Under link_xi = "identity", xi <= -1 has no likelihood, so every link holds xi above -1. A fit that stops within 1e-3 of -1 with the likelihood not stationary in xi there warns that its log-likelihood is a supremum. A maximum inside that band gets a message instead.
    • discrete_weibull(): fitted() is the mean to about 1e-12, where brms’s posterior_epred() stops the series at the first term below 0.001. cens() follows frmtmb’s inclusive discrete convention. So right and interval censoring differ from brms, which reads a right bound as P(Y > y). Subtract one from those bounds to reproduce a brms fit. Left censoring and trunc() agree with brms.
  • family = "wiener" (or a brms wiener() object) takes the family from frmtmb.eam when that package is loaded. Otherwise it is refused with a message that names frmtmb.eam. Before, it was refused as an unsupported family, with a list of the core families. A brms object keeps its links, so brms::wiener() fits brms’s log link for ndt (see ?frmtmb.eam::wiener).

  • For frmtmb.sample, a sampling chart for a scalar GEV xi (gev_xi_chart_frame() and its companions, ?frmtmb-gev-xi-chart), so that frm_sample() samples it through brms’s tmp_xi.

  • A frm_multiple() result has vcov(), fitted(), coef(), ranef(), VarCorr(), nobs(), formula(), family() and an emmeans basis. Each pools the imputations by Rubin’s rules, where brms’s brm_multiple() combines their draws. The test case is a poisson model on 5 mice imputations, against glm() pooled by mice::pool(). The largest relative differences are 1.4e-5 in fixef(), 8.5e-6 in vcov() and 3.1e-5 in the pooled degrees of freedom (dev/postfix-validate.R).

  • fitted() of a frm_multiple() result pools on the link scale, as its conditional_effects() does, and transforms back. Its interval therefore stays inside the response’s range. On the poisson case it agrees with mice::pool.scalar() of glm()‘s linear predictor to 7.5e-6. A category probability takes only its interval from the logit scale. Its estimate is the mean of the imputations’ probabilities, so the categories sum to 1.

  • emmeans() of a frm_multiple() result is emmeans’s own pooling of a mice::mira. It agrees with that pooling of glm() fits to 4.4e-5 in the means and 4.3e-4 in the degrees of freedom.

  • ranef() and coef() of a frm_multiple() result average the modes and coefficients of each level. ranef(condVar = TRUE) gives Rubin’s total variance. VarCorr() pools on the log and Fisher-z scales of the pooled table, and its estimates match that table. Before, coef(), fitted() and residuals() returned NULL in silence (stats’ default methods).

  • pp_check(resp = ) checks one response of a multivariate fit. It compares that response’s observed values with its own simulated replicates, on its own rows, as brms does. A subset() response uses the rows of its subset. A missing, plural or unknown resp is refused in brms 2.23.0’s words. Every multivariate fit was refused.

  • The sampling API (?frmtmb-sampling-api) exports ce_method(), ce_predict_family_check(), ce_sim_one() and pp_check_resp(). ce_plan_eval() takes one =. frmtmb.sample reads them for conditional_effects(method = "predict") and pp_check(resp = ) on draws.

Bug fixes

  • (x | p || g) and (x || p | g) are refused in brms’s words. The second was read as a grouping variable p. (x | p | gr(g, cor = FALSE)) is refused too; brms drops the key there without a word.

  • A known-covariance term with several columns and any structure other than the default is refused. This covers gr(g, cov = A) and gr(g, prec = Q), and a factor in a || term over one of them. The known-covariance block correlates the columns of a term within a level, so such a term silently became a correlated block. A numeric (1 + x || gr(g, cov = A)) is unchanged. So is a one-column term in any structure, such as homdiag(0 + x | gr(g, cov = A)).

  • The message about a duplicated group-level effect names a || term as written, not as the diag() term it is rewritten to.

  • A family with several locations takes a formula for the first one too. bf(f ~ x, mub ~ z) on a categorical() response was refused when b is the first predicted level. So was bf(y ~ 1, mu1 ~ x, mu2 ~ x) on a mixture(). brms 2.23.0 builds the mixture form and the same first-category formula on dirichlet(). On categorical(), brms stops with an internal error.

  • A brms family object passed as family = keeps its refcat.

  • The pooled conditional_effects() of an ordinal frm_multiple() result has categories that sum to 1. Each estimate is the mean of the imputations’ probabilities; it was the inverse logit of their mean logit. The band still comes from the logit scale.

  • The error of a nonlinear fit that fails from its default start names this model’s nonlinear parameters. It gives the two ways to give starts: start = with the names of par_template(), or a located prior. It says that frmtmb offers no start of its own, because a searched start can converge at a local maximum with no warning. Its example named ult, a parameter of another model.

  • A frm_multiple() result refuses by name the methods that have no Rubin’s-rules meaning. They are predict(), residuals(), pp_check(), logLik() (and so AIC() and BIC()), loo(), waic(), update() and model.frame(). Each says why, and names anova() or one imputation’s fit, x$fits[[j]].

  • frm_multiple() refuses imputations whose fits have different coefficients, and names them. It stopped inside vapply() with “values must be length 4”.

  • summary(fit, waic = FALSE) is accepted. It asks for nothing, and brms 2.23.0 prints it as it prints summary(). The refusal of waic = TRUE stays, with its reason.

  • frm(threshold = ) is refused by name. The refusal names the family argument the threshold structure belongs to, as family = sratio(threshold = "equidistant"). It reads the family from family = or from bf() + family(). brms before 2.0.0 read the argument in brm(), and brms 2.23.0 refuses it too. It was R’s “unused argument”.

  • The standard errors of an mi() fit are right again. This is lane remeasure’s R1. On brms_missings’ bf(bmi | mi() ~ age * mi(chl)) + bf(chl | mi() ~ age), 0.70.0 reported “false convergence (8)” at 0.67.0’s optimum, with every standard error NaN. With mi(se) it withheld both residual-SD standard errors as flat and gave the rest up to 35% too large. There were two causes. First, the coefficient of a mi() or me() term was stepped in the response’s units. Its unit is the response’s over the spread of the variable the term stands for. Second, the response units read an imputed value, stored as 0, as data. Now the standard errors of both fits agree with a closed-form reference to 2.1e-6 and 6.0e-7 relative. In that reference each row’s pair is bivariate normal, with the missing values integrated out.

  • A finite-difference outer Hessian that does not factor is retaken along the coordinates whose step was too large. A step is too large when the objective rises by more than 1e-2 over it. Those columns are taken again at the step that gives a rise of 1e-4. The retake is kept when it factors and its smallest eigenvalue is above its own noise. A Hessian that factors is unchanged, bit for bit.

  • A mo() simplex split by a category that no row is in loses the split’s standard errors on every fit at its maximum. The two steps around such a category enter only as their sum. That is an exact flat direction, curved in the simplex’s chart. The SE check read it off the Hessian. It missed it where the fit stopped a tolerance away from the floor of that ridge. The 0.70.0 consolidation had four designs and 160 fits. 8 of them kept zeta standard errors up to 949, with no warning. The split is now taken from the data, as a design alias is. None of the 160 fits is silent. 156 name the split. The other 4 stop short of the maximum with a large-gradient warning, as on 0.70.0.

Documentation

  • ?brmsfamily and ?frmtmb-families (“Conway-Maxwell-Poisson counts”) say that the mu of compois(), which brmsfamily("com_poisson") gives, is the mean of the count. brms 2.23.0 fits mu as the rate of the density, so the coefficients of the two fits are on different scales unless nu = 1 (dev/remeasure-findings.md, R6).

frmtmb 0.70.1

The macOS check of 0.70.0 failed where the Ubuntu and Windows ones passed. dev/ci701-findings.md has the causes and the numbers.

Bug fixes

  • confint_varcorr() keeps the sd intervals of a block where one parameter has no standard error. Before, the NaN row of that parameter went into every row of its block. So a correlation at +/-1, or one sd at zero, removed the other sd intervals. VarCorr() still reported their standard errors (8 of 20 fits of y ~ x + (1 + x | g) with no slope variance). The delta method now reads the covariance as VarCorr() reads it. Only a row that moves with the lost parameter is NA.

  • The standard-error check does not call identified standard deviations flat beside a correlation at +/-1. This applies to an unstructured block of dimension 2. Its correlation parameter can run past the clamp of confint_varcorr() (a correlation of 0.9999). There the finite-difference Hessian is noise. On a (1 | q | id) block, a fit that stopped with code 0 at t = 2000 lost both sds as flat. Now the check first removes such a correlation, as a boundary, if the likelihood no longer changes along it. Then it takes the Hessian of the other parameters with the correlation held at the clamp. The sds then get the standard errors of the rank-one model that the boundary corresponds to, to 2e-4. The same rule runs on a code-1 fit that se_boundary_stop() reads as a boundary stop.

  • On other fits with such a correlation, the standard errors move toward an independent reference. On 6 of 20 fits of y ~ x + (1 + x | g) with no slope variance, the largest relative error was 7.6e-4 to 5.3e-3 in 0.70.0, and is 1.2e-4 to 2.8e-4 now. The largest move is 0.55 percent.

  • A test asked confint_varcorr() to warn about a boundary. On some platforms the fit’s own boundary message names it instead. Another test did not allow “singular convergence (7)” on a degenerate ordinal mixture. nlminb reports it in 7 of 20 fits on ulp-moved data, at the same log-likelihood. Both tests now ask for what holds on every platform.

frmtmb 0.70.0

Three lanes, each with an adversarial review and punch rounds: moparity (brms features that were refused or diverged: mo() crossed with a factor and in group-level terms, Dirichlet priors on a simplex, update(prior =) merging as brms merges, the cox() baseline), optrap (fits that stopped short of the maximum because of the response’s units, a restart that raised, or a badly conditioned design) and seflags (which standard errors a fit reports: an exact design alias, curved ridges, an sd the likelihood does not read, raw polynomials). Each lane’s dev/<lane>-findings.md has the validation, the numbers and the scripts, and dev/reviews/2026-10-08-<lane>.md has the review; dev/round-20261013.md is the round’s record. In brms’s own ported suite, bin 1 passes 389 of 494 assertions (390 at 0.69.0): brmsfit-methods:326 asks a fixture that does not converge for finite standard errors, and it held at 0.69.0 by the luck of where the optimizer stopped.

Breaking changes

  • A factor, character or logical multiplier of mo() is coded as brms codes it, where it was refused. mo(x) * f with f of levels a, b, c fits mox, mox:fb and mox:fc, each with its own simplex and coefficient, and mo(x):f fits mox:fa, mox:fb and mox:fc: the columns of brms’s special-term model matrix. A logical multiplier is mox:xTRUE, as in brms, where it was mox:x. A label’s factors are named as brms names them, in the order model.matrix() reads the special terms alone, so z + mo(x):z is z:mox (it was mox:z) while z * mo(x) stays mox:z, and mo(x) * z * w fits, with mox:z:w, where it was refused. brms 2.23.0 writes wrong Stan code for these terms (every column multiplied by the first covariate) and stops on a factor of three or more levels crossed with the main term (brms#1828); frmtmb fits the model brms 2.22.0 fits. Against brms’s compiled program for the same model written with numeric indicators, the log density agrees with frmtmb’s at its estimates (to 1e-6 relative, after brms’s flat Dirichlet constants), with brms’s gradient below 1e-3 there.

  • update(prior = ) merges the new prior into the stored one, as brms’s update() does: a new specification replaces the stored one for its slot (class, coef, group, resp, dpar, nlpar), and the stored specifications for every other slot stay. It replaced the stored prior, so a stored normal(0, 0.05) on b_x was lost by update(fit, prior = <a sigma prior>). The same holds for a brmsprior and for a table from default_prior() or brms’s get_prior(), where every row takes its slot, as in brms: a row with an empty prior returns its slot to the default (flat), so an unedited row of a whole table resets the stored prior of its slot. prior = NULL, or a prior with no rows, keeps the stored prior, as in brms. A stored specification the updated model cannot take is still dropped with a message; a NEW one is still checked as frm() checks it, where brms drops it silently.

  • cox() holds its baseline simplex in the chart of a mo() simplex, not a softmax, and frmtmb_control(mo_search = ) checks a weight at 0 too. Of 200 fits of a right-censored model, 5 stopped at “singular convergence (7)” with weights near 1e-50 and a warning, and one 1.86e-4 below the maximum; now 200 of 200 are within 1.8e-8 of the maximum, at code 0, with no warning, for 1.05 times the objective evaluations. The sbhaz_raw coordinates have other values; cox_baseline() reads the weights as before.

  • A gaussian, student, lognormal or other location-scale fit reaches the maximum likelihood whatever the units of its response, and the fit of y * k is the fit of y scaled, to about 1e-7 in the log-likelihood. The identity-link mu of these families now has a unit: the spread (the MAD) of the response after the fixed-effect design is taken out by least squares, on evenly spaced rows for a design of more than 5e6 cells, rounded to a power of two. The standard deviations of its random-effect terms start at that unit (they started at 1 in the response’s units), nlminb steps its coefficients in the unit over each column’s spread, and the inner solver’s gradient tolerance is divided by it. par_template() shows the new starts. Before, with y in units 1e3 times larger, 20 of 40 random-intercept fits stopped short of lme4’s maximum at optimizer code 0 and without a warning, by up to 0.67 log-likelihood units; with y in units 1e6 times larger, 40 of 40 sleepstudy-like random-slope fits, by up to 127, and 10 of 10 plain y ~ x fits, by up to 23. Now every one of these fits over units from 1e-6 to 1e6 is within 2.1e-7 of lme4’s or lm()’s maximum, at code 0. A response whose residual spread is between 0.71 and 1.41 has a unit of 1 and fits as before. Where it has a unit, the estimates agree with lm() and lme4 to about 1e-5 of a standard error (at most 8.5e-5 over 160 y ~ x + z fits; before, 2e-7 to 1.2e-5 at moderate units and 2.8 standard errors with y in units 1e6 times larger), because nlminb’s stopping tests now read steps in the unit. Mixtures, extension families and a sigma held at a constant (se()) keep their starts.

  • An alias in a linear predictor’s design is tested exactly. A smooth’s fixed column joins the design after the check that drops aliased columns; on brms’s example fit 1 it is exactly -0.046 + 0.190 * Age. The standard-error check found the alias only to the precision of the Hessian, so under OpenBLAS’s LAPACK all five predictions of fitted(newdata) got NaN and a warning though they are estimable, and a hypothesis 12 percent off the alias got a finite standard error with no warning. The check now takes such an alias from the design itself: every coefficient it moves (on fit 1 the intercept too) has NaN as its standard error, and a prediction or hypothesis has one exactly when it does not move along the alias. The prediction test reads every direction in the unit-diagonal metric of the Hessian, so its verdict does not depend on the units the optimizer works in. On a mixture whose mixing weight has run to zero, a prediction that reads the vanished component’s flat parameters now gets NaN and the warning.

  • Curved ridges lose their standard errors. A fit that stops within its tolerance of a ridge along which the likelihood is flat on a curve (a + exp(b), a * b, c0 + exp(a)^k with a group effect, a mo() simplex with an unobserved category, two group terms on the same factor) has a small eigenvalue along the ridge that is not curvature of the likelihood, and the check kept every standard error with no warning: on 9 of 20 seeded a + exp(b) fits, 20 of 20 with (1 | g) and 16 of 18 c0k fits. The check now measures the curvature again on the ridge’s floor, takes out the part that comes from the step leaving a curved ridge, and removes the direction when what is left is flat. Only a direction that involves a parameter outside a family’s primary linear predictors is asked, since a generalized linear model has no curved ridge in its coefficients.

New features

  • A group-level term can vary a monotonic coefficient, as in brms: y ~ mo(x) + (mo(x) | g) adds r_g[, mox] to bsp_mox and multiplies the sum by the term’s transform, on its simplex. The population-level column is required; brms 2.23.0 drops (mo(x) | g) without one silently, and frmtmb refuses it in brms’s words. It is available in (... | g) and (... || g) terms, with predictions, ranef(), VarCorr(), conditional_effects() and frmtmb.sample’s draws. The joint log density agrees with brms’s compiled program’s at frmtmb’s estimates and conditional modes. A || term with mo() is one diag() block over the whole term, coded as brms codes it: (mo(x) * f || g) has Intercept, fb, fc, mox, mox:fb and mox:fc. A || term without mo() keeps lme4’s expansion, so (f || g) still has a column for every level of f.

  • set_prior("dirichlet(alpha)", class = "simo", coef = "mox1") puts brms’s Dirichlet on one mo() simplex, and class "sbhaz" on a cox() baseline; prior_dirichlet() is the object. brms’s rules come with it: a coef per simplex, the length of alpha checked, no bounds, and only dirichlet() on these classes. get_prior() lists a simo row per simplex and a sbhaz row per cox() response, as brms does. On a MAP fit the density is that of the simplex’s coordinates, with the chart’s Jacobian: the mode under dirichlet(a) is the mode of dirichlet(a + 1/2) on the weights, pulled further toward the uniform simplex, so dirichlet(1) is not the absence of a prior (?frm, “Monotonic effects”). A concentration below 1/2 has no maximum there and is refused.

Bug fixes

  • A random-effect block with a scale per coefficient no longer has a +Inf or NaN log density where a log sd underflows: us and us_t with two or more coefficients, cs, toep, ar1, hetar1, and gr(cov =) and gr(prec =) with correlated slopes. Their standard deviations are floored at 1e-150 (sd_floored()), the square root of the variance floor of 0.69.0, which is below half an ulp of any sd of 1.1e-134 or more: above that the objective and its gradient are unchanged bit for bit. Over every test of the eight packages, one optimizer stepped a floored log sd below that (to -4318): a separated bernoulli fit with (1 + x | g) under frmtmb_control(autoscale = TRUE), whose pre-fit stopped on “NA/NaN gradient evaluation” and now returns a fit with the pre-fit’s and the separation’s warnings. A sampler’s proposal below the floor meets a finite density or -Inf, so a chain that proposed there takes another path.

  • parnames() warns once whichever generic dispatched it. The method decided from what the parnames binding resolved to, so frmtmb’s own generic, captured before brms loaded and called after, gave no warning.

  • A fit could stop well short of its maximum at optimizer code 0 and without a warning when its design is badly conditioned: on raw polynomials of degree 6 with x on [1, 2], 3 of 20 fits ended up to 0.022 log-likelihood units below lm()’s maximum, and with the response 16 times larger 19 of 20, by up to 1.9. With (1 | g), at degree 6, 5 of 20 stopped up to 0.023 short, 4 of them in silence. Such a fit now takes Newton steps from where the optimizer stopped: on the exact Hessian without random effects, and with them, on a near-collinear fixed design only, on the Hessian of the Laplace objective by differences of its gradient. A step is taken while the Hessian is positive definite and the step predicts a gain above 1e-6, kept only if it gains at least a tenth of that, and the steps are undone if they end where the Hessian is not positive definite. None of these fits stops short now, with or without the group term. A fit already at its maximum is not changed, and neither is a fit whose optCtrl sets one of the optimizer’s tolerances or whose optimizer is a function.

  • A restart that raised (“NA/NaN gradient evaluation”) ended the whole fit although the run before it had a finite optimum. The fallback restarted from the lowest objective the tape had recorded, and a REML likelihood without a bound records that where the objective, solved from another inner start, is NaN (fuzz seed 20379118 under OpenBLAS’s LAPACK). That point is no longer taken, a restart that raises keeps the run before it, and the objective is left at that run’s optimum.

  • A restart that ended worse than the run it started from left the objective at its own last point, where ranef() and sdreport() read it. Every run the optimizer discards now leaves the objective at the kept optimum, and so does the mo() search when it refits nothing.

  • A mo() fit could stop below its maximum in the other sign’s cone without a warning: the search for that cone started from one vertex, at the coefficient’s own value, which is near 0 for a fit in the wrong cone, and it was skipped for an interior simplex whose coefficient is near 0. It now starts at least one of the coefficient’s units from 0, moves to the next vertex while the coefficient ends on its bound, and searches both cones when the coefficient is within 1e-3 of its unit of 0. On brms_monotonic’s ls ~ mo(income) * age, 199 of 200 fits reached the exact maximum, and 194 of a further 200 (up to 0.45 short); now 400 of 400, at 1.2 to 1.4 times the objective evaluations.

  • A group standard deviation that the likelihood does not read is “flat”, not “at zero”. The boundary message said that the sd of a group effect on a nonlinear parameter that enters no row was at zero, while it was 0.22. A boundary verdict now needs the likelihood to change when the sd moves up from its estimate, as far as 10 units on the log scale, so an sd that the likelihood reads only far above its estimate is still at its boundary.

  • Standard errors of a raw polynomial that the Hessian cannot resolve come from the likelihood itself. On a degree-7 raw polynomial with (1 | g) the check named three coefficients and kept the other five with standard errors 8 to 18 times smaller than lmer()’s, built without the two smallest directions of the Hessian, whose finite-difference noise (3e-12) is above one of them (8.9e-15 of the largest). A direction that the Hessian has no digits for is now measured again, with the confirmed small directions next to it, by second differences of the objective; all eight standard errors are now within 0.5 percent of the exact ones from a QR of the whitened design (lmer()’s own are 9 percent below them). A direction the objective cannot confirm is still flat.

  • An ill-conditioned design is named as one. On a degree-8 raw polynomial the smallest curvature is below machine epsilon of the largest, and 0.69.0 said that the data do not determine six of the nine coefficients and kept three. One double-precision covariance cannot hold that curvature beside the others, so the coefficients it carries still have no standard error, and neither does a prediction that reads them. The warning now says that the design is ill-conditioned and that centering the covariate or poly() removes this, and, when it applies, that the optimizer stopped short of the maximum, with the log-likelihood a Newton step would gain (up to 4.5 on these fits). Terms beside the polynomial that do not read those coefficients (a covariate, a factor, sigma) keep their standard errors.

Extension API

  • The sampling API exports simplex_frame_terms() (?frmtmb-sampling-api): every simplex of a fit’s frame in the shape mo_frame_terms() gives a mo() term, the mo() simplexes and then each cox() baseline under brms’s name sbhaz[k]. mo_chart_frame() now sets a cox() baseline’s chart too. frmtmb.sample 0.18.0 samples every simplex through them.

frmtmb 0.69.1

The Ubuntu and macOS checks of 0.69.0 failed where the Windows ones passed. dev/ci691-findings.md has the causes and the numbers.

Bug fixes

  • An nlminb fit reports the objective at its own estimates. When PORT stopped on a finite trial point it had rejected, fit$opt$par was that trial and fit$opt$objective the best value, at another point. 0.69.0 repaired this only when the trial’s objective was not finite. On a cs() ordinal mixture the two values differed by 1 to 9 ulps in 10 of 100 runs; the estimates now come from the best point evaluated, as they already did after a non-finite trial.

  • Three tests asked for an outcome that rounding decides: the standard errors of an xbeta() fit whose Hessian sits at the standard-error check’s flat threshold, the convergence code of a degenerate three-component ordinal mixture, and a list of lost standard errors that can be empty. They now ask for the properties that hold on every platform.

frmtmb 0.69.0

Four lanes, each with an adversarial review and punch rounds: ciharden (tests and CI made independent of platform rounding, CI pinned to ubuntu-24.04 with a Stan job on Ubuntu 26.04 beside it, less memory in the gp() kriging covariance), surface (the brms post-processing calls a ported script meets on a maximum-likelihood fit now answer or refuse by name), setier (which standard errors a fit can report, a boundary fit said as lme4 says it, separation named) and optima (fits that stopped short of the maximum likelihood: mo(), the probit tails, cs() ordinal mixtures, nlminb’s rejected trial point). Each lane’s dev/<lane>-findings.md has the validation, the numbers and the scripts, and dev/reviews/2026-10-07-<lane>.md has the review; dev/round-20261007.md is the round’s record. In brms’s own ported suite, bin 1 now passes 390 of 494 assertions, up from 389 at 0.68.1.

Breaking changes

  • stancode(), standata() and pp_mixture() are frmtmb’s generics, with methods for a fit; they were frmtmb.sample’s, with methods for draws only. stancode(fit) and standata(fit) stop with the reason and name build_objective() and fit$frame. Before, they said “no applicable method”, and with brms loaded they reached brms’s default method, which said “Data must be specified using the ‘data’ argument” about a fit that has data. frmtmb.sample re-exports the three generics and needs this version.

  • The pooled table of frm_multiple() names its rows as brms does: Intercept, not (Intercept), as fixef() of each fit names them. A distributional parameter without a formula, pooled on its link scale, is sigma_Intercept.

  • update() drops a stored prior the updated model cannot take, as brms’s update() does, and says which in a message, where brms is silent. Updating brms_overview’s fit2, which removes the last correlation, stopped at the class-wide lkj(2) prior; it now fits without it. A prior passed to update() itself replaces the stored one, where brms merges the two, and is checked as frm() checks it.

  • A standard error built on a row of noise is gone, on every platform. The standard-error check kept sdreport()’s inverse of the outer Hessian whenever it gave positive variances, even when a parameter’s row of the finite-difference Hessian was pure noise. A variance component at zero then got a standard error of about 10^3 to 10^5 on its log scale, or NaN and a warning, by the sign of that noise: (1 | Subject/a) on sleepstudy gave 375,150 with the reference BLAS and NaN with OpenBLAS. The check now keeps an inverse only where its third tier would remove nothing: no row within the Hessian’s noise, no log standard deviation at its edge (below), no parameter held by a bound, and no direction flatter than 1e-9 of the largest unless the likelihood curves along it. Such a parameter now has a NaN standard error in vcov(), summary() and confint(), and the other standard errors are those of the Hessian without it. A fit that loses nothing keeps sdreport()’s covariance bit for bit. A parameter held by a bound is now reported on every fit, where before it was only when some unrelated direction was nearly flat.

  • A covariance parameter at the edge of its parameter space is a boundary fit, said with a message. A group standard deviation is at its edge when moving it a factor of 7 toward zero changes the log-likelihood by at most 1e-6; a correlation or mixing parameter when moving it further toward its limit does the same; the one correlation of a block whose standard deviation is at zero is undefined. Its lost standard error is reported as lme4 reports a singular fit: one message, “Boundary (singular) fit: …”, of class frmtmb_boundary_fit, naming the parameters with brms’s names (sd_g__Intercept), and not the standard-error warning. An sd near zero is first moved up to 0.01, 0.03, 0.1, 0.3 and 1 times the residual sd (1 on the link scale without one): if any gains, the fit stopped short of its maximum, and the warning says so (“the fit stopped short of the maximum”; at a non-zero code the convergence warning stays). Of 24 random-intercept fits that stop 1e-4 to 0.825 below lme4’s log-likelihood, none gets the boundary message. Under quadrature = TRUE no boundary verdict is given, since that check cannot run on a quadrature objective: such an sd keeps the “flat” standard-error warning, as before. On 600 simulated random-intercept, random-slope and binomial fits the message fired on 267 of the 269 that lme4 calls singular and on none of the other 331; on larger single-sd designs (60 to 300 groups) on 16 of the 17 lme4 calls singular and none of the other 103; on random slopes with a correlation of 0.95 or 0.99, on 19 of the 33 lme4 calls singular (20 with OpenBLAS) and on 2 others, whose correlation lme4 puts at 1 - 2e-7 and 1 - 4.5e-6. When nlminb stops with “singular convergence (7)” there, the gradient is within grad_tol and every lost parameter is at its edge, the fit gets this message and not “Optimizer did not report convergence”, as lme4’s checkConv() does (67 of 69 such random-slope fits). frmtmb_control(check_se = "ignore") silences it, and "stop" makes it an error at frm(), which now builds the check there even when it would otherwise wait for the first summary(). A smooth, gp() or hsgp() term at its own limit (a smooth reduced to its unpenalized part, as mgcv reports in silence) loses those standard errors with no message: 91 of 140 gamSim fits do. summary() lists both under “Parameters without a standard error”, and diagnose() lists the standard deviations as singular. VarCorr(), confint_varcorr(), predict() and fitted() do not warn again about them; predict()’s draws hold them at their estimates.

  • A small eigenvalue of the outer Hessian keeps its standard error only if the likelihood is quadratic along it. The check takes the symmetric second difference f(p + t) + f(p - t) - 2 f(p) along the direction, at the step the curvature says loses 2 * grad_tol each way and at half of it; it keeps the direction when the full one is at least 2 * grad_tol and 3 to 5.5 times the half one (a quadratic gives 4). Symmetric, so that a fit not exactly stationary along the direction keeps it: an identified poisson or bernoulli pair at a correlation of 1 - 5e-11 and raw polynomials of degree 5 or 6 on [1, 2], alone or with a random intercept, keep every standard error, close to glm()’s, lm()’s and lme4’s. A straight step off a curved ridge, which loses as the fourth power, does not keep it.

  • A nonlinear ridge on a fit with random effects loses its standard errors. y ~ a + b with a ~ 0 + f and b ~ 1 + (1 | g), where only a_k + b is identified, kept finite standard errors from the finite-difference Hessian, and a prediction of a reported 822,571. The ridge’s coefficients now have NaN standard errors, and a prediction along the ridge gets NaN and one warning, as without random effects. The identification warning for nonlinear parameters now names only the coefficients the standard-error check lost: at a correlation of 1 - 1e-9 between two covariates it named coefficients whose standard error, 1268, lm() and the exact Hessian both give.

  • Separation is a warning of its own, named once the fit has run far enough, whatever code the optimizer returned. In a bernoulli(), binomial() or beta_binomial() mean, “The data separate the outcomes (complete separation)” or “(quasi-complete separation)”, with the coefficients involved (the first five and the count), class frmtmb_separation. The fit is checked against the definition, along the direction it ran off in, so a large but finite effect is never named (0 of 338 non-separated fits, 162 of 162 separated ones, logit link; probit and cloglog are named too). It replaces the convergence warning it explains, and the coefficients it names show no standard error (NaN) on every path. A fit stopped very early may not have run far enough to show it: complete separation at eval.max 5, 10, 20 and 40 was named on 5, 16, 19 and 20 of 20 fits, a level of a factor with no successes at 5, 10 and 20 on 0, 0 and 20; the misses keep the convergence warning. A factor too wide to decompose (above 2000 columns) has each column tried on its own. Before, on yb ~ z + x with yb = z > 0, nlminb stopped at code 9 with OpenBLAS and the user read only “Optimizer did not report convergence”; on a level of a factor with no successes, nothing was said. The check keeps a sparse design sparse: 0.4 s on a 1e6-row fit with a 500-level factor that took 719 s.

  • A mo() fit reaches the maximum likelihood. The simplex of a monotonic term was held as softmax coordinates, which put a step of 0 at an infinite coordinate. A fit whose step ran toward 0 stopped on that plateau with code 0, wherever the likelihood still rose, or at the local maximum of the coefficient’s other sign. On brms_monotonic’s own ls ~ mo(income) * age, 84 of 200 data sets ended more than 1e-6 below the exact maximum, 53 by more than 0.01 and up to 1.95, and 18 had standard errors that were not finite. Now 199 of 200 reach it to 1e-6 (the other is 0.049 below), every standard error is finite, and no fit warns. The simplex is held as the coordinates of stereographic projection onto the unit sphere, whose squared components are the simplex: zeta = 0 is still the uniform simplex, and every face is at a finite coordinate, where a step of 0 keeps a finite curvature. After the fit, a term whose simplex is on a face is refitted with its coefficient held at the other sign, and the better fit is kept; fit$opt$mo_search records the refits and frmtmb_control(mo_search = FALSE) turns them off. The search costs objective evaluations, and the factor grows with the model, since each refit is of the whole model: 2.4 times 0.68.1’s over the 200 interaction fits, 1.3 times on the plain ls ~ mo(income) (which reached the maximum before and does now), and 3.3 to 3.8 times on a model of 5000 rows with a random intercept over 100 groups. The zeta<j> values that confint(), vcov() and start read are these coordinates, which have no reading of their own; summary() now reports the simplex itself, as brms does, under “Monotonic Simplex Parameters” (moincome1[1], …), with delta-method standard errors; a weight at 0 or 1 has none there (the chart folds at the face, and the delta method would give an interval of [0, 0] or about [1, 1]), and a line under the table says so. dev/optima-findings.md has the measurements.

  • The probit, cloglog and softit links keep their log-odds in the far tails. The probit’s was log(pnorm(eta)) - log(pnorm(-eta)), infinite past |eta| = 38.2 and with a NaN derivative on 24 of 2001 points inside it; an ordinal probit’s density was then NaN. It now reads both logs from pnorm(log.p = TRUE): within 7e-16 of Rmpfr in value on [-200, 200] and 3.6e-12 in derivative. The old form erred by up to 4.3e-12 where it was finite, and probit fits move with it: estimates by up to 9e-10 relative, log-likelihoods by up to 3.4e-13. Past |eta| = 1000, where RTMB’s derivative of pnorm(log.p = TRUE) degrades, it continues as the tail’s leading term. The cloglog’s and softit’s log-odds lost their derivative below eta = -708 and their value below -745; below -40 they are eta itself, which they equal there to double precision. Above -40 both are unchanged in value, and their fits move by an ulp of the derivative: estimates by up to 3.6e-12 relative.

New features

  • pp_mixture() on a mixture fit, ordinal mixtures included: the component probabilities at the estimates in brms’s observations-by-statistics-by-components array, with components named P(K = k | Y). The quantile columns are a Wald interval on the logit, and Est.Error is the standard deviation of that same logit-normal law, integrated numerically: the delta-method error of the probability itself understates the spread near 0 and 1 by orders of magnitude. On a 300-row two-gaussian mixture against brms 2.23.0’s pp_mixture() (4 chains of 1500 draws), Est.Error was a median 1.11 of brms’s posterior SD over all rows (the delta method’s was 0.36), 0.84 to 1.27 by bin for probabilities between 0.001 and 0.999, and up to 4.8 times it within 0.001 of an edge; the interval was a median 1.04 of brms’s width, and every estimate was inside brms’s 95% interval. brms’s draws arguments are refused by name.

  • fixef(), summary() and conditional_effects() of a frm_multiple() result pool the imputations by Rubin’s rules, where brms’s brm_multiple() combines their draws. fixef() has brms’s rows, ordinal thresholds included, and a t interval on the Barnard-Rubin degrees of freedom. Its estimates agree with mice::pool() of lm() on nhanes to 4.1e-6; its standard errors are 0.93 of mice’s, because each imputation’s variance is the maximum-likelihood one, and its complete-data degrees of freedom are df.residual() of the fit, which counts sigma (20 there, where lm() has 21), as anova() of a frm_multiple() result already takes them. summary() prints brms’s blocks with df and fmi columns. conditional_effects() pools every grid point of the first imputation’s grid on the scale its Wald band is symmetric on; method = "predict" and the bootstrap and profile bands are refused by name.

  • add_criterion() exists, with brms’s criterion names. On a fit it refuses each criterion by name: every one is a posterior quantity, and AIC() and BIC() are the maximum-likelihood comparison. On draws, frmtmb.sample’s method stores it as brms does.

Bug fixes

  • plot() of a fit refuses brms’s plot.brmsfit() arguments by name (N, nvariables, variable, regex, pars, fixed, combo, bins, theme, plot, newpage), with the reason and plot(frm_sample(fit), ...) for brms’s display. It suggested x for N.

  • summary(fit, waic = TRUE) is refused by name with the reason. brms 2.23.0 ignores the argument.

  • fitted() on an ordinal fit whose disc predictor has no fixed column (disc ~ 0 + gp(x, k = 6), disc ~ 0 + (1 | g)) stopped with “requires numeric/complex matrix/vector arguments”, in sample and on new data.

  • frm_sample(fit) on an exact y ~ gp(x) fit moves. A one-scale dense block (gp(), ou(), homcs(), homtoep(), the spatial structures and gr(cov =) with one coefficient) whose log sd passed about -372 had the zero matrix as its covariance, and its field’s log density was +Inf. The sampler’s first long step from the mode reached such a point and stayed there: acceptance 0, step size NaN, every transition divergent. The variance is now floored at 1e-300, which is the variance itself, bit for bit, above 9e-285, so no fit changes.

  • (cs(1) | g) with brms attached after frmtmb got the internal “variable lengths differ (found for ‘.frm_cs(1)’)”; it gets the refusal it gets without brms. The refusal of conditional_effects(method = "predict") on categorical() names that family, where it said “ordinal family”.

  • ranef(condVar = TRUE) on a fit that lost a standard error reads the repaired joint covariance that predictions read. It read sdreport()’s own conditional variances, built on the indefinite inverse: on a gr(g, by = f) fit they ran from 0 to 99 times the repaired ones.

  • frm() read its estimates, and started its restart, from the trial point nlminb had rejected when nlminb stopped on one (“false convergence (8)”), while logLik() reported the best point. The best point the optimizer evaluated is now the fit’s.

  • A fit whose family declares a stationary point (skew_normal()) and that has random effects reported the random effects’ modes of the last escape it tried, not of the optimum it kept. On skew_normal() with (1 | g), seeds 1 to 40, the escape ran on 14 fits and on 5 ranef() was off by 0.002 to 0.143.

  • cs() on a cumulative component of an ordinal mixture no longer stops with “NA/NaN gradient evaluation”. mixture(cumulative(), sratio()) with cs(z) died on 7 of 20 data sets and mixture(cumulative(), cumulative()) on 4 of 20; both now finish on all 20. Two causes: the rejected trial point above, and two thresholds of one row that the line search had brought to the same double, where the component’s density was -Inf, the mixture’s finite and its gradient NaN. That touch is now as undefined as the crossing past it. Most of these fits still report non-convergence, correctly: their optimum is where a row’s category closes in one component, and the gradient there does not vanish. On the 18 of 20 mixture(cumulative(), sratio()) fits that stop with “false convergence (8)”, some row’s two thresholds are within 2.3e-13 of each other.

Performance

  • The kriging covariance of a gp() term no longer holds extra n x n copies of the prediction grid: gp(x, by = <numeric>) held two more, and every exact gp() one more for writing its diagonal. It is the same matrix to the bit.

Extension API

frmtmb 0.68.1

The Ubuntu checks of 0.68.0 failed where the Windows and macOS ones passed. dev/cifix-findings.md has the causes, the numbers and an OpenBLAS build of R for Windows that reproduces them.

Breaking changes

  • A fit with random effects and a parameter without a standard error gives its predictions a covariance. When the outer Hessian lost a standard error (a smoothing or group sd at zero, for example), the joint precision built on it was not positive definite, and inverting it gave grid rows of s(x, by = fac) + gp(x) coefficient variances as low as -0.10 (with OpenBLAS; -0.016 with the reference BLAS). frm_linpred(se.fit = TRUE) then reported the kriging variance alone on those rows, 0.00076 where the standard error is at least 0.064, and too small a variance on others. The joint covariance now propagates the outer parameters’ uncertainty only along the directions the fit determines, as the fit without random effects already did. It is conditional on the lost directions: when each lost parameter is a direction of its own, that is the covariance with them held at their estimates. frm_linpred(), frm_lp_basis(), frm_joint_cov() and everything built on them change on such fits, and only on them. frm_joint_cov() shows a lost parameter’s row and column as NaN, as vcov() does, and returns their rows as lost_pos. vcov() under REML, which reads the joint precision directly, is not changed.

Bug fixes

  • On a fit with random effects, a prediction that moves along a direction the fit does not determine gets a NaN standard error and one warning, as it already did without random effects. On y ~ x + (1 | id) with one observation per id, where sigma and the id sd lose their standard errors, frm_linpred(dpar = "sigma", se.fit = TRUE) and fitted(dpar = "sigma") reported a standard error of 0 with no warning, under ML and REML.

  • Rows of one prediction at the same unseen exact gp() position get bit-identical kriging weights, variance and covariance on every BLAS. Each distinct position is kriged once. With OpenBLAS the rows differed by a few ulps, so a difference curve across a factor at one position kept 9 ulps of the residual it cancels.

  • Tests that passed on Windows by a platform accident now pass on Ubuntu too: the separation fit of test-se-check.R has a budget at which nlminb converges on both, and two fits whose sd runs to zero allow the standard-error warning that OpenBLAS’s rounding gives them.

frmtmb 0.68.0

Five lanes, each with an adversarial review and punch rounds: fixes (emmeans() on transformed predictors, trunc() and se() in conditional_effects(), the ordinal leftovers of 0.67.0, brms’s s() null-space basis, the nonlinear flat-direction warning), gpby (gp(x, by = ) with brms’s arguments and prior classes, and the exact gp() kriging covariance at new positions), ordmix (ordinal mixtures, hurdle_cumulative() with thres(gr = ) and cs()), nanse (a fit says which parameters have no standard error, and why, and frm_allfit() starts where lme4::allFit() does) and vigport (the brms vignette port measured again, records only). The consolidation lifted the refusal of cs() on cumulative() and made conditional_effects(method = "predict") report the median, both the user’s decisions of 2026-10-06. Each lane’s dev/<lane>-findings.md has the validation, the numbers and the scripts, and dev/reviews/2026-10-05-<lane>.md (2026-10-06-nanse.md for lane nanse) has the review; dev/round-20261005.md is the round’s record. In brms’s own ported suite, bin 1 now passes 389 of 494 assertions, up from 387 at 0.67.0.

Breaking changes

  • frm() builds the Hessian at the optimum and warns when a parameter has no standard error. The warning starts “Standard errors are not available for k of n parameters”, names each parameter, and gives the reason: a bound holds it, the data separate the outcomes, the likelihood is flat along it, a step along it raises the log-likelihood (confirmed with two objective evaluations, else it is called flat), or its Hessian row is not finite. Before, a converged fit could return NaN for every standard error with no warning: ls ~ mo(income) * age on the brms_monotonic vignette’s own data code did so on 55 of 200 data sets at optimizer code 0. Now 26 of those 200 fits get this warning, 17 get a convergence warning, none loses a standard error silently, and the warning fires on none of the 357 mo() fits whose standard errors are all finite. In the eight test suites it fires 43 times outside its own tests, every time at optimizer code 0 on a fit where sdreport() gave a non-finite standard error. A fit that did not converge gets its convergence warning instead. An earlier warning explains only the parameters it names: a grouping factor with one level its variance, an observation-level effect its variance and sigma, a family’s own fit-end check (a threshold no row places, an unidentified disc intercept, zero_one_inflated_beta()’s coi) all but bound-held parameters. A check set to "ignore" explains nothing. The cost grows with the number of parameters. Without random effects the check reads the exact Hessian, about one gradient per coefficient: 8 to 23 percent of the fit on seven small models and 19 to 24 percent on a glm with 301 coefficients. With random effects it builds the finite-difference Hessian, two gradients per outer parameter, when the model has at most 10 outer parameters or those gradients are at most a quarter of the evaluations the optimizer made. Otherwise the check waits for the first use of a standard error and warns there, also when that use is fixef(). The rule counts work and does not read the clock, so the same call warns in the same place on an idle machine and a loaded one (dev/nanse-rev2-determinism.R: 40 of 40 at fit time in both). Mixed models with 14 to 23 outer parameters were checked at fit time for 19 to 23 percent; with 42 to 403 the check waited, and the fit cost 3 percent or less. summary() reuses the finite-difference Hessian. frmtmb_control(check_se = "ignore") skips the check and "stop" makes it an error (dev/nanse-findings.md).

  • A parameter without a standard error no longer takes the others with it. Where sdreport()‘s inverse fails, the covariance is recomputed. First the Hessian is scaled to unit diagonal, and on a model without random effects the exact Hessian replaces the finite-difference one. Of the 55 mo() fits above, 47 get every standard error back, equal to the same Hessian inverted with the saturated simplex coordinate held to 2.9e-5 relative, and 8 lose only one to three parameters. An a * exp(b * x) fit with x spanning 0 to 1e5 gets its standard errors back too; the finite-difference step of 1e-3 in b = -2e-5 had given a diagonal of 1.3e96. Where both repairs fail, a parameter held by a bound (test-backlog “The covariance machinery is not bound-aware”) and a simplex weight whose Hessian row is exactly zero lose their standard errors, and so does every parameter with a measurable part along a direction the likelihood does not determine, however many share it: y ~ a + b with a ~ 0 + f at 60 levels loses all 60 a coefficients and b. The other parameters keep theirs. summary() lists the lost parameters with the reason instead of a bare NaN. A prediction, emmean, hypothesis or VarCorr() entry that moves along a removed direction gets a NaN standard error, and fitted(), predict(), frm_linpred(), conditional_effects(), emmeans(), hypothesis() and VarCorr() warn once per call with the number of such rows; one that does not keeps its standard error. So conditional_effects() keeps its bands on the mo() fits, and on y ~ x + (1 | g1) + (1 + x | g2) with the g2 block at variance 0, VarCorr() keeps g1’s sd error (0.233, as hypothesis() gives it) and the residual sd’s (0.0794). A parameter that keeps its standard error does not pass a removed direction to the predictions through its own small part in it, and a conditional_effects() plot whose band is NaN draws the curve alone instead of stopping on “need finite ‘ylim’ values”. Under REML or profile = TRUE the coefficients’ standard errors come from the joint precision, which this does not repair, and the warning says so. A fit that did not converge keeps sdreport()’s own covariance.

  • frm_allfit() starts every optimizer where lme4’s allFit() does. The refits started from zero, so on a nonlinear model 3 of 4 refits failed and bobyqa reported success 155 log-likelihood units below the fit it was checking. They now start at the original fit’s estimates (start_from_mle = TRUE, lme4’s default) or, with start_from_mle = FALSE, at the original fit’s own start, including its start =. The printed table compares every refit with the best log-likelihood reached and marks a success code more than grad_tol below it as “converged elsewhere”. A failed refit keeps its error message in $errors. The fit object stores its caller’s start as fit$start.

  • A smooth’s null-space coefficient is brms’s. s() is built with mgcv::smoothCon(diagonal.penalty = TRUE), as brms builds its Xs and Zs, so fixef()’s sx_1 (and sigma_sx0_1) is brms’s coefficient and a prior with coef = "sx_1" or a hypothesis on it is about brms’s parameter. Before, it was brms’s coefficient times a data-dependent factor (5.0 to 7.7 on gamSim data, of either sign). The design is now brms’s standata Xs and Zs bitwise for every single-penalty smooth, by = and 2-D s() included; t2() is unchanged. The model is a reparameterization of the old one, but the optimizer’s path changes, so a different few fits stop at a worse optimum. On 1020 gamSim fits per build (gamSim seeds 1 to 60, 18 smooth shapes; the review’s dev/fixes-rev2-smooth.R and -smooth-cross.R): convergence codes other than 0 in 11 against 16 on the old basis, non-finite fixed-effect standard errors in 37 against 31, and 9 fits on each build more than 1e-3 below the best logLik either build or mgcv found, 6 of them the same fits; each fit only one build got wrong reaches the other’s optimum from the other’s smoothing SDs. The optimizer steps the null-space coefficients in unit-SD coordinates, without which a smoothing SD at zero ran far down its log scale and left NaN standard errors far more often.

  • A nonlinear fit warns when its nonlinear parameters’ coefficients are not identified at the optimum, naming the coefficients: when the likelihood is flat there along a combination of them, as with two parameters that enter only through their sum or difference while their formulas share a term (a + b with a ~ 1 + x and b ~ 1 + x), or a product of two intercepts. Such a fit used to return one point of the ridge without a word. The check reads the Hessian of those coefficients at the fitted point, which costs 2 k gradients for k coefficients (0.61 s of an 11.3 s fit at n = 200000). A curved ridge whose curvature does not vanish to 1e-9 there (exp(a)^k) is not flagged; vcov() and summary() still report its standard errors as not finite. A prior that reaches every flat direction removes the warning, and then the prior alone sets the split, so a weak one gives very wide intervals.

  • conditional_effects() holds the variable of a trunc() bound or an se() term at its mean when conditions does not set it, as brms does, where it refused the display. An expression bound is evaluated on the grid, where the response is at its mean too, so trunc(lb = min(y) - 1) is mean(y) - 1, as in brms. The expected value is the truncated mean at the held bound and agrees with brms 2.23.0.

  • confint() and vcov(full = TRUE) name an ordinal fit’s internal threshold parameters by what they are, where they read tau_raw_<k>: Intercept[1], Intercept[a,2] or delta where the internal parameter is that one, and log(Intercept[2] - Intercept[1]) or log(delta) where it is a transform. A cs() coefficient takes its fixef() name. parm = takes these names, brms’s b_Intercept[1], and the old template names.

  • gp() defaults to iso = TRUE, as brms 2.23.0 does. A multi-dimensional gp(x1, x2) now shares one lengthscale over the Euclidean distance; gp(x1, x2, iso = FALSE) gives one per dimension, which was the old default. The code comment that called the old default brms’s was wrong: brms’s gp() signature has iso = TRUE, and its Stan program for gp(x, z) declares one lengthscale (dev/gpby-brms-explore.R).

  • Prior class "sd" no longer reaches a gp() term. brms priors a GP’s marginal standard deviation as class "sdgp" and its lengthscales as class "lscale", and frmtmb now has both classes (see New features). set_prior(class = "sd") on a model whose only random-effect block is a gp() is refused, and the message names "sdgp". A brms prior table with sdgp and lscale rows, which was refused by name, now carries over row for row.

  • get_prior() lists no class "sd" rows for a gp() term. It listed sd rows for the GP’s block, which no prior could reach as brms names it; it lists the sdgp and lscale rows brms’s default_prior() lists instead.

  • summary()$gp labels a lengthscale row with brms’s name, lscale(gpxzx) and lscale(gpxzz) where it wrote lscale(gpxz[1]) and lscale(gpxz[2]) for gp(x, z, iso = FALSE) (dev/gpby-p1-sumgp.R). Code that indexed the table by the old row names must use the new ones.

  • cs() in the formula of a parameter other than the latent predictor is refused, in brms’s words (“Category specific effects are only supported for the main parameter ‘mu’”). The offsets of every predictor went into the one slot the ordinal densities read, so bf(y ~ x, disc ~ cs(z)) fitted y ~ x + cs(z) under disc’s name, its log-likelihood that model’s to 1.1e-9, with the coefficients reported as disc_z[k] (dev/ordmix-base-behavior.R).

  • conditional_effects(method = "predict") reports the median of its simulated responses as estimate__, as brms reports the median of its predictive draws. It reported the expected response, which method = "epred" still shows. The band is unchanged, and the estimate is the 50% point of the same draws: a whole count for a discrete family when ndraws is odd, inside the bounds of a truncated one. At a Poisson fit’s x = -1, 0, 1 it is 1, 2, 4, where it was 1.46, 2.52, 4.35, and brms’s median at the same parameters is 1, 2, 4 (dev/fixes-ce-pred.R, test-ce-predict-median.R). frmtmb.sample’s draws route reported the median already. The user’s decision of 2026-10-06.

New features

  • default_prior() lists one class "Intercept" row per threshold, coef = "1", "2", …, under flexible thresholds, and per level of thres(gr = ), as brms does, and set_prior(class = "Intercept", coef = "2") puts a density on that threshold. It is refused under threshold = "equidistant", as in brms.

  • The compatibility table has rows for disc and the "equidistant" and "sum_to_zero" threshold structures.

  • gp(x, by = f), with brms’s semantics. gp() takes brms’s whole argument list: by, k, cov, iso, gr, cmc, scale and c (k = NA and by = NA mean none, as in brms). A factor by fits one GP per level under cmc = TRUE and an intercept GP plus contrast GPs under cmc = FALSE, each over its own rows, scaled and centered over those rows alone and with its own standard deviation and lengthscales, as brms:::data_gp() builds them; a numeric by multiplies one GP. gr = FALSE keeps one latent value per row and scale = FALSE keeps the inputs unscaled. On six Hilbert-space forms (factor and numeric by, cmc = FALSE, gr = FALSE, isotropic and anisotropic two-dimensional) brms’s Stan log density at frmtmb’s estimates equals frmtmb’s joint density to 2.8e-14 and its gradient in the latent values vanishes to 2.6e-14 (dev/gpby-lpcheck.R); the exact form’s marginal likelihood is the closed form to 1e-8 relative (tests/testthat/test-gp-by.R). A by-level the fit never saw is refused with brms’s own message, allow_new_levels = TRUE included, as brms refuses it. cov = other than "exp_quad" is refused: the Matern and exponential kernels are not implemented.

  • brms’s names for a GP’s parameters. variables() and hypothesis() read sdgp_<term> and lscale_<term> (sdgp_gpx, sdgp_gpxfa for level a of gp(x, by = f), lscale_gpxzx for a non-isotropic term), on brms’s scales, and summary()$gp labels its rows the same way (see Breaking changes). Before, variables() listed neither parameter, so brms’s variables(fit6) test could not transfer.

  • Prior classes "sdgp" and "lscale", brms’s, with coef the sub-GP brms names ("gpxfa"). Both are on brms’s natural scales with the log-Jacobian applied; the lengthscale is on the inputs brms divides by their largest distance. get_prior() lists the rows brms’s default_prior() lists.

  • frm_lp_basis(extra_cov = TRUE) returns extra_cov, the full covariance over the rows of the variance that is not coefficient uncertainty: an exact gp()’s kriging residual at an unseen position, which every unseen row of one field shares, and a new grouping level’s draw, which every row at that level shares. Its diagonal is extra_var exactly. It is a sparse Matrix where its sources cover few entries, an empty one (nothing allocated) on a fit with no such source, and dense otherwise: a 2000-row grid past an exact gp()’s data peaks at 106 MB in the call (dev/gpby-p1-mem3.R). One call on a stacked grid gives the covariance between two grids.

  • frm_extra_cov_deriv(), the covariance of the order-th derivative of that same part, built from its sources: an exact gp()’s kriging residual by the squared exponential kernel’s closed form derivatives, a new grouping level by differencing its draw’s design rows. Differencing extra_cov itself divides its rounding by the step to the fourth power at order 2, which gave a standard error of 0 past an exact gp()’s data and up to 1.5 for the curvature of a term linear in the variable, whose true value is 0 (dev/reviews/2026-10-05-gpby.md, B1).

  • emmeans() on an exact gp() at a position the fit did not see. Refused until now; the kriging covariance across the grid enters V, so a mean at an unseen position carries it and a contrast between two rows at one position cancels it.

  • mixture() takes ordinal components, as brms does: cumulative(), sratio(), cratio() and acat() in any combination, each with its own link, disc<k> and threshold structure, or hurdle_cumulative() for every component. order = "none", brms’s default for them, gives each component its own thresholds, b_mu1_Intercept[k], b_mu2_Intercept[k], and order = "mu" gives all of them one shared vector, brms’s fixed_Intercept, which a sum-to-zero component centers (with components that have a mu intercept, order = "mu" stays refused). thres(x = ), thres(gr = ), cs() per component, theta<k> ~ and disc<k> ~ work. The category probabilities that fitted(), predict(), simulate(), conditional_effects() and frmtmb.sample’s posterior_epred() and log_lik() report are the theta-weighted sums of the components’. The log density equals brms 2.23.0’s compiled program at the optimum and at three perturbed parameter vectors to at most 2.5 ulp on the 20 mixture shapes of dev/ordmix-lpcheck.R; a probit component whose latent distance from a threshold passes about 38 has a NaN density where brms’s is finite (one point of the review’s three-component probit, sratio and acat mixture). fixef() has brms’s rows in brms’s order. default_prior() lists brms’s threshold rows, and set_prior(class = "Intercept", dpar = "mu1") (with no dpar under order = "mu") lands where brms puts it. A sum-to-zero component lists no class "Intercept" rows, where brms lists one per threshold, as for one sum-to-zero family. brms’s equidistant mixture does not compile (its parameters block declares a bare delta per component, and its body reads delta_mu<k>); frmtmb fits the model the body describes and names it delta_mu<k>. groups = and a hurdle_cumulative() component beside one without a hurdle are refused. A mixture whose likelihood has a flat direction by construction warns: no predictor in any parameter, or hurdle components whose hu<k> and mixing weights have none, unless priors hold enough of those parameters (a threshold prior holds none of the hurdle’s); or shared thresholds with no mu<k> predictor on components of one family, link and disc. A fit whose component ends at a degenerate boundary warns and names it: a step function of its predictors (doubling its latent distances costs the log-likelihood less than 0.1), a threshold no row is near, or two thresholds that are the same number. Maximum likelihood for an ordinal mixture often has its supremum there (22 of 160 simulated two-component fits; the warning fires on 20 of them and on none of the other 138, nor on any of 167 sound fits with strong predictors under four links), so compare several starts, or hold the components with priors.

  • hurdle_cumulative() takes thres(gr = ) and cs(), as brms fits them. Each group’s density keeps the hurdle, the counts are over the categories above it, and cs() comes off each row’s thresholds, with brms’s warning that the effects are experimental. The log density equals brms 2.23.0’s compiled program to at most 1.6 ulp under the logit and the probit, with hu ~, disc ~ and every threshold structure (dev/ordmix-lpcheck.R). A row whose cs() offsets cross two thresholds has a negative category probability: its density and fitted() are NaN there (brms returns the negative difference), and simulate() gives NA. cs() with thres(gr = ) stays refused, as in brms.

  • cs() on cumulative(), as brms 2.23.0 fits it, with brms’s warning that category-specific effects for the family are experimental. It was refused. Row i reads the thresholds tau_k - cs_ik, brms’s Intercept - transpose(mucs[n]), and the log density equals brms’s compiled program to at most 0.9 ulp at the optimum and at three perturbed points on six shapes: the logit and the probit, a predictor beside cs(), two cs() terms, a factor in cs(), disc ~ 0 + z, equidistant thresholds and a mixture with sratio() (dev/rel068-cs-lpcheck.R). The offsets can make a row’s thresholds cross: such a row’s density and fitted() probabilities are NaN, as brms’s density is (brms’s posterior_epred() returns the negative difference), and simulate() gives NA there. predict() gives NA proportions for a row that crosses at the estimates; a row that crosses only in some of its simulated parameter draws reports the proportions over the other draws, and the call warns with the draws dropped per row, here and on hurdle_cumulative() (the user’s decision of 2026-10-06). hurdle_cumulative() and a cumulative() component of an ordinal mixture take cs() the same way and warn as brms does, once per component. A family that takes no cs() is refused in brms’s words, “Category specific effects are not supported for this family”. The user’s decision of 2026-10-06.

  • Ordinal mixtures and the per-threshold features meet. default_prior() lists the per-threshold class "Intercept" rows of each component under dpar = "mu<k>", and of the vector an order = "mu" mixture shares once with no dpar, as brms lists them; set_prior(class = "Intercept", coef = "2", dpar = "mu1") puts a density on that threshold alone. confint() and vcov(full = TRUE) name a mixture’s thresholds by what they are, mu1_Intercept[1] and log(mu1_Intercept[2] - mu1_Intercept[1]), and the shared vector once, as Intercept[1]. The compatibility table’s cells for disc, "equidistant" and "sum_to_zero" with mixture say what the release does: equidistant is refused under order = "mu", as in brms.

Performance

  • The kriging draw on a high-rank grid is faster. Past a quarter of the unseen positions, gp_krig_factor() takes the whole conditional covariance and LAPACK’s pivoted Cholesky instead of its column loop: 0.020 s against 0.071 s at rank 400 of 400 positions and 0.161 s against 0.293 s at rank 499 of 1000, with the law unchanged (dev/rel068-krig-rank.R). The low-rank regime the loop was built for is untouched.

Bug fixes

  • A nonlinear fit whose default start a prior placed outside the family’s domain stopped at that start. A prior’s location places every coefficient of a nonlinear parameter, so fit2’s normal(2, 2) on a ~ Age put mu = 2 + 2 Age below 0 on Gamma("identity"). After such a failure the fit restarts from the placed intercepts with the slopes at zero; with fit2’s priors the update of ledger row brmsfit-methods:955 then converges near brms’s posterior means. A fit that converged from its start is never restarted.

  • emmeans() stopped with “undefined columns selected” when a transformed predictor’s variable was found in the formula environment rather than in the data.

  • vcov_cluster() and cluster_scores() stopped with “A map factor length must equal parameter length” on every ordinal fit whose disc is held at 1.

  • confint(parm = "Intercept[2]") on a family that holds that threshold through a transform says so and points to fixef() and hypothesis().

  • emmeans() on a fit with a transformed predictor (poly(z, 2), log(abs(z) + 1), scale(z)) stopped with “undefined columns selected”. The reference grid now holds the variable at its mean, as brms’s and lm()’s do, and the transform is evaluated with the fit’s frozen basis, so scale() takes the training center and scale and poly() its training coefficients, also under at =. The means agree with brms 2.23.0 at fixed parameters and with glm().

  • conditional_effects() on a gp() fit had nothing to draw. y ~ gp(x) stopped with “No plottable predictors found”; a gp() term’s covariates and its by variable are now effects, and a term over several variables adds their two-way displays, as brms’s get_all_effects_type(x, "gp") does: x, f and x:f for gp(x, by = f).

  • fitted()’s Est.Error on a category distribution left out an exact gp()’s kriging variance at a position the fit did not see, and a new grouping level’s variance, which the scalar route has always added. Past the data the shortfall was 8 percent of the standard error on the test-fd-chain.R fixture. The analytic route conditional_effects() takes for an ordinal family had the same gap.

  • fitted()’s finite-difference Est.Error costs one pair of evaluations per smooth or gp() block where it cost one per coefficient. Such a block reaches a category probability only through its predictor’s eta, linearly, so the derivative is the chain rule (d p / d eta) Z. Counted calls of the fitted value on dev/gpby-fdcost.R’s cells: a 160-coefficient gp(x) at three new rows 330 to 12, s(x, k = 8) + (1 | g) 26 to 16 and 24 to 14, a 12-column gp(x, k = 12) 34 to 12, a control unchanged at 12.

  • Kriging at an unseen gp() position works in correlation units, so a draw whose standard deviation underflows no longer stops the solve as exactly singular, and a row at an observed position is the fitted value exactly rather than through the kriging weights.

  • A gradient that is not finite at the reported optimum passed check_convergence() in silence, because its test compares a finite number with grad_tol. nlminb reports X-convergence there (a three-component ordinal mixture whose probit component saturated, gradient Inf). The fit now warns and names the parameters.

  • The draws of a model whose location predictor has no column (an ordinal y ~ 1 with a modeled disc, or y ~ cs(x)) were named one column off: brms_par_labels() dropped the labels of every distributional coefficient, so frmtmb.sample stored disc’s slope as b_Intercept[1] and the last threshold as tau_raw[3] (dev/ordmix-emptybeta.R).

  • ?refit says that refit() does not repeat the warning frm() gives when a nonlinear model’s likelihood is flat at the optimum: the design and its flat direction are the original fit’s.

  • vignette("brms-migration") was stale on hurdle_cumulative() (it takes the threshold structures, thres(gr = ) and cs()), on acat()’s links, on disc, and on order = "mu" for ordinal mixtures, and now says what cs() on the cumulative families does.

Extension API

  • ord_thres_linpred() joins the sampling API: brms’s linear predictor with the thresholds included, at a fit’s estimates, which frmtmb.sample’s posterior_linpred(incl_thres = TRUE) reads per draw. An ordinal mixture is refused first, in brms’s words: “‘incl_thres’ is not supported for mixture models.” # frmtmb 0.67.0

Three lanes of brms parity, each with an adversarial review and punch rounds: ordinal (brms’s disc, threshold structures and acat() links on every ordinal family), ceplot (plot() of conditional effects and hypotheses, new levels in conditional_effects() and fitted(), small post-fit methods) and formrobust (addition-term expressions, update() that keeps the parameter formulas, bernoulli coding, the autocorrelation functions, offsets in the grids). Each lane’s dev/<lane>-findings.md has the validation, the numbers and the scripts, and dev/reviews/2026-09-30-<lane>.md has the review; dev/round-20260930.md is the round’s record. In brms’s own ported suite, bin 1 now passes 387 of 494 assertions, up from 368 at 0.66.0.

Breaking changes

  • Every ordinal family has brms’s discrimination disc. cumulative(), sratio(), cratio() and acat() take link_disc = "log" and threshold = "flexible", in the order of brms’s constructors, and disc is held at 1 unless the formula models it: bf(y ~ x, disc ~ 0 + z). lf(disc ~ ...) is no longer refused, and family() lists disc among the dpars. Held at 1, disc shows nowhere, as in brms: not on the Links: line, not as a Fixed dpar:, not in fixef(flatten = TRUE) or coef(). Every other output of a flexible fit is unchanged: on 26 models of every kind, 570 of 581 outputs are identical() to frmtmb 0.66.0, and the 11 that are not are the changes listed here (dev/ordinal-p1-bitwise.R). The likelihood cannot tell an intercept in disc apart from the scale of the thresholds, so the fit warns unless a prior holds it, also when cell-means columns (disc ~ 0 + h) add up to one; brms holds it with its default normal(0, 1).

  • hurdle_cumulative() hides its disc held at 1 too, as brms does: its summary() and print() no longer read disc = log and Fixed dpar: disc = 1, and fixef(flatten = TRUE), and with it the default statistic of frm_bootstrap(), drops the constant disc_(Intercept) entry. fixef(flatten = TRUE) names are again all row names of confint(), the invariant ?fixef shows.

  • default_prior() lists one Intercept row per level of thres(gr = ), as brms does, beside the class-wide one; each is a row set_prior(group = ) takes.

  • acat() takes every link brms takes for it: probit, probit_approx, cloglog, cauchit and softit beside logit. Off the logit, a category probability is brms’s second form, a product of distribution functions times a reversed product of survivals, normalized (brms:::inv_link_acat()). They were refused.

  • plot() of a conditional_effects() result and of a hypothesis() result returns plot objects, invisibly, as brms’s methods return their list of ggplot objects: a named list of frmtmb_ce_plot objects (one per effect) or a list of frmtmb_hyp_plot objects (one per page). Each draws when printed or plotted. Both methods returned their input before. A hypothesis plot stacks up to nvariables = 5 hypotheses on a page, as brms’s does, where it drew one page per hypothesis, and a surface is drawn as colored contour lines (brms’s stype = "contour") where it was an image with contours. Graphical parameters (col, main, …) are ignored as before, and now with a warning that names them.

  • predict(sample_new_levels = "old_levels") chooses one seen level per grouping factor, as brms’s get_new_rdraws() does, not one per term. On bf(y ~ x + (1 | g), sigma ~ (1 | g)) or (1 + x || g) a new group took each term’s effects from a different seen group, a group the data does not have. Under gr(g, by = f) the choice is made among the row’s own by-level only, with no draw for the by-level the row does not read. A multivariate call makes one choice for all its responses, so a grouping factor two responses share reads one seen group in both, as in brms.

  • conditional_effects(effects = ) checks each effect against the model, as brms does. An effect naming a variable the model does not read is dropped with brms’s warning “Some specified effects are invalid for this model”; with no valid effect the call stops with brms’s “All specified effects are invalid for this model”, which lists the valid variables. One invalid effect among valid ones was an error, and a variable in the data but not in the model drew a flat curve.

  • update() with a complete formula keeps the stored parameter formulas, as brms’s update.brmsfit() keeps them through update.brmsformula(). update(fit, y ~ x2) on bf(y ~ x, sigma ~ z) fitted y ~ x2 with a constant sigma; it now fits bf(y ~ x2, sigma ~ z). A formula of the update replaces a stored one of the same name, with brms’s message. On a nonlinear fit the update replaces the body and keeps the nonlinear parameters’ formulas, with brms’s message “Argument ‘formula.’ will completely replace the original formula in non-linear models”, so update(fit, bf(count ~ a + b, nl = TRUE)) now refits where it was refused (ledger row brmsfit-methods:955). The updated fit’s call holds the pooled formula as a frmtmb::bf() call; the family goes in as its constructor call only when that call says everything the family holds, and as the family object otherwise, so an option such as huber(k = 3) survives the update. A complete formula on a multivariate fit is refused, as brms refuses it (“Updating formulas of multivariate models is not yet possible”); it replaced every response’s formulas before.

  • bf(nl = TRUE) without a parameter formula is no longer refused at bf(); frm() refuses it, as brms refuses it at brm(). The formulas may arrive afterwards, bf(y ~ a * x, nl = TRUE) + lf(a ~ 1), or from the fit update() keeps.

  • A bernoulli() response can hold any two values. They are coded 0 and 1 by level order, as brms codes them (-2, -1 become 0, 1; so do 1, 2, 0.1, 0.9, "no", "yes" and FALSE, TRUE). The coding is kept with the fit and applied to the response of newdata in residuals(), pp_check() and frmtmb.sample’s predictive_error(). Before, anything but 0/1 was refused (ledger row standata:75). Two values that both lie strictly between 0 and 1 are coded too, with a warning that they look like a proportion, which binomial() with trials() or Beta() fits (lme4#682); effect coding such as -0.5 and 0.5 does not warn. refit() takes the 0/1 codes that simulate() returns, and refuses any other value by name. refit() of any family refuses a response holding NA. One divergence: brms codes an all-TRUE logical response 0, and frmtmb codes it 1.

  • An addition term’s expression that gives NA is refused by name, as brms refuses it at standata(): trunc(lb = ifelse(x > 1, NA, -5)) and trials(ifelse(x > 1, NA, n)). At 0.66.0 the expression was a column of the model frame, and na.omit dropped those rows.

  • An addition term reads data columns only (user decision, 2026-09-30), as brms requires. A bare name such as weights(w), with w a full-length vector outside the data, worked at 0.66.0 and is now refused with a message that names w. Predictor formulas and offsets keep R’s rule of looking in the formula environment. See “An addition term takes an expression” under Formula grammar.

New features

  • threshold = "equidistant" and threshold = "sum_to_zero" on cumulative(), sratio(), cratio(), acat() and hurdle_cumulative(), which refused both before. Equidistant thresholds are the first threshold plus k - 1 times delta, as in brms; variables(), summary() and hypothesis() name it delta (delta_<k> per level of thres(gr = ), delta_<resp> in a multivariate model), and set_prior(class = "delta") gives it a prior. delta is positive for the two families whose thresholds are ordered, as brms bounds it. Class "Intercept" then addresses the first threshold alone, as in brms. Sum-to-zero thresholds sum to zero and the design is not centered, as in brms. brms declares one parameter per threshold there and relies on its prior for the common location, which the likelihood does not see; frmtmb estimates the free directions instead, so class "Intercept" has no parameter to address and is refused with the reason. Both structures work with thres(x = ), thres(gr = ), cs(), disc, the post-fit methods and the one-step residuals. equidistant needs two thresholds per vector, since delta has nothing to measure on one. sum_to_zero holds a single threshold at zero and is fitted there, with no threshold parameter at all: under thres(gr = ) brms runs the same model, and on an ungrouped two-category response it is the logistic regression without an intercept. default_prior() lists delta with brms’s lower bound of 0 for the ordered families.

  • The log density with disc, with both structures and of acat() off the logit is brms’s: against brms 2.23.0’s compiled Stan program at 29 shapes, the value agrees to at most 2.6e-16 relative and brms’s gradient at frmtmb’s optimum is at most 2.3e-4 (rows 12e to 12h of test-brms-likelihood.R; dev/ordinal-findings.md lists the shapes brms’s own program fails on).

  • plot() of a conditional-effects object takes every argument of brms’s plot.brms_conditional_effects(): rug, mean, stype, jitter_width (with brms’s deprecation warning), the layer lists line_args, cat_args, errorbar_args, surface_args, spaghetti_args, point_args, rug_args and facet_args, theme, ask and plot. frmtmb draws with base graphics and has no ggplot objects to return, so plot = FALSE returns the plot objects undrawn. The layer lists are translated to base graphics (colour, fill, alpha, linewidth, linetype, shape, size, an error bar’s width, a rug’s sides, a surface’s bins, a facet’s ncol, nrow and scales), and an element with no counterpart is named in one warning. A ggplot2 theme is accepted with a warning that it is not applied. points = TRUE on a conditional_smooths() result is brms’s error.

  • plot() of a hypothesis takes every argument of brms’s plot.brmshypothesis(): nvariables (and its alias N, with brms’s warning), ignore_prior, chars, colors, theme, ask and plot. A fit has no prior draws, so ignore_prior changes nothing, as it changes nothing in brms without them.

  • fitted() takes sample_new_levels = "old_levels". Each unseen level reads one seen level of its grouping factor, chosen at random once per call as brms chooses it and as predict() chooses it, and every term of the factor reads that level: the row takes its fitted effects, and Est.Error their conditional variance. One choice serves every response of a multivariate call. At seeds 1 to 12 brms 2.23.0 and frmtmb choose the same level on (1 | g) with one or two unseen levels, on (1 | g) + (1 | h), on (1 | g) in mu and sigma, and within the row’s by-level of gr(g, by = f) (dev/ceplot-oldlevels.R, dev/ceplot-rev-oldlevels.R).

  • parnames(), brms’s deprecated spelling of variables(), answers on a fit with brms’s deprecation warning, once, whether or not brms is loaded. The generic moves from frmtmb.sample to frmtmb, so one generic serves fits and draws.

  • conditional_effects() draws a new level where it refused to: crossed (1 | g) + (1 | h) + (1 | g:h) with g and h at observed levels the data never has together, under band = "boot" and on draws. The bootstrap band has the Wald band’s width (0.89 to 1.08 at 200 refits, seeds 5 to 7, dev/ceplot-rev-bootratio.R). With one new level in the call, the band on draws equals brms’s at the same draws and seed to 4.4e-16 (dev/ceplot-crossed-brms.R); with two, brms draws level by level and frmtmb draw by draw, so the bands agree in law and not number. A new member of an mm() term with a by variable is drawn in its own by-level’s block: the bootstrap band is 0.83 to 1.11 of the Wald band’s width at 200 refits (the same script). brms 2.23.0 cannot draw such a level at all; it stops with one of three errors.

  • conditional_effects(categorical = FALSE) on an ordinal fit gives brms’s warning “Predictions are treated as continuous variables”. The default per-category display gives none. This is a deliberate divergence from brms, decided by the user: brms’s default display is the expected category number and warns on every default call, and its own warning calls that display likely invalid for ordinal families and asks for categorical = TRUE, which is frmtmb’s default.

Formula grammar

  • An addition term takes an expression, as brms evaluates it on the data rows: weights(wt * 2), rate(time * 2), se(s / 2), trunc(lb = lb - 1), subset(x > 0), index(id * 3). Before, the expression went into the model frame as a formula term, where wt * 2 is an interaction, and the fit stopped with “invalid model formula in ExtractVars”. A single value is used for every row, so trunc(lb = min(y) - 1) works too, and a value of any other length is refused by name. Fit, predict(), fitted(), simulate() and conditional_effects() read the expression the same way, in a multivariate model too. Every variable an addition term reads must be a column of the data, as brms requires: trials(k), trials(k + 0), weights(wt * k) and rate(time * k) with k defined only outside the data are refused with a message that names k and asks for it in the data (0.66.0 refused them too, as a scalar that is not a column, or stopped with “invalid model formula in ExtractVars” where the expression was not a valid formula term). Predictions on newdata and refits read the term again, and an outside value can be missing or changed by then, for example after saveRDS() and readRDS() in another session; in trials() that change would be silent. Breaking for one case 0.66.0 read: a bare name such as weights(w) with w a full-length vector outside the data is now refused too. Function calls such as log() are allowed, and predictor formulas and offsets keep R’s rule of looking in the formula environment. A name that R finds only as a function, as t in weights(t * 2) without a column t, is refused by the same rule. A prediction reads an addition term from newdata alone. The interval bound of cens(x, y2) takes one value per row, as brms requires.

  • weights(w, scale = TRUE) scales the weights to a mean of one on the rows the response reads, as brms does.

  • brms’s deprecated 0 + intercept is accepted with brms’s warning (“Reserved variable name ‘intercept’ is deprecated”) and read as brms reads it, a data column of ones whose coefficient is intercept. Newdata needs no such column (ledger rows standata:970, :974).

  • ar(), ma(), arma(), cosy() and unstr() exist as functions that return the term, as in brms. Added to a formula, bf(y ~ 1) + arma(x), the object is refused with brms’s message (ledger row brm:112). brms’s two ways to write the terms apart from the formula work: bf(y ~ x, autocor = ~ ar(t, g)) and bf(y ~ x) + acformula(~ ar(t, g)).

New arguments and answers

  • frm(drop_unused_levels = ), brms’s argument. With FALSE an unused level of a factor predictor gives a column of zeros, which frmtmb drops with its rank-deficiency message (ledger row standata:1133: brms keeps the column).

  • autocor() on a fit, brms’s deprecated accessor, returns NULL with brms’s deprecation warning, as brms does for every fit since 2.11 (ledger rows brmsfit-methods:112, :113).

  • predict(newdata = ) under ar()/arma() with cov = FALSE answers when the response is NA in some rows, or absent. As in brms’s .predictor_arma(), the recursion runs in each group’s time order and a missing response is filled with a draw from the family at its shifted mean, so the rows after it read that draw’s residual. Before, the call was refused by name (ledger row brmsfit-methods:747). fitted() diverges from brms here, on purpose: brms fills with a draw per posterior draw, so its fitted() carries the fill’s spread, and a maximum likelihood fit has no draws to carry the fill through. fitted() fills with the expected value, which is the mean of brms’s fill at fixed parameters, and its Est.Error is the parameter uncertainty alone (see ?fitted.frmtmb_fit). frmtmb.sample’s posterior_epred() fills with draws, as brms does.

Bug fixes

  • A hurdle_cumulative() fit’s draws are named b_Intercept[k] in frmtmb.sample, as the other ordinal families’ are: the family now declares its inverse threshold map. They were tau_raw_k.

  • conditional_effects() of a response whose predictor reads mi(x, idx = ) stops with the reason: the grid holds idx and index() at one reference value, so every grid row names the same row of x. It stopped with “Could not match all indices” from inside the prediction. brms 2.23.0 stops on the same grid.

  • A grouping term (1 | g:h) on integer or character columns predicts on new data. g:h was evaluated as R’s sequence operator, so every predict(), fitted() and conditional_effects() on new data stopped with “non-conformable arrays” (and a “numerical expression has 3 elements” warning escaped), or reported observed combinations as new levels. It is now the interaction the fit reads.

  • conditional_effects() varies a variable the model reads through a transform, x in poly(x, 2) or z in log(abs(z) + 1), as brms does. The model frame keeps only the transformed columns, so naming such a variable stopped with “not stored in the model frame” and the default display found nothing to draw. The fit keeps the raw variables beside the model frame for it.

  • conditional_effects() and emmeans() on a model with an offset() stopped with “non-numeric argument to mathematical function” and “undefined columns selected”: the model frame held the column offset(log(time)) and not time, so the grids had no time. The frame now holds the offset’s variables, and both grids hold them at their means, as brms’s do. emmeans() includes a predictor’s offset at the grid’s value, log(mean(time)) or the value at = sets, which is emmeans’s own .offset. column and what brms’s emmeans() gives. An offset inside a nonlinear parameter’s formula is left out of the body’s emmean, as brms leaves it out, and epred = TRUE includes every offset once. One divergence, on purpose: over the responses of a multivariate fit with no resp =, each response gets its own offset, where brms’s single .offset. column adds one response’s offset to every response. An offset’s variable is not a default display of conditional_effects(), as in brms, and a factor read inside an offset, as in offset(log(as.numeric(f))), no longer lets model.frame()’s “is not a factor” warning escape.

Extension API

  • arma_cond_fill_dpars() gives a response’s parameters on newdata for a predictive draw, filling a missing cov = FALSE response as brms does, arma_cond_fill_epred() gives one draw’s expected response with that fill, and response_codes_newdata() codes a response read from newdata as the fit coded its own. All three are on ?frmtmb-sampling-api, for frmtmb.sample.

frmtmb 0.66.0

Six lanes of brms parity, each with an adversarial review and one to three punch rounds: sampfix, fams2, aterms2, formula2, defects and postfit2. Each lane’s dev/<lane>-findings.md has the validation, the numbers and the scripts, and dev/reviews/2026-09-29-<lane>.md has the review; dev/round-20260929b.md is the round’s record. Silent wrong answers fixed: the display of an observed group under re_formula = NULL, the bootstrap of a model with a smooth, the linear predictor of an ordinal cs() fit, and the residuals of a missing mi() response. In brms’s own ported suite, bin 1 now passes 368 of 494 assertions, up from 275 at 0.65.0.

Breaking changes

  • frm_bootstrap() never redraws a smooth (user decision, 2026-09-29, withdrawing the whole-model default of 2026-09-24). It now reads re_formula exactly as predict() and simulate() do. A smooth, gp() or hsgp() term, one indexed by a grouping factor included (s(g, bs = "re"), s(x, g, bs = "fs"), a t2() with an re margin), is a term of the formula and not a group effect: every replicate holds it at its fitted coefficients and the refit estimates it again. NA, ~0 and ~1 redraw every true group-level effect, NULL holds all of them, and a one-sided formula holds the terms it names. The old NA default redrew each smooth from its prior, which treated a smooth as a group effect, contrary to the package’s own re_formula rule, and turned the spread of the fitted curve into the spread of random curves. On dev/simnewdata-boot.R’s y ~ s(x) (200 rows, 40 refits) the bootstrap SD of the smooth’s SD parameter was 1.4376 at NA and 0.3693 at NULL; it is 0.3693 at both now, and the replicates are identical (dev/postfit2-p1-simboot.R). confint(method = "boot") calls frm_bootstrap() and changes with it on a model with a smooth.

    conditional_effects(band = "boot") inherits the rule. Its band on y ~ s(x) was 10.2 to 17.8 times the Wald band; it is 0.73 to 1.03 times now, the same under re_formula = NA and NULL, and 0.75 to 1.03 times on y ~ s(x) + (1 | g) at the population, at a new group and at an observed one, where the old band missed its own estimate (dev/postfit2-p1-smooth.R, 60 refits).

  • nobs() is brms’s nobs(): the rows of the data, after the rule for missing values and before any subset() cut, and nobs(fit, resp = ) one response’s rows. It refused resp before. On a model with subset() it is therefore not the count the likelihood sums over. logLik()’s nobs attribute, which BIC() reads, and df.residual() keep that count, so BIC() is unchanged; a hand-written penalty k = log(nobs(fit)) on such a fit now uses the data’s rows, as it does on a brms fit.

  • fitted(scale = "linear") on an ordinal fit with a cs() term returns one layer per threshold, n x 4 x (K - 1) named eta1, eta2, … as brms does; on a multivariate fit the layers stack with the other responses’, as in brms. It returned the shared predictor alone, which is the predictor at no threshold of the model, because a cs() term moves each threshold’s predictor by its own coefficient. The layers are the ones the density reads: on an sratio() fit, P(Y = 1) is F(tau_1 - eta_1) of the first layer to within rounding.

  • The prior table spells a smooth’s unpenalized column and a nonlinear parameter’s intercept as brms does, sx_1 for s(x).fx1 and Intercept for (Intercept), and lists a block’s coefficients in brms’s order, sorted by name. set_prior(coef = ) takes either spelling.

  • The ordinal families’ draws in frmtmb.sample carry brms’s names (b_Intercept[k], bcs_<column>[k]); see its NEWS. frmtmb.sample needs this frmtmb for them.

New families

  • xbeta(), zero_inflated_beta_binomial() and hurdle_cumulative(), with brms 2.23.0’s parameters, links and defaults. Each reaches fitted() (brms’s posterior_epred()), predict(), simulate(), conditional_effects(), emmeans(), default_prior(), frm_compat() and frm_sample(). The log-likelihood of each agrees with brms’s compiled Stan program at the same parameters to 1e-13, and zero_inflated_beta_binomial() agrees with glmmTMB’s betabinomial with ziformula to 3e-9 in the log-likelihood.
    • xbeta() is the extended-support beta for a response in [0, 1] with exact 0s and 1s. Its end masses are an incomplete beta function, taken from a continued fraction because the third derivatives of RTMB::pbeta() are NaN at some ordinary points. With RTMB::pbeta(), each of ten fits with a random effect stopped at “NA/NaN gradient evaluation”. RTMB::pbeta() is used only near the mean at large shapes, where its third derivatives are finite, and never at the mean itself, where all its derivatives are NaN. The result agrees with stats::pbeta() to 1e-12 (relative) for shapes from 1e-3 to 1e7, at the mean included. The interior density does not use RTMB::dbeta(), whose gradient is NaN past a shape sum of about 1e3, so xbeta() fits precise data (tested at phi 2e4, and at 2e5 with kappa held).
    • When the response has no exact 0 or 1, only the interior shape places kappa. The xbeta() fit warns when it does not: when kappa runs to 0 at every row, or when the standard error of a coefficient of kappa is above 10 on the log scale or not finite.
    • hurdle_cumulative() codes its response 0..K, with 0 the hurdle and the first level of an ordered factor. disc is held at 1 unless the formula models it. thres(gr = ), cs() and a threshold other than "flexible" are refused. The likelihood cannot tell an intercept in disc apart from the scale of the thresholds. brms holds it with a normal(0, 1) prior; frmtmb warns unless a prior holds it.
  • cse(), brms’s second name for cs(), is accepted.
  • beta_binomial() has a variance function, so residuals(type = "pearson") works on it. It used to be refused with “has no variance function”.
  • The summary line of an ordinal family names the links of its other parameters, as in cdf = logit; hu = logit; disc = log.

New addition terms

  • brms’s addition terms subset() and index(), and the predictor mi(x, idx = ). In a multivariate model, y1 | subset(s) ~ x fits y1 on the rows where s is TRUE, so each response has its own rows, design, smooth bases and grouping levels. A missing value does not drop a row that every response using the variable leaves out, brms’s na_omit() rule. x | mi() + index(id) names the rows of an imputed response and mi(x, idx = ref) reads them by value, which brms requires when either response uses subset(); brms’s refusals are brms’s words. The log-likelihood equals brms 2.23.0’s compiled program at the same parameters, with and without mi(idx = ) (test-brms-likelihood.R, rows 25 and 25b), and a subset model is the sum of the separate fits. fitted(), predict() and frm_linpred() on such a model need one resp =, as brms’s do, and on newdata keep the rows where the subset is TRUE. In a univariate model subset() filters the rows. See “Response subsets, subset() and index()” in ?frm for what is refused and the three differences from brms, all levels that occur only outside a response’s rows.
  • brms’s rate() on poisson(), negbinomial() and geometric(): the expected count is mu * d, and the negative binomial’s shape is multiplied by d too, as in brms. On poisson() with the log link it is the offset(log(d)) model bit for bit. The log-likelihood equals brms’s compiled program on all three families (test-brms-likelihood.R, row 24). fitted(), predict(), simulate() and the residuals read the exposure; newdata must hold it, and it must be positive there too, as brms requires.
  • brms’s deprecated cat(K) is read as thres(K - 1), with brms’s warning; cat() beside thres() is refused.

Formula grammar

  • A distributional parameter can be equated to another, brms’s bf(y ~ x, sigma1 = "sigma2"), also spelled lf(sigma1 = "sigma2"). In a mixture the components then share one parameter, sigma2; sigma1 reads its coefficient, so the model has one parameter fewer and is the same function as the hand-written model with one shared sigma. fixef(), get_prior() and par_template() hold sigma2 alone, and variables() and summary() list sigma1 beside it with its value, as brms lists its transformed parameter. brms’s five rules apply with brms’s messages; theta1 = "theta2", which stops brms with an internal error, is refused by name. brms’s log density at the maximum likelihood estimate equals logLik() (test-brms-likelihood.R, row 17c).
  • cmc, brms’s switch for cell-mean coding, on bf() and lf(). bf(y ~ 0 + g, cmc = FALSE) keeps the factor’s contrasts (treatment, or polynomial for an ordered factor) and drops only the intercept column, in the population-level and the group-level terms, a multi-membership term included, with brms’s design and brms’s likelihood (row 24). re_formula names such a term as it was fitted. It is refused on ar1(), cs(), toep() and the distance structures where it changes a column, because they read one coefficient per level and have no brms counterpart; the refusal names the columns of both codings. lf(cmc = ) was refused with “lf() takes two-sided formulas”, which was not the reason.
  • y ~ . is expanded against the data, as brms does, in frm(), get_prior(), default_prior(), par_template() and frm_simulate(). It was refused. Each parameter formula is expanded on its own, so sigma ~ . includes the response, as in brms (row 25). The fit keeps the expanded formula, so update(fit, newdata = ) refits the same model, as brms’s update() does.
  • A list of families as family =, one per response of a multivariate model, as in brms: family = list(gaussian(), poisson()) (row 26). An entry fills only a response that has no family of its own, which is also brms’s rule for the family argument.
  • update() reads a bf() delta as brms does: update(fit, bf(~ ., family = acat())) keeps the formula and changes the family, and bf(. ~ . + z, sigma ~ z) updates the location formula and adds the parameter formula. It was refused as a formula with no response.
  • lf() takes what bf() takes besides formulas, as brms’s does: a parameter fixed to a constant, lf(sigma = 2), or equated to another.

Post-fit functions

  • conditional_smooths() draws each smooth term (s(), t2()) on its own, which is brms’s function of that name, with brms’s grid, keys ("mu: s(x)", "sigma: s(z)", "mu_a: s(x)") and columns. On a fit, estimate__ is the term at the estimate and se__ its delta-method standard error over the term’s coefficients. A term of two numeric covariates is a surface, and too_far trims it. Every smooth is drawn, a factor-smooth s(x, g, bs = "fs") included, which is the re_formula = NA rule of 0.65.0. At brms’s parameter values (a fixed_param brms fit at the frmtmb estimate) the curve equals brms’s to 3.8e-15 on s(x), s(z, by = f) and t2(x, z), with identical rows, columns and attributes (dev/postfit2-brms-compare.R). The help page of conditional_effects() said that function “also covers what brms calls conditional_smooths()”. It did not: it draws the expected response, which on y ~ f + s(x) + s(z, by = f) sits 0.0213 above the s(x) term with a standard error up to 1.214 times the term’s (dev/postfit2-before.R). The sentence is gone.
  • make_conditions() and update_adterms(), brms’s helpers. Their output is identical() to brms’s on 5 and 9 inputs. update_adterms() also takes a bf() formula and keeps its other parts.
  • conditional_effects() takes brms’s surface, too_far, select_points and spaghetti. surface = TRUE was refused before; it now draws a two-numeric effect over both ranges, and plot() draws the surface as an image with contours. select_points drops the observations far from the values the curve holds the numeric predictors at. It measures the predictors only: brms also measures the RESPONSE against its mean, which hides the scatter the points are there to show (5 observations kept here where brms keeps 3 at select_points = 0.1, the 2 dropped by the response alone). spaghetti = TRUE needs draws, so on a fit it needs band = "boot", one curve per refit, and is refused by name with any other band.
  • A nonlinear predictor gets the default Wald band. conditional_effects() refused it before and named band = "boot". The band is the delta method through the Jacobian that frm_lp_basis() tapes, which agrees with central differences to 2.1e-11 relative. Its standard error matched the standard deviation of 20000 curves drawn from the fit’s covariance: median ratio 1.0014 and 1.0008 over 100 grid points for two seeds, against a Monte Carlo error of 0.005 (dev/postfit2-nlmc.R). A display that runs through several predictors (the mean of a zero-inflated family, say), or a new level of a group-level term the displayed predictor itself reads, still needs band = "boot", and says so; a group only another parameter reads (sigma ~ (1 | g)) does not.

New arguments and answers

  • residuals() takes brms’s newdata, the response there minus fitted(newdata = ) with that value’s standard error, and allow_new_levels. It answers a multivariate fit in brms’s n x 4 x nresp array, and resp narrows it; it refused both before. type = "osa" stays univariate.
  • ranef() takes brms’s pars and groups instead of refusing them.
  • family() takes brms’s resp; print() of a family takes brms’s links and newline, and a mixture prints as brms prints it, Mixture and then each component.
  • fitted() takes sample_new_levels = "gaussian", which is what its interval at an unseen level already was; "old_levels" and "uncertainty" are refused by name, since this Wald summary has no draws of the group effects.
  • predict() takes sample_new_levels = "old_levels", which it refused: each unseen level borrows the effect of one seen level of its block, chosen at random once per call as brms chooses it, from every replicate’s draw of the group effects. "uncertainty" is still refused.
  • mixture() takes brms’s order: "none" and FALSE are what a fit does, "mu" and TRUE are refused, and any other value is refused in brms’s words.
  • categorical() reads a numeric response as categories in numeric order, as brms does. It claimed a response with ten categories had “fewer than two”.
  • default_prior() and validate_prior() no longer read the response’s values, as brms’s do not (user decision, 2026-09-29): a Beta() model on a response outside (0, 1) gets its table, and frm() still refuses the fit. An ordinal family is the exception, as in brms: its threshold rows are counted from the response, so a response it cannot read is refused.
  • summary() reports a gp() term’s standard deviation and length scale, under brms’s heading “Gaussian Process Hyperparameters” and brms’s row names, sdgp(gpx) and lscale(gpx), in $gp. The section had a printer and nothing to fill it, so neither number appeared in any summary. The length scale is on brms’s scale, the inputs divided by their largest distance; confint_varcorr() keeps it in data units.
  • A fit records the name of its data in attr(fit$data, "data_name"), and update(newdata = ) the new name, as brms does. summary() prints that name. A data frame passed by value through do.call() used to print its whole contents on the Data: line, and prints nothing there now.
  • update(fit, data = ) stays accepted and refits on the new data, as stats::update(), lme4 and glmmTMB read it; brms refuses that spelling. A recorded divergence (user decision, 2026-09-29).

Bug fixes

  • conditional_effects() under re_formula = NULL now reads each grid row’s groups by brms’s rule. brms predicts its display with allow_new_levels = TRUE: per grid row and per group-level term, the term reads its fitted effects where every one of its grouping variables is set to a level the fit saw, and a NEW level everywhere else. frmtmb used one rule for the whole grid: every grouping variable was a new group, even one conditions set to an observed level, and a variable varied as an effect ("x:g") was overwritten by a placeholder. Under band = "boot" every replicate then drew fresh effects into the first level, so levels 1 and 2 got the same band, bit for bit, and the band did not contain its own estimate. Found by lane sampfix on the draws method, which shares the construction. Now:

    • a row reads its groups’ fitted effects where it sets them to observed levels, in conditions or as an effect;
    • an unset variable, a level the fit never saw ("99", which the frame now keeps instead of NA) and a nested (1 | g / h) with g set and h unset each draw a new level, the last one within g’s own level;
    • rows that mix a level and NA in one grouping column each get their own answer, where they stopped with “New levels … NA”;
    • under band = "boot", a term read at an observed level is held at its fitted effects when the responses are simulated, and the other group-level terms are redrawn fresh (the smooths are held; see the breaking change above). A term read at an observed level in some rows and at a new one in others is refused by name.

    On y ~ x + (1 | g) with a group SD of 2 the refits’ mean at an observed level sat 0.857 modes from its estimate before and 0.089 after, and the boot band went from 3.117 to 0.998 times the Wald band (dev/postfit2-celevel.R). Only group-level terms enter this rule: a model with none reads nothing new under re_formula = NULL, so its band there is its band under NA. Reusing a bootstrap (boot = attr(ce, "boot")) for a call whose refits are simulated the other way is refused, saying so: a population call and an observed-level call can have the same grid.

    Two kinds of term read a row in their own way, and a third is refused:

    • gr(g, by = f) is one term per level of f, and a row now reads only the term of its own level. The first build of this rule read every level’s term in every row, so band = "boot" and draws refused every re_formula = NULL display of such a model, even at an observed g. With g unset or unseen, the row draws a new level with the SD of its own f level. On draws at brms’s parameter values, at g = "99" with f = "a" or "b", the curve and band equal brms’s (sample_new_levels = "gaussian", same seed) to 8.9e-16 (dev/postfit2-p2-brms.R). With g unset, brms 2.23.0 stops (“missing sd_g__Intercept:f”); frmtmb answers as for "99".
    • mm(g1, g2): the grid held unset members at their first level, an observed group, without a word. They are now NA and read a new level, as brms reads them. Members with the same new value share one draw, and their weights add. At nothing set, the Wald band had the width of level 1’s curve (0.58 to 0.72); it is 3.73 to 3.75 now. The draws band equals brms’s at the same draws and seed (3.52 to 3.63). At an observed g1 = g2 = "2", the boot band was 5.1 to 6.0 times the Wald band, near the population curve, because the term was redrawn; the term is held now, and the ratio is 1.03 to 1.23 (dev/postfit2-p2-checks.R). A row with one member observed and one new is refused by name on band = "boot". Beside a plain (1 | g1), with g1 unset or unseen, both terms now draw a new level on band = "boot" and on draws; they were refused. The new-level offsets have the model’s variance in both packages: 1.0085 against brms’s 0.9994, SE 0.013, over 12000 draws (dev/postfit2-p3-brms.R).
    • A factor-smooth, or a smooth with a by factor, set to a level that the fit never saw returned an NA estimate under re_formula = NULL, and under re_formula = NA a message that called the level NA. Both now stop and name the variable, the level and the smooth. A grid that leaves such a factor unset (a (1 | g) beside s(x, g, bs = "fs") under re_formula = NULL) stops too.

    Reusing a bootstrap under another re_formula is refused: on (1 + x | g) at an observed g, a re_formula = NULL bootstrap was accepted by a ~ (1 | g) call, whose estimate is a different curve.

  • residuals(), pp_check() and dharma_residuals() no longer read a missing mi() response as an observation. The frame holds the placeholder 0 there, and residuals() reported 0 - mu at those rows, where brms’s is NA. The residual there is NA now, in all four columns, and pp_check() and dharma_residuals() leave the row out, pp_check() with brms’s warning. The univariate y | mi() ~ x had the fault before; a multivariate fit refused residuals() before. residuals(type = "osa") on such a response is refused by name; it died inside TMB.

  • A factor column of newdata holding the levels as numbers (Trt = 0) is read as those levels, as brms reads it. It died with R’s “contrasts apply only to factors”. Prediction on a slice of the fitted data whose factor carries a contrasts attribute no longer warns “contrasts dropped from factor”.

  • y ~ x + set_rescor(TRUE) and the other model-building helpers (lf(), nlf(), set_mecor()) written inside a formula are refused by name, in brms’s words. They died in model.frame() with “invalid type (list) for variable”.

  • (cs(x) | g) is refused by name: on a family without category specific effects as cs() is, and on an ordinal family because a group-level category-specific effect is not supported. It died with R’s “could not find function "cs"”.

  • se() on a family whose density does not read it is refused even when its column is missing from the data, as brms refuses it from the formula alone. The missing column was reported first and hid the refusal.

Extension API

  • ?frmtmb-sampling-api adds the engine of conditional_smooths() (cs_build(), cs_coef(), cs_frame(), cs_finalize(), cs_probs()), brms’s display options (ce_spaghetti(), ce_spaghetti_check(), ce_check_distance()) and brms’s per-row, per-term rule for which group a grid row reads (ce_level_plan(), ce_plan_eval(), ce_plan_has_new()), which replaces ce_new_level_spec(), ce_boot_grids() and ce_draw_new_levels() in both methods. Those three are removed, with ce_new_level_key(): nothing called them any more. An extension that did calls ce_level_plan() and ce_plan_eval() instead. ce_grids_build() takes surface and too_far, ce_frame() takes surface, and ce_finalize() takes select_points, pred_vars and draws_by_eff. The effect frames carry brms’s "points" attribute.
  • subset_resp_check() and subset_newdata() join the sampling API (?frmtmb-sampling-api), for the draws methods of a subset() model.
  • The four built-in ordinal families and thres(gr = ) now declare the inverse of their threshold map, post$ord_thresholds_raw, beside the forward map post$ord_thresholds. brms_fixef_rows() carries it for each threshold and cs() block. frmtmb.sample reads it to store an ordinal fit’s draws under the names variables(fit) gives them. An ordinal family from another package that declares only the forward map keeps its internal draw names there.
  • ?variables no longer lists the ordinal and cs() exception for draw columns, which frmtmb.sample removes. It now says which draw columns are not brms’s: the covariance parameters, stored as sampled (theta_1).

frmtmb 0.65.0

Seven lanes, each with an adversarial review. Each lane’s dev/<lane>-findings.md has the validation, the numbers and the scripts, and dev/reviews/2026-09-29-<lane>.md has the review. Three silent wrong answers are fixed: cs() on a factor, re_formula = NA on a fit with a factor-indexed smooth, and the standard error of a category probability on any fit with a smooth.

Breaking changes

  • re_formula = NA now keeps EVERY smooth, which is brms’s rule: re_formula removes the (x | g) group-level terms and nothing else. Three kinds of smooth were dropped before, and each is indexed by a grouping factor: s(g, bs = "re") and s(x, g, bs = "re"), the factor smooth s(x, g, bs = "fs"), and a t2() with an re margin. brms::posterior_epred(re_formula = NA) is bitwise the same as at re_formula = NULL on a fit whose only group-indexed content is one of those, and differs on s(x) + (1 | g); frmtmb now agrees on both. (1 | g) is still dropped under NA, unchanged.

    This changes numbers, silently before and reported here:

    • predict(), fitted(), frm_linpred(), frm_lp_basis(), posterior_epred() and posterior_linpred() at re_formula = NA now return the full prediction on a fit whose only group-level content is such a smooth. On y ~ t2(x, g, bs = c("cr", "re")) the old answer was the intercept at every row.
    • conditional_effects() passes re_formula = NA, so its curves change on those fits. On y ~ s(x, g, bs = "fs") the drawn curve was FLAT, the whole fitted structure having been removed; it is now the curve of the grouping factor’s reference level, which is where the display holds every predictor it is not varying.
    • emmeans() on such a fit now carries the grouping factor in its reference grid and averages the mean over its levels, where it used to drop the term. emmeans(re_formula = NULL) on the same fit works now too; it was refused for want of that column.
    • simulate(re_formula = NA) no longer redraws those smooths, so the default pp_check() changes on any fit that has one. The draws were far more spread out than the model says: on y ~ s(x, g, bs = "fs") the per-row standard deviation of the draws over the fitted sigma was 2.93, and the pooled standard deviation over the data’s own was 1.34; both are 1.00 now. A pp_check() that looked mismatched on such a fit may now look right, and the earlier picture was the wrong comparison, not a finding about the model.
    • A smooth indexed by a grouping factor is kept, so newdata must carry its grouping column at every re_formula, and a level the fit never saw is refused. re_formula = NA was a way out of both and is not any more; the refusals say so. brms refuses both too, and not through its group-level machinery: the grouping factor of a smooth is an ordinary predictor there, so a missing column is a missing variable and an unseen level is “New factor levels are not allowed” whatever allow_new_levels says. frmtmb keeps one opt-in brms does not have: under allow_new_levels = TRUE an unseen level of an fs term takes the population curve, mgcv’s fs basis returning a zero row for a level it does not know. An re basis or margin has one column per fitted level and no such row, so an unseen level there is refused either way.
    • To read the population curve off a model with an fs term, predict at an unseen level of the grouping factor with allow_new_levels = TRUE. The functional regression section of vignette("case-studies") now draws its figure that way.
    • One fittable term is left with no newdata route at all: a factor smooth that also carries a by = factor, s(x, g, bs = "fs", by = f). mgcv’s random-effect split of that basis is one this version cannot invert, so predict(newdata = ), fitted(newdata = ), frm_linpred(newdata = ) and conditional_effects() stop with that named error at every re_formula, where re_formula = NA answered before by dropping the term. What it answered was the intercept at every row, so a flat line became a named refusal, not a curve became an error. In-sample fitted() and frm_linpred() on such a fit are unaffected.
    • A PARTIAL re_formula on a fit with such a smooth, for example re_formula = ~(1 | f) on y ~ s(x, g, bs = "fs") + (1 | f) + (1 | h), is now accepted: it drops (1 | h) and keeps the smooth, as brms does. It was refused, because NA used to drop the smooth and a partial formula could not say which of the two it meant; with every re_formula keeping the smooth there is nothing left to decide. predict(), fitted(), frm_linpred() and simulate() all take it. With it goes the last case of that refusal: ?frm_linpred also listed a car() or spde() field, but a field is a group-level term named by its grouping factor, (1 | loc), and was never refused. The help page now says so.

    frm_bootstrap() is unchanged. Its default is a whole-model parametric bootstrap that redraws every block including the smooths, which simulate() does not offer.

  • cs() on a factor or a character column was fitted and predicted on the factor’s INTEGER CODES, a silent wrong answer. The term is now expanded by stats::model.matrix() with the fit’s contrasts, so a discrete predictor becomes treatment-contrast dummies with one coefficient per dummy per category boundary, exactly as brms 2.23.0 builds its Xcs. What changes for a fitted model: a three-level factor gets FOUR coefficients where it had two, and their names move from fc[1], fc[2] (bcs_fc[k]) to fcb[1], fcb[2], fcc[1], fcc[2] (bcs_fcb[k], bcs_fcc[k]), which is what brms calls them. Every estimate moves with them: on the recorded construction the log likelihood went from -446.9233 to -446.6361, the value the same model written with hand-built dummy columns already reached. newdata is recoded against the levels of the FIT rather than against its own, so a one-row newdata holding factor("c") no longer returns the reference level’s probabilities, from fitted(), predict(), simulate() and conditional_effects() alike, and a level the fit never saw is refused by name. A CHARACTER cs() column did not fit at all before: as.numeric() on it gave NAs and the optimizer died on a NaN gradient. set_prior(class = "b") and default_prior() list and reach one row per dummy, under brms’s coef spelling.

  • y ~ x + cs(x) is refused instead of fitted. The global coefficient of a column and the category-specific coefficients of the same column span one direction twice: adding a constant to the first and subtracting it from each of the others leaves the likelihood unchanged. It was fitted, and reported standard errors of 2.7e5 on every coefficient involved. The message names the edit that fixes it, y ~ cs(x), which fits the same set of distributions. A factor on both sides (y ~ f + cs(f)) and a constant cs() column are refused the same way. This is a deliberate departure from brms 2.23.0, which builds both blocks and samples the ridge: a Bayesian fit has a prior to hold the ridge and maximum likelihood has none. vignette("brms-migration") records the departure.

    The check is a rank test, so it catches a column that only a basis or an expression spells differently (y ~ poly(x, 2) + cs(x), y ~ s(x) + cs(x), y ~ x1 + x2 + cs(x1 + x2), and two cs() terms that are the same column twice), and the message names the columns involved and the edit that works, which for poly(x, 2) + cs(x) is I(x^2) + cs(x) and for s(x) + cs(x) is to drop one of the two. It does NOT reach an mi() or me() term, whose design column is a zero placeholder at assembly filled later with observed-or-latent values: me(x, sdx) + cs(x) appears to be identified and mi(x) + cs(x) is not settled. mo() has a placeholder column too, but whatever fills it is a monotone function of the predictor’s categories, so mo() is tested against that category basis and y ~ mo(m) + cs(m) is now refused as well. It used to fit, buying 3 degrees of freedom for 6.4e-09 of log likelihood with NaN on all 9 standard errors. ?frm states the mi()/me() gap.

The convergence check

  • The convergence warning “Large maximum absolute gradient at the optimum” no longer fires on correct fits. It used to read one absolute number, frmtmb_control(grad_tol = 1e-3), against the largest gradient component, and that number does not mean the same thing on every design: the optimizer stops on a RELATIVE change in an objective that grows with the sample size, so the gradient at a good optimum grows with it too, and a parameter held on a bound by set_prior(ub = ) has a nonzero gradient by construction. Over 720 correct fits on nine designs at four sample sizes the old check warned on 289 of them, rising from 11 percent of the fits at 200 rows to 75 percent at 20,000. It now warns on none of them.

    grad_tol is read twice: first as the gradient trip-wire it always was, then on the objective’s own scale. A fit that trips the trip-wire warns only if one exact Newton step over the parameters no bound holds would still gain more than grad_tol in log-likelihood, and the warning reports that number, so it says how far short the fit is rather than only that a derivative is large. Every fit’s estimates, objective and optimizer status are bitwise unchanged; only the warning and diagnose() differ. The warning names every number it used and the parameter each belongs to, so on a bounded fit it no longer prints the projected gradient under a phrase that says “maximum”, and it no longer sends the reader to the one parameter the verdict excluded.

    diagnose() gains grad_proj, grad_proj_par, grad_bound_held and grad_headroom, names the bounds that hold a gradient, and judges the gradient by grad_tol instead of a hardcoded 1e-3 that ignored the setting. On an importance-corrected fit it reports no verdict at all, because frm(importance = ) optimizes a Monte Carlo estimate whose gradient no convergence criterion applies to, and there the clean verdict keeps being decided by the raw gradient against grad_tol, as it was before.

    The new criterion also reports what the gradient could not: a near-collinear design that nlminb calls converged at a gradient of 5.1e-3 is short by 0.528 log-likelihood units, which the Newton step confirms to 0.2 percent and which a re-optimization from the same point does not recover. What it does NOT cover is documented on ?frmtmb_control: the gradient is what admits a fit to the second reading, so a badly scaled column that holds the gradient down is never looked at, and the autoscale default is what closes that.

Refits and influence()

  • A refit inside the package now carries the FITTED model’s ordinal threshold count, and the per-level counts of thres(gr = ), instead of recounting them from the data it is given. influence() and cooks.distance() rebuild the design from a subset, so deleting the last observation in a category used to refit a model with one threshold fewer: the deleted unit’s row of the table held an NA, its Cook’s distance was NA, and the coefficients above the emptied category were reported one column early, with nothing but that trailing NA to show it. The deletion now refits the model that was fitted, and the row agrees with a fit that pins the count by hand.

  • A refit also carries the fitted model’s response CATEGORIES. The model frame drops a factor level that no row of a subset takes, which renumbers every category above it, so an ordered-factor response could give a different influence table from the same data coded as integers. Emptying an INTERIOR category, and a hand-written thres(K), were the two cases where it did. Integer, character and ordered-factor codings now give the same table, bitwise.

  • influence() and cooks.distance() now count the deletion refits that failed. A partly failed table warns with the count and the first reason; a table whose every refit failed is an error instead of a silent matrix of NA. Two refits cannot represent the fitted model at all and are refused by name: data = holding a response category, or a thres(gr = ) level, that the fit never saw, and groups = deleting a whole thres(gr = ) level.

  • influence() warns when deleting a unit drops a coefficient. A deletion refit can succeed and still estimate fewer coefficients than the full fit, because the model frame drops an unused factor level: the last row of a rare level disappears from the subset. Those cells of the influence table stay NA and cooks.distance() is NA for that unit, which it already did, in silence. The warning names the unit and the coefficients, and it fires for ANY factor level a deletion empties, not only a cs() one: y ~ x + f warns where it gave one silent NA before. A cs() factor level makes the case easier to meet, because cs() gives each level K - 1 coefficients rather than one.

  • frm_bootstrap(), refit(), frm_allfit(), anova(refit = TRUE), confint(method = "profile") and the autoscale pre-fit reuse the assembled design and already kept the count; that is now documented in ?frm_bootstrap and pinned by a test. simulate() followed by frm(), written out by hand, is not a refit and still counts from the data it is given, which ?frm_bootstrap now says.

Standard errors and the sampling API

  • A category probability’s Est.Error now carries the smooth’s uncertainty. fitted() on an ordinal, categorical or multinomial fit takes its standard errors by finite differences, and the coefficients it differenced never included a smooth’s. The differenced function was therefore “move the thresholds with the fitted curve frozen”, which is not the standard error of anything, while the analytic route used everywhere else has always carried the curve. Checked against Monte Carlo over the same joint covariance the delta method uses: on y ~ s(x, k = 8) with cumulative() the shipped value was 74 percent from the Monte Carlo standard deviation at the median cell, 0.37825 against 0.06509 on the first row, and is 2 percent from it now; on a s(x, g, bs = "fs") fit it was 50 percent from it, and understated rather than overstated, because the cross term between the outer parameters and the coefficients can go either way. The same fix applies to the standard error of an autocor(cov = FALSE) quantity.

    What is left is first-order curvature, not a missing term, and it is the same on every earlier release: the corrected standard error is within 2 percent of Monte Carlo on a population smooth and within 8 percent beside a group effect, the worst measured cell being s(x) + (1 | g) at re_formula = NULL.

  • rescor_row_loglik() lost the Student-t joint density at a large nu. It wrote lgamma((nu + K) / 2) - lgamma(nu / 2) and log(nu * pi) as they read, where the taped objective goes through lgamma_shift_diff(). The as-written difference of two lgamma() values loses its leading digits as nu grows and overflows to Inf - Inf above nu = 5.1e305, so the density left the gaussian limit it should approach and then stopped being a number. How far it drifted depends on the design, so no single figure is quoted here: dev/arcovsample-log/tnu.txt has one construction and dev/arcovsample-rev-log/13-rrl-ref.txt another, with the loss reaching hundreds to tens of thousands of log units before the NaN. A sampler reaches those values whenever nu is weakly identified, so frmtmb.sample’s log_lik() of a set_rescor(TRUE) Student-t model gave NaN for most draws. It now equals the objective’s own mvt_std_loglik() at every nu tried, and agrees with it to 2.8e-14 absolute over 200 NUTS draws whose nu reaches 3e306. logLik() and the fit itself were never affected: those use the objective.

  • Two exports for the sampling extension, documented with the rest of the contract on ?frmtmb-sampling-api: arma_cond_resp() names the responses carrying brms’s default cov = FALSE form of ar(), ma() or arma(), and arma_cond_dpars() gives an eval_dpars() list back with each such response’s mu moved to brms’s one-step conditional mean. They exist so that the pointwise log-density of such a fit and the taped objective share ONE definition of the shifted mean rather than each holding its own; frmtmb.sample’s log_lik() reads them.

Other fixes

  • frm_simulate(prior = ) used to fail inside vapply() with “values must be length 1” whenever one prior covered several parameters at once. It now says what it cannot do and names the alternative. This is about the shape of a prior entry, not one family: it is reached by class "Intercept" on an ordinal model with more than one threshold, by class "cor" on a block with more than one correlation, and by class "ar", "ma" or "cortime" above order 1. The underlying defect, one draw written into every parameter the entry covers, is refused rather than performed; an entry covering exactly one parameter still draws.

  • A cov = FALSE ar()/ma()/arma() group’s opening rows were described wrongly. ?frmtmb-autocor, the comment in R/autocor.R and the compatibility note all said a group’s first rows get no lagged term. Measured false: lag i first reaches the row at within-group position i + 1, so only the FIRST row of a group is unshifted, a row at position k carries the lags up to k - 1, and from max(p, q) + 1 on a row carries all of them. Measured over ar(1), ar(2), ar(3), ma(2), arma(2, 2) and arma(3, 1) on groups of unequal length, one of them a single row (dev/arcovsample-log/firstrows.txt). The likelihood was always this; only the sentences were wrong.

  • ?frmtmb-me documents what me() returns, and a roxygen block in R/ad-env.R no longer carries text after @noRd; both failed pkgcheck on the 0.64.0 release.

frmtmb 0.64.0

brms parity: every gap in the model menu that the last round listed is closed. Each lane’s dev/<lane>-findings.md has the validation, the numbers and the scripts that produced them.

New brms grammar

  • me(x, sdx) and me(x, sdx, gr = g) noise-free predictors, with brms’s meaning. The latent values are integrated by the Laplace approximation, which is exact for a gaussian response with linear me() terms. Several terms are correlated unless set_mecor(FALSE) is added. The names are brms’s: bsp_mexsx, meanme_mex, sdme_mex, corme__mex__mez and Xme_mex[i]. The log density agrees with brms 2.23.0’s Stan program to 1e-13 on five shapes. New data use the observed value of the noisy variable. brms’s prior classes meanme, sdme and corme are refused by name. vcov(cluster = ) now refuses me() fits, as its documentation already said, and anova() refuses fits whose me() calls differ.

  • thres() for the ordinal families. y | thres(K) ~ x fits K thresholds, and y | thres(gr = g) ~ x fits one threshold vector per level of g, with a count per level from the data or from thres(n, gr = g). The log-likelihood equals brms’s compiled program’s to 17 digits on all four families, and agrees with MASS::polr() and ordinal::clm() at the optimum. The thresholds have brms’s names, Intercept[g,k] and b_Intercept[g,k], and set_prior(class = "Intercept", group = "g") reaches one level. fitted() and predict() give probability 0 to a category past a row’s own, as brms does. cs() and residuals(type = "osa") are refused with gr. A threshold above the highest observed category is not identified without a prior, and the fit warns about it.

  • gr(g, by = f) fits brms’s by-split group-level term: one set of standard deviations and correlations per level of f, with each level of g in exactly one level of f. It is fitted as one block per by-level, with any structure whose density is a product over the grouping levels (us, diag, cs, ar1, toep, the spatial ones, and dist = "student"). Parameters take brms’s names (sd_g__Intercept:fa, cor_g__Intercept:fa__x:fa). ranef(), coef() and ngrps() keep one entry over every level of g, and a new level in predict(allow_new_levels = TRUE) takes the covariance of its own by-level. brms’s refusals are reproduced word for word. mm(g1, g2, by = cbind(f1, f2)) works the same way over the pooled levels. gr(g, by = f, cov = A) is refused by name, because brms correlates the by-levels through A, which is not a by-split. It agrees with lme4 and glmmTMB fitted with one indicator term per by-level.

  • ar(), ma() and arma() without cov = TRUE fit brms’s default residual-regression form, which was refused before. mu gains a regression on the group’s earlier residuals and each row keeps the family’s own density, conditional on each group’s first rows. The likelihood matches brms 2.23.0’s to 1e-15 relative, for gaussian and student, with weights(), cens(), trunc(), mi(), random effects and rescor = TRUE. It also matches stats::arima(method = "CSS") on one series. The coefficients are unconstrained, as in brms, and class ar and ma priors and bounds act on them directly. fitted(), predict() and residuals() use brms’s one-step mean, and simulate() runs the recursion over its own draws. Other families are refused in brms’s words.

  • 0 + Intercept is brms’s reserved intercept: y ~ 0 + Intercept + x fits the model y ~ 1 + x with the intercept as an ordinary class "b" coefficient, not centered. A class "b" prior reaches it and class "Intercept" is refused, as in brms. It works in the location, distributional and nonlinear-parameter formulas, factors take treatment contrasts, and newdata needs no Intercept column. bf(center = FALSE) and lf(center = FALSE) are the same mechanism. Ordinal families refuse 0 + Intercept, as brms does. The maximum likelihood fit is unchanged.

Multivariate models

  • Any number of bf() formulas can be summed with +. The sum of bf(y1 ~ x), bf(y2 ~ x) and bf(y3 ~ x) used to stop with “non-numeric argument to binary operator”, because the formula and the multivariate formula had two different + methods. lf() and nlf() take brms’s resp = to name the response they belong to. summary() of a multivariate fit prints brms’s Family: MV(...) line and every response’s formula. A family added after several responses fills only the responses that have none; brms instead gives it to every response.

  • student() with rescor = TRUE fits brms’s multivariate Student-t: one nu shared by all responses (named nu, with no response), a sigma per response, and distributional sigma allowed. A formula or a constant for nu, and a mix of gaussian and student responses, are refused as brms refuses them. It agrees with mvtnorm::dmvt() and with brms’s own log_lik(). predict() draws the responses jointly from the multivariate t.

  • Ordinal responses in a multivariate model. cumulative(), sratio(), cratio() and acat() responses keep their own thresholds (b_o_Intercept[1], set_prior(class = "Intercept", resp = "o")) and can share |ID| group effects with other responses. Other families with extra parameters (cox(), mixture_mvn()) stay refused there, now with the reason.

  • fit_extras() takes resp, and the new sampling-API export rescor_row_loglik() is the joint row density of a rescor fit.

Families

  • New families hurdle_negbinomial() and zero_one_inflated_beta(), with brms’s parameters, links and defaults. Both reach fitted() (brms’s expected response), predict(), simulate(), pearson residuals, conditional_effects(), emmeans() and frm_sample(). The hurdle negative binomial agrees with glmmTMB’s truncated_nbinom2 hurdle to 7e-11 in the log-likelihood, and both families agree with brms’s densities to a few ulps. A zero-one-inflated beta fit whose response cannot identify coi warns and names the remedy.

  • BREAKING: sratio()’s thresholds are unconstrained, as brms declares them, and no longer kept increasing. frmtmb held them as (first threshold, log increments), like cumulative(), so where the maximum has crossing thresholds the fit stopped on the ordering boundary with two thresholds equal, and was not brms’s mode. brms’s own inhaler example with cs(treat) was one such fit: its log-likelihood rises from -455.115 to -451.257. Where the thresholds do not cross, a fit is unchanged to optimizer precision. The internal tau_raw of an sratio fit is now the thresholds themselves, so a newparams or start written for the old storage means something else. A class "Intercept" prior on sratio’s thresholds carries no Jacobian any more, as in brms and as on cratio().

emmeans

  • emmeans() takes brms’s dpar, nlpar, resp, epred and re_formula. A nonlinear model is supported: nlpar = averages that parameter’s linear predictor, and the whole mu or epred = TRUE uses the delta method through frm_lp_basis(). A multivariate fit without resp stacks its responses as brms’s rep.meas factor. See ?frmtmb-emmeans.

  • Silent wrong answers in emmeans(), now fixed: dpar = was ignored and mu returned; type = "response" did not apply the inverse link; s(), t2() and mo() terms were left out of the means; y | weights(w) was read as a response transformation; re_formula was ignored.

  • emmeans now shows frmtmb’s reason when it refuses, instead of “Perhaps a ‘data’ or ‘params’ argument is needed”.

Other fixes

  • A class "Intercept" prior with group = on an ordinal model without grouped thresholds is refused. It used to be applied to every threshold without a word.

  • residuals(type = "osa") on a zero-inflated, hurdle or zero-one-inflated family is refused by name. It used to fail with base R’s “comparison (==) is possible only for atomic and list types”.

  • frm_compat() no longer calls simulate() refused for hurdle_poisson(), compois() and tweedie(), whose simulators work.

  • summary() lists a grouping factor’s standard deviations before its correlations when the factor has several blocks, as brms does.

frmtmb 0.63.0

  • simulate() takes newdata, and pp_check(newdata = ) on a fit answers as brms does instead of refusing. The draws are for the new rows; pp_check() plots them against the newdata’s own response and reads group and x from it, as brms reads them. A newdata row with a missing response is dropped with brms’s warning, and a newdata without the response is refused for a ppc type, as brms refuses it. A grouping level the fit never saw is refused, as in predict(), unless allow_new_levels = TRUE, a new argument of simulate(); under re_formula = NA every level is redrawn anyway, so an unseen one needs nothing. A residual correlation term such as ar(time, gr = g, cov = TRUE) is rebuilt on the new rows, so rows of one group are drawn jointly at their lags, counted in the fitted time levels; a time the fit never saw is refused. brms counts a lag by a row’s position among the newdata rows of its group, so the two differ when newdata skips a time: rows at times 2 and 4 correlated at 0.389 here and 0.656 in brms on one AR(1) fit. The departure is deliberate, because under brms’s reading the correlation of two rows changes when a third row is added to newdata. mixture(groups = ), mixture_mvn(), and the hidden Markov and learning families of frmtmb.latent and frmtmb.learn refuse newdata: their draw walks the fitted groups or sequence, and mixture_mvn() goes through the same structured slot.

  • BREAKING: simulate() reads re_formula as predict() reads it. NULL keeps every group-level term, NA, ~0 and ~1 keep none, and a one-sided formula keeps the terms it names. A term that is not kept is REDRAWN from its estimated distribution in every replicate, as NA always did. Through 0.62.0 every formula meant “keep everything”, so simulate(re_formula = ~1) gave the conditional draws while predict(re_formula = ~1) gave the population prediction. pp_check() and dharma_residuals() on a fit pass re_formula to simulate() and change with it. A term the fit does not have, and a partial formula on a fit with a factor-smooth term, are refused with predict()’s words, and so is a re_formula that is not NULL, NA or a formula, which used to condition on everything with nothing said. When a formula keeps some columns of a term and drops others, as ~ (1 | g) does on a (1 + x | g) fit, the dropped columns are drawn given the kept ones.

  • BREAKING: simulate(re_formula = NA) keeps a population smooth. It redrew the penalized coefficients of s(), gp() and hsgp() terms from their smoothing prior, which replaced the fitted curve with a random one: on y ~ s(x) the draws spread with an sd of 2.13 around a fit whose residual sd is 0.28. pp_check() on a fit defaults to re_formula = NA, so its check of any smooth model compared the data with those draws. A factor-smooth term is group-level and is still redrawn.

  • frm_bootstrap() is unchanged: by default it still redraws the group effects AND the penalized coefficients of every smooth, gp() and hsgp() term in every replicate, a whole-model parametric bootstrap, and its draws are identical to 0.62.0’s at the same seed. re_formula = NULL is the bootstrap that conditions on the fitted random effects and smooths and redraws the noise alone. ?frm_bootstrap now says which terms each setting redraws.

  • The pp_check(type = "error_binned") refusal on a multinomial fit calls the response a set of counts over categories, not a category.

Five defects filed in the 0.62.0 round (lane wt-predfix; dev/predfix-findings.md has each reproduction and measurement).

  • BREAKING: a fit with a badly scaled predictor is now standardized by default. frmtmb_control(autoscale = ) defaults to NULL, which runs the autoscale = TRUE pre-fit when a qualifying column has a standard deviation below 1e-3 and its coefficient is optimized directly, or when a column that carries a random slope ((1 + x | g)) has one below 0.05. Without it such a fit could stop short, report convergence 0 and say nothing: a poisson y ~ 0 + x with x on a 1e-6 scale sat 62.58 log-likelihood units below glm(), the same happened with an intercept and a group effect in every family measured, by up to 91 units, and a random slope fell short by up to 14.4 units at a spread of 0.01.

    The pre-fit now also rescales a random slope’s column in Z, with its log standard deviation and its effects, for us(), diag() and the Student-t blocks; before, autoscale = TRUE rescaled the fixed effect only and a random slope on a 1e-6 column was still up to 6.35 units short. A random slope in any other covariance structure, or on a column the fixed effects do not carry, is not rescaled. The pre-fit’s own warnings are no longer shown; only the reported fit’s are.

    The default never reports less than autoscale = FALSE would. When the pre-fit errors, the default fits the model as autoscale = FALSE does. When the pre-fit stops without converging in its coefficients (a completely separated fit does), the default fits both from the pre-fit and without it and reports the one without it, with its warnings, unless the other has the better likelihood and either warns at least as often or is a verified optimum. verbose = TRUE says which. With autoscale = TRUE, which you asked for, a pre-fit error stands and names autoscale = FALSE, and a pre-fit that did not converge warns.

    A mu coefficient under REML = TRUE or profile = TRUE does not engage it by itself: those coefficients are fitted by the inner solver, which reached the optimum at every scale measured. A fit that engages is not guaranteed to be higher: on 108 skew-normal fits it was never lower by more than 1e-6, and one REML random-slope fit was lower by 5.5e-7, which is optimizer tolerance. A fit with no such column is unchanged to the last bit: 444 of 480 fits over 32 families and five designs, under ML, REML and profile = TRUE, are identical, and the other 36 differ only in the vcov() fix below. autoscale = FALSE turns it off, and frmtmb_control()$autoscale is now NULL. diagnose() and verbose = TRUE say when it ran.

  • predict() now draws a cs() term. On an sratio(), cratio() or acat() fit with a category-specific effect, predict() drew every row as if the term were absent, in sample and at newdata, with no warning: at x = 3, where the category probabilities are (0.95, 0.002, 0.045), it reported about (0.42, 0.39, 0.19). The offsets were written one level too deep in the list the simulator reads, and at newdata they were the training rows’ offsets. Every predict() of such a fit changes. fitted(), simulate() and the log-likelihood were right and are unchanged.

  • cs_offsets_add() joins the extension API: it adds one response’s cs() offsets at newdata. The documentation of with_cs_offsets() now says what it does: it takes the list of every response, not one response’s list.

  • fitted() answers a multivariate fit, in brms’s n x 4 x nresp array. It refused before, while predict() answered. A scalar response is one layer named by the response, and a categorical one is one layer per category, P(Y = k), as brms 2.23.0 stacks them. resp takes several responses in the order given, and one scalar response is still an n x 4 matrix.

  • fitted() on a quadrature = TRUE fit warns that Est.Error leaves out the group-effect uncertainty at a level the fit saw. The ordinal route and predict() already said so; the ordinary route returned the smaller number silently. At a level the fit never saw nothing is left out, and it does not warn.

  • vcov() answers a REML = TRUE or profile = TRUE fit that has no random effect and no free dispersion (poisson, bernoulli, binomial, geometric, exponential, categorical). It died with “length of ‘dimnames’ [1] not equal to array extent”. The standard errors are glm()‘s for poisson (1.9e-11 relative) and bernoulli (3.8e-8). For geometric and exponential they differ from glm()’s by 2.3e-2, because frmtmb uses the observed information and glm() the expected information, which differ on those families’ log link.

  • A new grouping level refused by predict(), fitted() and friends is now classed frmtmb_new_levels, so an extension can say what it can do instead. The message is unchanged.

  • BREAKING: in a multivariate model a prior needs resp, as it does in brms. A class "b" or "Intercept" prior, a distributional parameter’s own class ("sigma", "nu", …), and a residual correlation class ("ar", "ma", "cosy", "cortime") written without resp are now refused with a frmtmb_error. Until now frmtmb applied such a prior to every response that had the slot, where brms 2.23.0 refuses it. The message gives the set_prior() call for each response on which that call resolves. What stops working: set_prior("normal(0, 1)", class = "b") on bf(y1 ~ x) + bf(y2 ~ x), and on a mi() model, which is multivariate. Write set_prior("normal(0, 1)", class = "b", resp = "y1") + set_prior("normal(0, 1)", class = "b", resp = "y2"). Class "sd" got the same rule in 0.62.0.

  • BREAKING: where the location is several distributional parameters, a prior names one with dpar. On categorical(), multinomial() and mixture() this is brms’s rule. frmtmb applies the same rule to its other families with several location dpars: mixture_mvn(), and in the extensions lca(), hmm(), lba() and rdm(). Class "b", "Intercept" and "sd" without dpar are now refused there; write dpar = "mub", dpar = "mu1" or dpar = "v1". Until now such a prior reached every category’s, component’s or accumulator’s coefficients at once. default_prior() lists these rows with their dpar, where it listed them with none.

  • Class "b" reaches the category-specific coefficients of cs() on sratio(), cratio() and acat(), and coef = "x" names the term cs(x), as in brms. Until now class "b" missed them: on ord ~ cs(x) it applied nothing, and on ord ~ z + cs(x) it reached z only. A MAP fit with such a prior moves, because the prior now applies. default_prior() lists a b row for each cs() term.

  • BREAKING: on a nonlinear location a class "b" or "Intercept" prior needs nlpar, as in brms. Until now class "b" without nlpar on bf(y ~ a * exp(b * x), a ~ 1 + z, b ~ 1, nl = TRUE) went to the slope a_z and nowhere else, and on a model whose nonlinear parameters are all intercept-only it went nowhere, without a word. Class "Intercept" or coef = "Intercept" reached the intercept of EVERY nonlinear parameter. All are now refused. class = "Intercept", nlpar = "a" stays accepted as frmtmb’s own spelling, although brms refuses it.

  • BREAKING: three more spellings brms refuses are refused (user decisions, 2026-09-24). resp on a model with one response; resp on class "rescor", which used to be ignored while the prior went on the whole residual correlation; and class "b" on a predictor with no population-level slope, such as y ~ 1, which used to apply nothing while prior_summary() listed it. resp on class "cor" stays accepted as frmtmb’s own spelling.

  • A fit that used one of the refused priors does not move: it now stops before fitting. No other fit moves.

frmtmb 0.62.0

  • A skew_normal() fit that used to stop at alpha = 0 now finds the maximum. alpha = 0 is a stationary point of the skew-normal likelihood at every sample, and the information is singular there, so a fit that reached it converged with code 0, no warning, and a log-likelihood up to 35 units below the optimum. The start is what put it there: alpha was started from the sign of the RESPONSE’s skewness, and a covariate can give the response the opposite skew from the residual. Two things change. alpha and sigma now start from the response with its mu predictor removed, including any offset(), not from the raw response. And a fit that lands on alpha = 0 anyway is refit from +2 and from -2, keeping the best optimum.

    Both halves read the alpha PREDICTOR rather than an "(Intercept)" coefficient, so they apply to alpha ~ 0 + g as well as to alpha ~ g. Cell-means syntax used to get neither: it started every coefficient at zero, which is exactly the stationary point, and stayed there. mixture() carries a component’s escape through.

    That placement is confined to a dpar that declares a stationary point, which today means skew_normal()’s alpha and nothing else. Every other dpar keeps the previous rule, an intercept or nothing.

    Nothing is printed. The refit runs only when a fit reaches the stationary point, and on a 200-fit spread of skew-normal designs that was 10 fits, costing 0.1 extra optimizer runs per fit overall; a fit that pays for it takes about 1.3 times as long. fit$opt$stationary_escape records the restarts and what they gained.

    What this changes for you, and nothing else does. A skew_normal() fit moves when its mu predictor is anything other than a bare intercept: a covariate, an offset(), or a design with no intercept. It also moves when alpha’s own design has no intercept. There logLik(), the estimates and the standard errors can all differ from 0.61.0, and where they differ the new value is the higher likelihood.

    A skew_normal() fit with mu ~ 1 and no offset is unchanged to the last bit, and so is every other family, including intercept-less designs such as y ~ 0 + g and distributional ones such as sigma ~ 0 + g.

  • A family’s init_dpars function may declare a third argument, (y, aterms, resid), and is then given the response with its mu predictor taken out by least squares. A two-argument initializer is still called with two arguments. A family may also declare post$stationary, which says where its likelihood has a stationary point and where to restart from when a fit reaches one. See ?frmtmb_family.

Seven places where frmtmb answered differently from brms 2.23.0 on the same model (lane wt-correct; dev/correct-findings.md has each construction and its brms measurement).

  • BREAKING: residuals() refuses type = "response" (brms’s "ordinary", also the default) and type = "pearson" on an ordinal (cumulative(), sratio(), cratio(), acat()) or multinomial() fit, as brms refuses them (“Predictive errors are not defined for ordinal or categorical models”). Until now an ordinal fit returned a residual on the category codes, y - sum_k k P(Y = k), which brms does not define. type = "osa" still works on an ordinal fit and uses only the order. plot(fit) is unchanged.

  • pp_check() on a fit works for every *_grouped type and takes x. group = "g" and x = "x" reached bayesplot as one string, so every grouped type and every type with an x failed. Both names are now looked up in the model frame, as brms looks them up, and a name the model does not use is refused. Every type bayesplot::available_ppc() lists (49 in bayesplot 1.16.0) now does on a fit what it does in brms, except the loo_* types, which need posterior draws and are refused with the reason.

  • BREAKING: pp_check() on a fit takes brms’s arguments in brms’s order: type, ndraws, prefix, group, x, newdata, resp, with re_formula after the dots. A positional fourth argument used to be re_formula and is now prefix. prefix = "ppd" plots the simulated responses alone. A type that is not a ppc type, such as "violin", is refused with the list of valid types instead of “object ‘ppc_violin’ not found”. type = "error_binned" is refused on an ordinal, categorical or multinomial fit, as in brms. newdata, draw_ids, nsamples and subset are refused with the reason. On a categorical fit the simulated categories reach bayesplot as their codes, so "bars" and "error_hist" work there.

  • pp_check() on a fit follows brms on two smaller points as well. resp is accepted and ignored on a model with one response, which is what brms does with it there. And a group or x given for a type that has no such argument reaches bayesplot, so bayesplot warns “unrecognized and ignored” exactly where brms makes it warn. brms is asymmetric about a name its data does not carry, and the asymmetry is copied: such a group is dropped and warns nothing, while such an x becomes numeric(0), stays, and warns.

  • BREAKING: a class "sd" prior reaches only its own response, distributional parameter and nonlinear parameter, which is how brms scopes it. set_prior("normal(0, 5)", class = "sd", group = "g") on bf(y ~ x + (1 | g), phi ~ (1 | g)) put the prior on the phi block too, where brms leaves that block at its own default. In a multivariate model a class "sd" prior with no resp, and in a nonlinear model one with no nlpar, used to reach every block; both are now refused, as brms refuses them, and the message lists the prefixes the model has. A block that spans several predictors, (1 | q | g) in mu and sigma, keeps brms’s own rule: there a specification with a field left empty reaches the whole block. A fit with such a prior changes its estimate; default_prior() lists one class-wide "sd" row per prefix. This covers a SMOOTH’s smoothing standard deviation as well, which class "sd" also addresses here: on bf(y ~ s(x), sigma ~ s(z)) a bare class "sd" prior reached both smooths and now reaches the mu one only, and default_prior() grows a dpar = "sigma" row for the other. brms keys its sds rows by the same prefix.

  • BREAKING: a mi() coefficient is named bsp_<resp>_mi<x>, as in brms, where it was b_<resp>_mi<x>. This changes variables(), draws, and hypothesis(): the bare "y_mixm > 0" is refused as brms refuses it; write hypothesis(fit, "bsp_y_mixm > 0", class = NULL). fixef(), vcov() and summary() keep the name y_mixm and now list it after every ordinary coefficient, in brms’s order, with a mi(x) * z main effect ahead of its interaction.

  • BREAKING: a mixture’s reference component is the one whose theta has no formula, as in brms. bf(y ~ 1, theta2 ~ x) on two components used to be refused; it now fits with component 1 as the reference, so theta2’s coefficients are the log odds of component 2 against component 1. A formula for fewer than K - 1 of the thetas is refused (“Can only predict all but one mixing proportion”), where theta1 ~ x on three components used to fit with an intercept-only theta2.

  • ?frm now says, under REML, that mgcv’s method = "REML" for a location-scale family integrates the coefficients of every linear predictor, so the two REML criteria differ for a smooth in sigma. test-smooths.R measures the gap.

predict() and fitted() carry the uncertainty in the group effects at a grouping level the fit saw, as brms does (user decision, 2026-09-22), and re_formula keeps the terms it names (lane wt-reunc). dev/reunc-findings.md has the construction and every measurement.

  • predict() draws the group effects at a known level. Each replicate draws the whole vector of group effects from its conditional law given the data and that replicate’s parameters, read from the fit’s joint precision. For a linear mixed model this is the prediction error variance of the best linear unbiased predictor, jointly with the fixed effects. Two rows of one group share that group’s draw, so predict(summary = FALSE) is right across rows as well as per row. The interval at a known level is wider than in 0.61.0, where the effects were held at their modes and the interval under-covered. This holds under ML, REML = TRUE and control(profile = TRUE). re_formula = NA, a model with no group-level term, and rows at a level the fit never saw give the same draws as 0.61.0, bit for bit. A fit made with quadrature = TRUE has no group effects in its joint precision: it keeps them at their modes and warns.

  • fitted() on an ordinal or categorical mixed fit carries the group-effect uncertainty in Est.Error and in the interval. The finite-difference delta method used to cover the outer parameters only; the group effects now join it, with their covariance taken jointly with the parameters. On a cumulative fit with 10 groups the 0.61.0 standard error of a category probability was 0.59 to 0.95 of the Monte Carlo value over the same joint law, and for P(Y = 1) smaller than at re_formula = NA; it is now 0.92 to 1.09 of it. The scalar route of fitted() already carried this term and does not change.

  • BREAKING: predict()’s param_uncertainty is now propagate_error, still defaulting to TRUE, and it now holds the group effects at their estimates as well as the parameters. The old name is removed rather than deprecated, so a call that uses it is an error naming it. The new name states the axis: whether the error in the ESTIMATES is propagated into the interval. A fitted group effect is an estimate, so propagate_error = FALSE holds it, and what is left is the observation noise. The fresh effect drawn for a level the fit never saw is not an estimate and is unaffected, and neither is the observation noise itself.

    It is not re_formula and neither argument can say the other’s thing. re_formula chooses WHICH terms enter the prediction, so only re_formula = NA can say “predict for an average group”; propagate_error chooses whether their error is carried, so only propagate_error = FALSE can say “include this group’s own effect but treat it as known”, which is what 0.61.0 did at every known level. brms has no such argument, because each posterior draw carries its own parameters and its own group effects. ?predict.frmtmb_fit has the four combinations.

  • BREAKING: a one-sided re_formula keeps the terms it names, in predict(), fitted(), frm_linpred(), frm_lp_basis() and everything that reads them. It used to keep EVERY term with nothing said, so re_formula = ~ (1 | g) on a fit with (1 | g) + (1 | h) returned the full prediction. brms’s rule applies now: a term is kept when the fit has a term with the same grouping factor whose columns include its columns, so ~ (1 | g) on a (1 + x | g) fit keeps the intercept and drops the slope. A dropped term is gone from the estimate, the standard error and the draws, and its grouping column is not needed in newdata.

  • BREAKING: re_formula = ~1 means no group-level effects, as in brms and as ~0 already did, in predict(), fitted(), frm_linpred() and frm_lp_basis(). It used to keep all of them.

    simulate() is the exception and does not change. There re_formula is lme4’s switch: NA REDRAWS the group effects and every other value conditions on them, so ~1, ~0 and ~ (1 | g) all still give the same draws as NULL. The two readings now differ, which matters for dharma_residuals() and for pp_check() on a fit, because both go through simulate(). What a partial formula should MEAN there is a decision rather than a fix, since brms has no simulate(); it is filed in dev/test-backlog.md.

  • BREAKING: a re_formula term that matches no term of the fit is an error that names the term and lists the fit’s terms. It used to be read as NULL. brms drops such a term silently, so re_formula = ~ (1 | nosuch) there is the population-level prediction; here it is refused, because a misspelled grouping factor should not change the answer with nothing said. Use re_formula = NA for the population level.

  • A re_formula that keeps SOME terms is refused on a fit that also has group-level content a formula cannot name, such as a factor-smooth term s(x, g, bs = "fs"): NA drops that content and NULL keeps it, and a partial formula cannot say which.

frmtmb 0.61.0

The silent wrong answers brms’s own ported test suite found (item 2.6f, lane wt-adefects). Each one is a case where frmtmb returned a number where brms refuses, or returned the wrong number with nothing said. dev/adefects-findings.md has the construction and the before-and-after figure for every item.

  • BREAKING: ar(), ma(), arma(), cosy() and unstr() take ONE VARIABLE NAME as the time index. An expression was evaluated against the model frame and its values became the time points, so ar(x + t, g, cov = TRUE) fitted a different likelihood and said nothing: log-likelihood -30.33421 against -30.88513 for ar(t, g, cov = TRUE) on the same 24 rows. brms refuses the same call (“Cannot coerce ‘x + t’ to a single variable name”). Compute the column first and name it. time = NA and an omitted time are unchanged.

  • BREAKING: gr = takes variable names crossed by : and nothing else, which is brms’s grammar (“It may contain only variable names combined by the symbol ‘:’”). Two things change.

    gr = a:b now CROSSES the two variables instead of being evaluated. R’s : is the interaction for two factors but the sequence operator for anything else, so on numeric group codes gr = g1:g2 used to produce a length-one vector; it now groups by the pair, as it already did for factors and as it does in brms.

    Every other operator is refused. gr = g1/g2 on numeric codes used to group the residual by the QUOTIENT, merging the series (1, 1) and (2, 2), for a log-likelihood of -30.87554 against -30.88513 with nothing said. gr = g1 + g2, gr = g1 * g2, gr = factor(g) and gr = interaction(g1, g2) are refused too; the last two worked before and now need the column computed first, which is what brms asks for.

  • BREAKING: a hypothesis must state a relation. hypothesis(fit, "Age") used to be answered as the row of "Age = 0". brms refuses it, “Every hypothesis must be of the form ‘left (= OR < OR >) right’”, and so does frmtmb now. Write "Age = 0", "x1 - x2 = 0" or a directional "x > 0". The bare spelling was documented as an extension; it is gone, and 48 call sites in this repository’s own suites were rewritten.

  • A fit carries its model frame as fit$data, which is what brms keeps there. A frmtmb_fit had no data element, so fit$data partial-matched fit$data2 and brms-shaped code read the data2 list (a covariance matrix, or an empty list) with no error. It is the same object model.frame() already returned, so it costs no memory.

  • predict(newdata = ) may omit a grouping column when allow_new_levels = TRUE, which is brms’s rule (“grouping factors do not need to be specified by the user if new levels are allowed”). The column is filled with NA, so every row is an unseen level: the population value, plus that block’s variance in an interval. It used to stop at base R’s “object ‘g’ not found”. Without allow_new_levels the call is still refused, now by a classed error naming the column and the two arguments that answer it.

  • log_lik() on a maximum-likelihood fit says to sample, the way loo() and waic() already did. frmtmb defines the generic and the frmtmb_fit method, and registers it on rstantools’ generic; frmtmb.sample re-exports it rather than defining a second one. It used to be “could not find function”.

  • BREAKING: predict() is brms’s predict() (item 2.6d). It returns a summary of the PREDICTIVE distribution of the response, with observation noise in it, in the columns Estimate, Est.Error, Q2.5 and Q97.5. It used to return the LINEAR PREDICTOR, glmmTMB’s convention, and on a lognormal fit the two differ by about 8 against about 6,700.

    What replaces it: frm_linpred(), which is the old predict.frmtmb_fit() unchanged, under a name that says what it gives back. Every argument is the same: frm_linpred(fit, type = "link") is the old predict(fit), frm_linpred(fit, type = "response") the old response scale, and se.fit, dpar, resp, re_formula, allow_new_levels and the glmmTMB aliases conditional, zprob, zlink and disp all behave as they did.

    predict() refuses type, se.fit, dpar and scale BY NAME and the message says where each one went, so a ported script stops rather than changing scale in silence.

    How the draws are made, since a maximum-likelihood fit has no posterior: each replicate draws the outer parameter vector from N(theta_hat, vcov(fit, full = TRUE)) and then a response from the family’s own simulator, the one simulate() uses. param_uncertainty = FALSE drops the parameter draw and simulates at the estimates alone. The random effects of a level the fit SAW stay at their conditional modes, as everywhere else in this package, so their own conditional variance is NOT in the interval; ?predict.frmtmb_fit says so and dev/shapes-findings.md reports the measured coverage of each arm. ndraws sets the number of replicates (default 1000), summary = FALSE returns them, and robust, probs and transform are brms’s and are answered. draw_ids, cores and sort = TRUE are refused by name.

    A level the fit did NOT see is drawn, not held at zero: allow_new_levels = TRUE takes that level’s effect from its block’s own estimated covariance, once per replicate at that replicate’s parameters, which is brms’s sample_new_levels = "gaussian". That value of the argument is therefore ACCEPTED; "uncertainty" and "old_levels" resample the posterior draws of the levels that were seen and are refused by name. Measured out of sample at unseen levels, the interval used to cover 0.8618 against a nominal 0.95 and its Est.Error was bit-identical to a known level’s. Each DISTINCT unseen level draws its own effect, and rows in the same unseen level share one, which is what makes summary = FALSE right for a mean, a contrast or a sum over new groups.

    On a MULTIVARIATE fit predict() returns brms’s nrow x 4 x nresp array with the responses named on the third dimension, and summary = FALSE the ndraws x nrow x nresp array. It used to answer for the FIRST response only, in an nrow x 4 matrix, with nothing said. resp = is a filter on that array. Every response is drawn at the same parameter draw, and under set_rescor(TRUE) the responses are drawn from their joint multivariate normal law, as brms draws them, so the draws carry the estimated residual correlation.

    predict(summary = FALSE) names neither the draws nor the rows, which is what brms’s predict(summary = FALSE) and posterior_predict() return (list(NULL, NULL), and the responses on the third margin of a multivariate fit).

    predict(ndraws = 2.5) is an error. The count check ran after as.integer(), so a fraction was accepted and silently truncated.

    A simulated parameter draw that makes a distributional parameter non-finite for a ROW makes that one cell NA, instead of failing the whole call in the family simulator; the warning names the rows and how many replicates each lost, and each row is summarized over the replicates it has. No row is summarized over a subset of parameter draws that another row’s overflow selected: the parameter draws and one seed per replicate are taken up front, and a masked cell takes nothing from its neighbours. The rows of one replicate still share one random stream, so a row’s Monte Carlo draws shift with the rows simulated before it. It happens where a parameter the draw law covers is barely identified: vcov(object, full = TRUE) has its variance, and param_uncertainty = FALSE simulates at the estimates alone. Every cell of a response non-finite is an error.

  • BREAKING: fitted(), residuals(), fixef(), vcov(), ngrps() and summary() return brms’s shapes (item 2.6f).

    • fitted() and residuals() return an n by 4 matrix with the columns Estimate, Est.Error, Q2.5 and Q97.5, where they returned a vector. Est.Error is the delta-method standard error and the Q columns are the Wald interval at probs, because there are no draws to take a quantile of. fitted() on an ordinal or categorical fit returns brms’s n by 4 by K array, the third dimension named P(Y = k); the category probabilities carry a standard error for the first time, by the finite-difference delta method over the whole outer parameter vector. The old value is the Estimate column, and frm_linpred(fit, type = "response") is the same numbers as a plain vector.
    • fitted() now takes probs, nlpar (a synonym for dpar) and allow_new_levels; ndraws, draw_ids, sort, summary and robust are still refused by name.
    • residuals() accepts brms’s "ordinary" as a spelling of type = "response", and takes probs. Its type stays in the second position, which is NOT brms’s (brms has newdata there).
    • fixef() returns brms’s summary matrix, one row per population-level coefficient under brms’s own name (Intercept, sigma_Intercept, Trt1) and in brms’s order, which puts every predictor’s intercept first. It takes brms’s pars and probs. fixef(flatten = TRUE) is unchanged and is the vector of estimates in confint()’s spelling. The per-dpar list fixef() used to return is fixef_by_dpar(), a new export.
    • fixef(), vcov() and summary()$fixed carry an ordinal fit’s THRESHOLDS and its cs() coefficients, which they left out entirely. On bf(ord ~ x) + cumulative() they are the rows Intercept[1], Intercept[2], x and a 3 by 3 covariance, which is what brms gives on the same model; the lane gave x and 1 by 1, and print(fit) showed a one-row coefficient block. The threshold Est.Error is the delta method over the joint covariance, because the family may estimate increments rather than the thresholds themselves. A cs(z) term adds z[1], z[2] after the b block, as in brms.
    • vcov() covers exactly the rows fixef() reports and names them the same way. It used to be one row wider: a distributional parameter nobody wrote a formula for, such as sigma or nu, is an intercept-only linear predictor here and is not a population-level coefficient in brms. It is in summary()’s Further Distributional Parameters block instead, on its own natural scale. vcov() now takes brms’s correlation and pars, and full, cluster and type moved after ..., so they must be named. vcov(full = TRUE) is unchanged and keeps the INTERNAL names, because it is the matrix a delta method on confint()’s rows needs.
    • ngrps() returns brms’s named LIST, and NULL for a fit with no grouping factor. A smooth, gp(), hsgp(), car() or spde() block is no longer counted: those are random-effect blocks here and are not grouping factors in brms.
    • summary() carries brms’s $fixed, $random, $spec_pars and $cor_pars slots beside the ones it had, and print() uses brms’s section headings (Multilevel Hyperparameters:, Regression Coefficients:, Further Distributional Parameters:). $fixed carries brms’s four columns first and then the Wald test this package reports, where brms writes Rhat and the two ESS columns. summary() now takes brms’s priors and prob, and robust and mc_se are refused by name; vcov moved after ... and must be named.
    • summary()’s EMPTY slots take brms’s empty shapes: $random is NULL where it was list(), and $spec_pars and $cor_pars are frames with no rows where they were NULL.
    • fitted(), residuals() and predict() leave their row dimnames NULL, as brms does. They carried the data’s row names, and under na.action = na.omit those names were the way to line a result up with the rows of the data that survived; that way is GONE, as it is in brms. frm_linpred() and model.frame() still carry them, and the row count of each summary matrix follows the surviving rows.
    • An ordinal fitted() cannot report a probability outside [0, 1]. The Q columns are a Wald interval, which is unbounded; 15 of 450 bounds were below 0 and 5 above 1 on a three-category fit. They are held inside the range now, so a clamped bound is a sign that the normal approximation is poor there rather than a probability of -0.0055.
    • print(fit) prints the summary, as brms’s does.
  • coef() on a mixed ORDINAL fit moves the thresholds by each group’s random intercept, as brms’s does. For cumulative() and sratio() the group’s Intercept[k] is the threshold minus the group’s mode, and for cratio() and acat() it is the mode minus the threshold, brms’s two sign conventions. brms’s own coef() on a cumulative and an acat fit reproduces exactly those formulas, draw by draw (dev/shapes-p2-brmsref.R). It used to repeat the thresholds unchanged in every group beside a stray (Intercept) column holding the mode alone.

  • BREAKING: ranef() names its columns as brms does, Intercept and sigma_Intercept, which is what coef() and brms’s ranef() say. Code reading ranef(fit)$g[, "(Intercept)"] must read "Intercept".

  • BREAKING: coef() names its coefficients as brms names them, Intercept and not (Intercept), which is what fixef() and vcov() now say. lmtest::coeftest() keeps only the rows both coef() and vcov() name, so the split dropped the intercept row out of a printed significance table with no warning. An ordinal fit’s thresholds are coef() entries too, for the same reason. fixef_by_dpar() is the design-column spelling.

  • BREAKING: hypothesis() no longer returns an object of class brmshypothesis. The class is "frmtmb_hypothesis" alone. The SHAPE is unchanged and so is every element; what stops working is is(x, "brmshypothesis") and inherits(x, "brmshypothesis") in a ported script. frmtmb owns print() and plot() for its own class, so neither changes, and a frmtmb object carrying a brms class name was the one thing the class rule forbids.

  • variables() lists an ordinal fit’s thresholds and its cs() coefficients, which it dropped: on a sratio() fit with a cs() term it listed 3 of 9 parameters, on every ordinal family. They are brms’s b_Intercept[k] and bcs_<term>[k], and hypothesis() reaches them too. The thresholds are reported on the model’s own scale, not the internal (first threshold, log increments) parameterization that cumulative() and sratio() estimate; the family declares the map in post$ord_thresholds.

  • insight::get_predicted() has a method of its own. Its default calls predict(x, type = ), which predict() no longer has, and it WARNS and returns NULL rather than erroring, so every prediction was silently lost. The method also carries standard errors, which the default never had: get_predicted(fit) used to report NA in every CI column. predict = "prediction" is predict()’s predictive interval and "link" the linear predictor.

  • insight::get_residuals() has a method of its own, insight’s brmsfit body. residuals() is brms’s n by 4 matrix now, and insight’s default returns whatever residuals() gives, so get_residuals(fit) began returning 600 numbers where its contract is one per observation. It is the Estimate column again, with the whole matrix on a "full" attribute and insight’s own insight_residuals class, which the default never set either.

  • marginaleffects::get_coef(), set_coef() and get_vcov(), and insight::get_parameters() and get_varcov(), read every ESTIMATED coefficient rather than vcov()’s brms block, because each is paired with a coefficient vector that has the extra rows. Without this the standard errors of avg_slopes() moved and some became NA.

    They also carry an ordinal fit’s THRESHOLDS and cs() coefficients: on their internal scale under confint()’s names (tau_raw_1) for marginaleffects, which perturbs and writes the vector back, and on the model’s own scale under fixef()’s names (Intercept[1]), with the delta-method covariance, for insight, which only reads it. marginaleffects builds its Jacobian by perturbing get_coef() one entry at a time, so a parameter the vector left out contributed nothing: avg_slopes() on bf(ord ~ x) + cumulative() reported 0.00029 for the middle category where MASS::polr reports 0.02016, a factor of 69, on a point estimate that agreed to five decimals. All three categories now agree with polr to five decimals. This defect predates the shapes work.

  • sim_context() takes max_iter, the rejection limit a trunc()ed draw needs, which predict() exposes as brms’s ntrys.

  • No dependency floor moves for any of this. insight, marginaleffects, emmeans and MASS are all in Suggests already and none of the methods added here needs a newer one: insight::get_residuals and insight::get_predicted are generics insight has had since 0.9, and marginaleffects’s four extension generics are unchanged. lmtest is used by dev/shapes-interop.R and by nothing in the package.

frmtmb 0.60.0

  • tmbstan (>= 1.2.1) in Suggests. tmbstan 1.2.1 is the first build that samples the model rather than a standard normal when built against StanHeaders 2.39 (dev/tmbstan121-findings.md).

  • BREAKING: every condition frmtmb raises is classed, the contract brms keeps with brms_error. An error has the class c("frmtmb_error", "error", "condition"), a warning c("frmtmb_warning", "warning", "condition") and a message c("frmtmb_message", "message", "condition"), so tryCatch(frmtmb_error = ) catches any refusal. A condition that an extension raises also has a subclass named for the package, such as frmtmb_eam_error. See ?frmtmb-conditions.

    What changes for a caller: the class vector no longer contains simpleError, simpleWarning or simpleMessage, so a handler or test that catches those classes by name stops matching. A handler for error, warning or message is not affected. A frmtmb_fit_error, raised when the optimizer fails, is now a frmtmb_error and is no longer a simpleError. The one exception is the refusal of model.matrix() on a multivariate fit, which is c("frmtmb_error", "simpleError", "error", "condition"): insight 1.5.4 tests for simpleError to give NA standard errors in get_predicted_ci(), and without it that call stopped with “argument is of length zero”.

    Unlike brms’s class vector, frmtmb’s does not contain rlang_error: frmtmb builds the conditions with base R and does not import rlang. Errors from other packages, such as RTMB, TMB and Matrix, keep their own class, and so does R’s “unused argument” for an argument that a function does not have.

  • BREAKING: refusals that base R used to raise now have frmtmb’s class and new text. Each is a frmtmb_error whose message names the argument or variable, the value given and what is allowed. Code that matches the old text stops matching:

    • A value that matches none of the choices of predict(type =), fitted(scale =), residuals(type =), confint(method =), drop1(test =), hypothesis(method =, scope =), conditional_effects(band =, method =), frmtmb_control(check_nlev_1 =, check_olre =), default_prior(route =), get_prior(route =), validate_prior(route =), vcov_cluster(type =), frm_periodogram(taper =, detrend =) and anova() on a frmtmb_multiple (method =, use =). The old text was 'arg' should be one of "response", "linear". The new text is `scale` must be one of "response", "linear", not character "latent". A partial value such as "resp" still matches.
    • A prior string with the wrong number of arguments, for each density that set_prior() reads. The old text was length(pars) == 2 is not TRUE. The new text is Prior 'normal(0)' gives 1 argument; normal() takes 2: normal(mu, sigma). An exponential() rate that is not positive, and a prior that is not one string, have messages of their own.
    • An argument of the wrong type that a stopifnot() refused with the text of the assertion: set_prior(...) + "x", frm(prior = list(1)), diagnose(), cluster_scores() and vcov_cluster() on an object that is not a frmtmb fit, check_custom_family() on an object that is not a family, and frmtmb_family() with a family, dpars or lpdf of the wrong type.
    • predict() and the other methods that take newdata refuse a newdata that lacks a variable of the model (was object 'z' not found, now Variable 'z' missing from newdata) or that has a level of a population-level factor that the fit did not see (was factor f has new levels zz). brms refuses both.
    • frm() refuses a variable that is neither a column of data nor an object found from the formula (was object 'nope' not found), a data that is not a data frame, a list column used as a variable (was invalid type (list) for variable 'lc'), and an object from the formula environment whose length is not the number of rows (was variable lengths differ).
    • hypothesis() refuses a name in backticks, such as `(Intercept)`, as it refuses other unknown parameters (was object '(Intercept)' not found).
    • lme4::getME(<multivariate fit>, "Zt") keeps its class. The text loses the prefix error in evaluating the argument 'x' in selecting a method for function 't': that R’s S4 dispatch added.
  • frm_stop(), frm_warning() and frm_message() are exported for extension authors. They take the arguments of stop(), warning() and message() and add the classes. Given one condition object, they signal that object with the classes added, as the base functions do. frm_warning(immediate. = TRUE) prints at once when getOption("warn") is 0. A helper given as a value, as in lapply(x, frm_stop), gets the subclass of the function that gave it. package = names the package whose subclass the condition gets. frm_match_arg() is match.arg() with a classed refusal, and frm_family_package() gives the package that built a family.

  • A refusal that frmtmb raises from text an extension wrote as data, such as frmtmb_structure(refusals =), or about a declaration of a family that an extension built, such as an addition term the family needs, has the subclass of that extension. For example, frm(..., family = hmm(...), REML = TRUE) is a frmtmb_latent_error. ?frmtmb-conditions says which refusals have the subclass and which do not.

  • Every extension in this repository now requires these functions, so the frmtmb (>= 0.59.0) floor of frmtmb.coupling, frmtmb.eam, frmtmb.latent, frmtmb.learn, frmtmb.ode, frmtmb.sample and frmtmb.spline must move to the release that includes this change.

  • The worked-example families in inst/bcm/ and inst/rl/, which vignette("bayesian-cognitive-modeling") and vignette("reinforcement-learning") load with source(), raise their refusals with frm_stop() too, so a copy of them keeps the classes.

frmtmb 0.59.0

  • BREAKING, and the silent wrong answer it removes. The link for the mean is now checked against the set brms 2.23.0 allows for the family, and a link outside it is refused with brms’s sentence and the set: 'sqrt' is not a supported link for family 'bernoulli'. Before, only link_<dpar> was checked, so bernoulli("sqrt") FITTED, returning coefficients for a probability on eta^2, which is not bounded by one. What stops working: every (family, link) pair brms refuses, 417 pairs over 37 families, of which 371 used to construct. Among them are exponential("cloglog"), Beta("1/mu^2"), zero_inflated_negbinomial("logit"), beta_binomial("log"), a stats family carrying such a link (frm(..., family = poisson("inverse"))), and any link but "logit" on categorical() reached by name. Every pair brms accepts still constructs and fits: 207 fits give the same log-likelihood as before to ten decimals. The sets are read out of brms by dev/famlink-gen-links.R, not typed. nbinom1(), tweedie() and huber() have no brms family and take the set of negbinomial, Gamma and gaussian. A custom link object is not held to a set.

  • A family object carries brms’s fields. $link is the name of the link for the mean, $linkfun and $linkinv are its functions, and $link_<dpar> names the link of every other parameter: beta_binomial()$link_phi is "log". $link used to partial-match the links list and return it. The fields are not stored: $ computes them from links when they are read, so they describe the links a fit uses however the object was edited, and a fit saved by an earlier version answers them too. names() and [[ do not show them. BREAKING: $ on a family object no longer partial-matches. A name that is only a prefix of one field (fam$lpd) is an error naming the field. An element stored on the list under one of those names is an error when $ reads it.

  • BREAKING: print() on a family object prints brms’s first two lines, Family: and Link function:, and then the parameters with their links. The <frmtmb family> header is gone.

  • The link for the mean may be unquoted, as stats::family() and brms allow: student(identity), negbinomial(sqrt), zero_inflated_poisson(log). A bare link name is read as the name without being evaluated, and a variable holding a link name is read for its value, which is the order brms uses. link = NULL and link = NA give the default. A bare link name the family does not take and that is bound to nothing, as in negbinomial(inverse), is refused as that link, where brms reports “object ‘inverse’ not found”.

  • New brmsfamily(), brms’s generic constructor: brmsfamily("gaussian", inverse), brmsfamily("zi_poisson"). A family name now follows brms’s spelling rules wherever a name is accepted: case does not matter, "normal" is "gaussian", "com_poisson" is "compois", and "zi_" and "hu_" stand for "zero_inflated_" and "hurdle_". An unknown name is refused with brms’s sentence, x is not a supported family.

  • BREAKING: frm_family() is removed. brmsfamily() is the one spelling, and takes the same arguments with the link second. frm(family =) takes the family and its link as one vector, c("weibull", "log"), which brms accepts for family =; a vector of three or more is refused.

  • Changes fits, and the silent wrong answer it removes. A brms family object passed to frm() now keeps its link_<dpar> links. They used to be dropped without a word, so family = brms::student(link_sigma = "identity") fitted sigma on the log link the user had not asked for. Such a model now fits the link it names, and its estimates move.

  • A correct fit no longer warns “NA/NaN function evaluation” when nlminb’s line search steps past the domain of a link, such as below zero on inverse.gaussian’s 1/mu^2 or Gamma’s inverse, or outside the unit interval on bernoulli("identity"). Such a trial value is handed to nlminb as +Inf rather than NaN; nlminb backtracks from both identically, so every optimum is unchanged (212 brms link pairs and five inverse gaussian designs give identical() estimates). A model whose objective is NaN at its starting values still warns. The number of mapped trials, summed over every optimizer run of the fit including restarts, is fit$opt$nonfinite_trials. When it is not zero, diagnose() prints it and returns it as nonfinite_trials, and verbose = TRUE adds it to the optimizer stage lines and to the final done line.

  • BREAKING, changes fits: inverse.gaussian takes brms’s default link, 1/mu^2, where frmtmb’s was log. It reaches every fit that names the family as a string or through brmsfamily() without a link, and those estimates move. stats::inverse.gaussian() already carried 1/mu^2 and is unaffected. Write brmsfamily("inverse.gaussian", "log") for the old model. Fits move, and some designs converge poorly on this link: over 12 designs of 20 data sets each, 135 of 240 default-link fits ended with code 0 and no warning, against 234 of 240 on log. The failures are warnings such as a large gradient or a Hessian that is not positive definite, never a silent wrong answer. If a fit warns, pass link = "log".

  • BREAKING: binomial(), beta_binomial(), zero_inflated_binomial() and multinomial() without a trials() term are refused with brms’s message, Specifying 'trials' is required for this model., and so is a mixture() with such a component. multinomial() used to take its trials from the row sums of the response; its trials() must now equal them, as in brms. The others used to be fitted as one trial per row. Write y | trials(n) ~ ..., or bernoulli() for a 0/1 response. cbind(successes, failures) supplies the trials and is unaffected. get_prior() does not refuse these responses, because brms’s get_prior() does not and no prior depends on the trials.

  • BREAKING: mixture() refuses what brms refuses. Components with real and integer support (mixture(lognormal, exgaussian, poisson())) are refused, because a weighted sum of a density and a probability mass is not a likelihood. categorical(), multinomial(), hurdle_gamma(), hurdle_lognormal() and zero_inflated_beta(), the families brms’s no_mixture() bars, are refused as components, and a mix of ordinal and non-ordinal components is refused with brms’s sentence. A named argument in the dots, such as brms’s nmix or order, is refused by name; it used to be read as a component and reported as an unsupported family.

  • BREAKING: an unordered factor response to cumulative(), sratio(), cratio() or acat() is an error, as in brms. It used to warn and fit with the alphabetical level order as the category order. Use an ordered factor or integer codes. The ordinal response refusals open with brms’s sentence, Family 'cratio' requires either positive integers or ordered factors as responses.

  • frm() messages brms’s suggestion, Only 2 levels detected so that family 'bernoulli' might be a more efficient choice., for a binomial(), beta_binomial() or zero_inflated_binomial() response whose trials are all one, including a mixture() of those, and for an ordinal or categorical response with two categories.

  • The link_<dpar> refusal now opens with brms’s sentence, 'logit' is not a supported link for parameter 'shape'.

  • BREAKING. A group-level coefficient that two terms of one predictor both give to the same grouping factor is refused, as brms refuses it. y ~ x + (1 | g) + (x | g) used to fit two independent intercept blocks on g, and only the sum of their variances is identified: on a 240-row construction the two intercept standard deviations came out 0.56151 and 0.56093, the log likelihood equal to that of (x | g) alone (dev/priorform-base-behaviour.R). What stops working: every such repeat, including (1 | g) + (x || g), (1 | g:h) + (1 | g/h) and (1 | mm(g, h)) + (x | mm(g, h)). Write the coefficient in one term. This includes the animal model’s (1 | gr(id, cov = A)) + (1 | id), which IS identified through the matrix but which brms also refuses; write the permanent-environment term on a copy of the column, (1 | id2), as brms does, and the message says so. The glmmTMB structures brms does not have follow the same rule: rr(...) + diag(...), equalto(...) + us(...) and ar1(...) + diag(...) repeating a coefficient on one factor are refused.

  • A bar term written twice, (1 | g) + (1 | g), is one block, as brms reads it; it used to fit two blocks splitting one variance at will. Grouping factors are compared as brms writes them, so (1 | g:h) + (1 | h:g) is two blocks, as in brms, while (1 | g:h) + (1 | g/h) repeats one and is refused. A multi-membership group is named by its members, as brms names it, so (1 | mm(g1, g2)) + (1 | mm(g1, g2, weights = cbind(w1, w2))) repeats the intercept and is refused; it used to fit two blocks. A whole-term special written twice, s(z) + s(z), is one term, as in brms; it used to build two smooths with one column name. A split that repeats no coefficient, such as (1 | g) + (0 + x | g), and the same term in two predictors, such as bf(y ~ (1 | g), sigma ~ (1 | g)), are still accepted. On 489 generated designs and 158 formulas harvested from brms’s own tests, vignettes and examples, the refusal fired on none that brms accepts and missed none that brms refuses (dev/priorform-falsealarm.R).

  • BREAKING, and the defect it fixes. cs(), a smooth (s(), t2(), te(), ti()), gp(), an autocorrelation term and mmc() inside an interaction or another call are refused by name, which is brms’s list of whole-term specials. y ~ x * cs(g) used to fit y ~ x, and so did y ~ x:s(z) and y ~ x * s(z); y ~ I(s(z)) fitted an intercept alone. The dropped terms went without a message. The message names the term as brms does, The term 'x:s(z)' is invalid, and points a smooth at by =. s(z) * s(z), which terms() collapses to s(z), is read as that one term, as brms reads it.

  • BREAKING. A formula whose right-hand side is itself a formula, y ~ ~ x, is refused by name in bf(), lf(), nlf() and in the parameter formulas of bf(), as brms refuses it (brms issue #749). Before, frm(y ~ ~ x) fitted y ~ x.

  • bf() returns a formula it already built unchanged, so identical(form, bf(form)) holds as it does in brms. It used to stop with “formula must be a formula”. Further arguments add to that formula; a parameter it already sets is refused, as + lf() refuses it. nl now defaults to NULL, as in brms: FALSE for a new formula, and the existing setting for a built one. bf(mvbind(y1, y2) ~ ..., nl = TRUE) now passes nl to each response, where it used to drop it.

  • bf() and lf() take a one-sided parameter formula named by its parameter, bf(y ~ x, sigma = ~ z), which is brms’s second spelling of sigma ~ z. A one-sided formula with no name is refused with brms’s wording, “Additional formulas must be named”, and an invalid parameter name now says it “must not contain dots or underscores”.

  • BREAKING. Two prior specifications for the same slot (class, coef, group, resp, dpar and nlpar) are refused by frm(), frm_sample(), frm_simulate(), par_template() and validate_prior(), as brms refuses them. What stops working: a repeated specification, two densities for one slot, and a density followed by a bounds-only specification, which used to tighten it. Write the density and both bounds in one call. Slots are compared field by field, so class = "b", dpar = "sigma" and class = "b", coef = "sigma" are two slots although brms prints both as b_sigma. On frm_sample() of a fit that carries its own prior, a specification on the call for a slot the stored prior names replaces the stored one, as brms’s update(prior = ) replaces a row, and is not refused.

  • BREAKING. When specifications for different slots reach one parameter, the more specific one applies whatever the order, as in brms: a coef over its class, and a group over a class-wide sd or cor. The later one used to win, so a coefficient prior written BEFORE its class prior was silently replaced by it. Fits change only for that order: on the construction in dev/priorform-punch-order.R (seed 20260916), coef = "x" then class b moved from a log likelihood of -261.34 to -409.94, the value the other order always gave, and a group sd prior then a class sd prior on a beta model from 211.86 to 215.28. The same-order fits are bitwise unchanged. Between different classes, cor and the theta hatch, the later one still applies.

  • set_prior() takes vectors in every argument and returns one specification per element, recycled as a data frame column is: set_prior("normal(0, 2)", class = c("b", "sd")) is two specifications, as in brms. A length that does not divide the longest is refused.

  • BREAKING. A prior object prints in brms’s layout. One specification prints as b_x ~ normal(0, 1), with bounds in front as <lower=0>; several print as a table with brms’s columns, and print(x, show_df = FALSE) gives one line each; a row with no density of its own shows its class row’s as (vectorized), and an empty prior prints nothing, as in brms. What stops working: code that reads the old normal(0, 1) class=b coef=x line. The density prints as it was written, so set_prior("cauchy(0,1)") prints cauchy(0,1) rather than the student_t(1, 0, 1) it parses into.

  • A prior object answers brms’s column reads: pr$prior, pr$class, pr$coef and the rest return one element per specification, from the table as.data.frame() now builds. Assigning a column, pr$prior[2] <- "exponential(1)", rebuilds the specifications with set_prior().

  • default_prior() is new and is the canonical name, as it has been in brms since 2.20.14. BREAKING: get_prior() is now its alias, get_prior(formula, ...), which is brms’s signature. Calls that pass data, family, data2 or route by position or by name are unchanged.

  • BREAKING. The default_prior() table has brms’s columns in brms’s order, prior, class, coef, group, resp, dpar, nlpar, lb, ub, source; resp used to come after nlpar, and source is new. A class "sd" or "cor" row for a block of a distributional parameter’s predictor now names that parameter in dpar. Before, bf(y ~ (1 | g), phi ~ (1 | g)) listed one sd row for g where the model has two blocks.

  • validate_prior() is new. It resolves a prior against a model with the resolution frm() uses, refuses it with the same message when any part of it addresses nothing, and returns the whole default_prior() table with the prior filled in and source marking the rows set and the rows that inherit a class row’s density. frm(prior = ) accepts that table, and a default_prior() table edited in place, and fits the same model as the prior that built it.

  • empty_prior() and as.brmsprior() are new. as.brmsprior() turns a data frame with a prior column into a prior object, filling set_prior()’s defaults and dropping other columns, as brms’s does; here the result is the frmtmb_priorlist that set_prior() returns.

  • BREAKING: variables() and hypothesis() use brms’s names. Every coefficient carries brms’s b_ prefix, and a coefficient of a distributional, nonlinear or multivariate predictor is spelled the way brms spells it: b_Intercept, b_x, b_sigma_Intercept, b_sigma_y1_x, b_a_Intercept. variables(fit) used to return Intercept, zBase, ..., sd_patient__Intercept, half of brms’s convention. A group-level standard deviation of a distributional parameter is sd_g__sigma_Intercept; it used to be a second sd_g__Intercept, which the mean’s block claimed, so it could not be reached at all. An |ID| block’s names are sd_id__y1_Intercept, not sd_id__y1.muIntercept.

    Every piece of a name goes through one renamer ported from brms’s rename() and its helpers: I(x^2) is b_IxE2, a factor level c-d is b_fcMd, a response y_a is ya (b_ya_x, sigma_ya), an interaction group keeps its : (sd_g:h__Intercept) and joins its levels with _ (r_g:h[1_p,Intercept]), whitespace in a level is a dot in an r_ name, and a smooth’s unpenalized column is bs_sx_1 with smoothing SD sds_sx_1. A distributional parameter nobody wrote a formula for is the parameter itself on its natural scale, sigma, shape or sigma_ya, and has no b_sigma_Intercept; written as sigma ~ 1 it is b_sigma_Intercept and there is no sigma, as in brms. The autocorrelation parameters are ar[1], ma[1], cosy, and a multivariate model’s residual correlations rescor__ya__yb. Checked against the names brms derives for thirteen models, including those hostile names and the unmodeled shape, nu, phi, zi and hu: 210 of 210 present and none extra, 12 of 210 before (dev/brmsnames-findings.md).

    What stops working: a bare coefficient name in variables()’s output ("x" %in% variables(fit)), a hypothesis() string that names a natural-scale quantity without class = NULL, b_sigma_Intercept on a model whose sigma has no formula (use sigma), and ar1 (use ar[1]).

  • BREAKING: hypothesis() takes brms’s class = "b" default and brms’s argument order, (x, hypothesis, class, group, scope, alpha, robust, seed, ...), with method, nsim and vcov after them. A bare name is read as the coefficient, x as b_x, as in brms, so hypothesis(fit, "x1 - x2 = 0") still works. A natural-scale name needs class = NULL, as in brms: hypothesis(fit, "sd_g__Intercept / sigma = 0", class = NULL). The string is read as brms:::eval_hypothesis() reads it: every variable gets the class and group prefix, a prefixed name the model does not have is refused with brms’s “Some parameters cannot be found in the model”, and brms’s renaming (: to ___, [ and ] to ., , to ..) is applied before parsing. x:fe is therefore the interaction coefficient. It used to parse as R’s : operator, which returned b_x whenever b_x and b_x:fe were less than 1 apart: hypothesis(fit, "x:fe > 0") gave 0.2415 for a coefficient of 1.0794, with no error. A name written in full under the default class ("b_x = 0") is refused, as brms refuses it, and the backquoted internal spelling `(Intercept)` is no longer an alias. alpha used to be the third positional argument, so hypothesis(fit, h, 0.1) now names a class. robust = TRUE and scope = "ranef" or "coef" are refused by name: both need draws.

  • BREAKING: hypothesis() returns brms’s object. A list of class c("frmtmb_hypothesis", "brmshypothesis") with brms’s elements hypothesis, samples, prior_samples, class and alpha, whose $hypothesis frame has brms’s eight columns: Hypothesis, Estimate, Est.Error, CI.Lower, CI.Upper, Evid.Ratio, Post.Prob, Star. On a fit, Est.Error is the delta-method (or bootstrap, or pooled) standard error, the interval follows brms’s rule (central 1 - alpha for =, central 1 - 2 * alpha for a directional row, so its relevant end is the one-sided bound; it used to be infinite on the other side), Evid.Ratio and Post.Prob are NA, and the test statistic and p-value are attr(h, "test"). What stops working: h$estimate, h$se, h$lwr, h$upr, h$z, h$p, nrow(h), and as.data.frame(h).

  • BREAKING: the reserved-name note and the dot spellings are gone. A covariate named sigma used to shadow the residual SD in hypothesis(), with a message and a .sigma escape. With brms’s b_ prefix the coefficient is b_sigma and sigma is the residual SD, so that pair no longer meets. hyp_shadow_arm() and hyp_shadow_disarm() are no longer exported.

  • BREAKING: names that brms’s renaming would give twice are handled as brms handles each case. Measured on brms 2.23.0 (dev/brmsnames-rev-collide-brms.R):

    • two design columns of one predictor that rename alike, y ~ Intercept + x, are refused when the model is built, with brms’s “Internal renaming led to duplicated names”;
    • a name given by two predictors takes brms’s __1 suffix on the later one: bf(y ~ sigma_z, sigma ~ z) has b_sigma_z and b_sigma_z__1, where the second used to be a duplicate that hypothesis() could not reach;
    • the same group-level coefficient on one group twice is refused with brms’s “Duplicated group-level effects are not allowed”. The animal model (1 | gr(id, cov = A)) + (1 | id) is refused this way; give the second term a copy of the column, (1 | id_pe);
    • two responses brms spells alike, y_a and ya, are refused with brms’s “Cannot use the same response variable twice”;
    • a group-level label given twice, the r_gd[lvl.1,Intercept] of the levels lvl 1 and lvl.1, takes brms’s __1 on the later level;
    • an interaction group two of whose levels brms joins to one string, 1_2:3 and 1:2_3 in (1 | gi:hi), is refused. brms fits them as one level and pools their effects; recode the factors.

    No name reaches variables(), hypothesis() or the draws labels twice. What stops working: the models above that are refused.

  • BREAKING: a mixture’s weights are brms’s theta1 ... thetaK. With no theta formula written, variables() and hypothesis() name the mixing PROBABILITIES, which sum to one, as brms does. They used to name the estimated log ratio theta1 (1.966 where the share was 0.876), so hypothesis(fit, "theta1 = 0.5", class = NULL) gave 1.466 where brms gives 0.376. frm_simulate(newparams =) takes the whole simplex, theta1 = 0.9, theta2 = 0.1, and refuses a part of it; it used to read theta1 = 0.7 as a log ratio, a share of 0.665. A mixture with a theta formula keeps the log ratios as coefficients, b_theta1_Intercept. An hmm() transition logit is likewise the coefficient b_tr12_Intercept, not tr12.

  • BREAKING: two more names are brms’s. A monotonic term’s scale is bsp_moxo, as brms names the same quantity (it was b_moxo), and a by-smooth’s smoothing SD is sds_sxf2u_1 (it was sds_sx:f2u_1).

  • BREAKING: VarCorr() returns brms’s structure. A list keyed by GROUPING FACTOR ("patient", as brms and lme4 key it, not "1 | patient"), each entry with sd (coefficients x statistics) and, for a group with correlations, cor and cov (coefficient x statistic x coefficient arrays), then residual__ where brms has it: a sigma with no formula, one row per response on a multivariate model (named ya, y2) with the residual correlations under rescor = TRUE, and no residual__ once any response predicts sigma. The statistics are Estimate, its delta-method standard error as Est.Error, and Wald quantiles at probs. Several blocks on one factor merge under it, as in brms. Smooth, GP, CAR and SPDE blocks are not in it, as in brms; confint_varcorr() reports them. A model with no group and no scalar residual SD is refused with brms’s message. What stops working: VarCorr(fit)[[1]] as a covariance matrix, names(VarCorr(fit)) as term labels, as.data.frame(VarCorr(fit)), and print()’s lme4 table from VarCorr() (the fit’s own print() and summary() keep it). The per-block matrices are varcorr_matrices(fit) on ?frmtmb-sampling-api.

  • BREAKING: fixef(), ranef(), coef() and VarCorr() take brms’s leading arguments in brms’s positions (summary, robust, probs, and pars and groups where brms has them). fixef(fit, FALSE) and ranef(fit, FALSE) used to set flatten and condVar and return the default shape, identical() to the call without the argument. A fit has no draws, so summary = FALSE and robust = TRUE are refused by name with the reason; brms’s defaults spelled out are accepted. flatten and condVar move after ... and must be named.

  • posterior_summary() on a fit, or on a frm_multiple() result, refuses and says to sample first. It used to die inside posterior_summary.default() with “is.atomic(x) is not TRUE”. posterior_summary.default() is brms’s, statistic for statistic, and now summarizes a three-dimensional draws array too.

  • confint(parm =) and profile(parm =) take brms’s b_ spelling of a coefficient as well.

  • BREAKING: frm_simulate(newparams =) takes brms’s names only, the strings variables() returns: b_Intercept, b_x, b_sigma_Intercept, sigma, sd_g__Intercept, sds_sx_1. A bare name (Intercept, x, sigma_Intercept) is refused, and the message gives the brms spelling of each. The prior-draw table attr(sims, "pars") uses the same names. Priors do not change: set_prior(coef = "x") takes the bare coefficient, as in brms. What stops working: every natural-scale newparams list written with bare names; the internal spelling (beta, betad, theta) is unchanged.

frmtmb 0.58.0

  • BREAKING, and the rule behind it. Where lme4 or glmmTMB and brms disagree on an argument name, brms is now the tiebreaker: this package takes brms’s spelling and DROPS the other one. Two spellings of one setting survive only where brms itself carries both.

    predict(), simulate(), frm_bootstrap() and dharma_residuals() now take re_formula. lme4’s re.form is gone from all four and is refused by name, with the replacement in the message. predict() no longer accepts allow.new.levels either; allow_new_levels is brms’s spelling and the only one. pp_check() on a fit drops re.form too, because brms’s own pp_check() forwards to prepare_predictions(), whose formals carry re_formula alone.

  • BREAKING, and the defect it fixes. fitted() took only ..., so every brms argument handed to it was SWALLOWED. fitted(fit, re_formula = NA) returned the conditional fit and said nothing: 1.291651 at the first row of the fixture in tests/testthat/test-arg-refusal.R, bit-identical to fitted(fit), where the population answer asked for is 1.954262 and the largest gap over the 120 rows is 3.065438. A misspelled argument was swallowed the same way.

    fitted() now takes newdata, re_formula, scale, resp and dpar, which is fitted.brmsfit()’s signature less the arguments that need draws, and it is implemented as one call to predict(), so the documented fitted() == predict(type = "response") identity is structural rather than two bodies that agreed.

  • BREAKING, and the reason it is worth the break. An argument that lands in a method’s ... is now an ERROR naming it. 145 registered S3 methods across this package and frmtmb.sample took a ... they never read; twelve of them keep it, and the rest refuse. A method that swallows its dots accepts every argument brms adds in future, and every misspelling today, in the same silence. predict(), conditional_effects() and hypothesis() warned rather than erroring; a warning still returns a number, and under suppressWarnings() or in a loop it leaves no record.

    tests/testthat/test-arg-refusal.R asserts the invariant structurally: no registered S3 method may have a ... it never touches. Two shapes are outside it. Twelve methods of generics that emmeans, insight and marginaleffects own are called BY those packages with arguments of their own choosing, so accepting an unknown name is the contract there (insight:::.n_parameters_component calls get_parameters(x, component = ), which our method has no formal for). And a method whose whole body is an unconditional refusal cannot swallow anything, so it keeps its own message; the test recognises that shape rather than listing those methods by name.

  • An argument brms has that a maximum likelihood fit cannot answer is refused BY NAME with the reason, instead of being reported as an unknown name. fitted(fit, ndraws = 10) says that this fit carries one estimate rather than a posterior and points at frmtmb.sample::posterior_epred(). The same holds for draw_ids, sort, summary, robust, probs and nlpar on fitted() and predict(); for newdata, re_formula, method and resp on residuals(), which reads the fitted rows only; for digits and short on print(); for priors, prob and mc_se on summary(); for summary, robust, probs and pars on coef(), fixef(), ranef() and VarCorr(); for resp on nobs() and family(); and for correlation and pars on vcov().

  • Arguments that R passes through its OWN generics are accepted, not refused, because R’s S3 contract is that a method tolerates what its generic carries. as.formula(model) is formula(object, env = ); model.matrix.default() and aov() call terms(object, data = ); step() calls nobs(object, use.fallback = ) and then drop1(object, scale = , trace = ). All of those worked before the dots were refused and broke afterwards. One table, s3_contract_args, now covers every generic at once, and a test walks R’s own packages for the same pattern so the next name of this kind fails the suite rather than reaching a user. Everything outside the table is still refused, so nobs(fit, nosucharg = 1) and terms(fit, re.form = NA) error as before.

  • residuals() forwarded its ... to TMB::oneStepPredict() on the type = "osa" branch and swallowed them on every other one. The dots are now checked against that function’s own formals under "osa" and refused otherwise.

frmtmb 0.57.0

  • frm_install_generics(pkgname, owners) joins the extension API on ?frmtmb-sampling-api. It is the mechanism frmtmb uses on itself, and owners is now an argument, so an extension that defines a generic under a name another package owns calls it from its own .onLoad() with its own table. frmtmb.sample does this for 28 names. A malformed table is refused rather than installing nothing.

frmtmb 0.56.0

  • frmtmb no longer breaks brms, lme4, posterior, loo, rstantools or bayesplot for objects that already exist. Attaching frmtmb after one of them used to cost that package every S3 method it had on a name the two share, because frmtmb defined and exported its own generic and UseMethod() reads the method table of the namespace where the generic it reached was defined. Measured on the previous release: library(brms); library(frmtmb) then loo(fit) on an existing brmsfit lost all 27 of the generics the two share, two of them SILENTLY by falling into frmtmb’s own .default; it is now 0 of 27, in every load order tested. library(lme4); library(frmtmb) lost all 5 of the names lme4 and frmtmb share; it is now 0 of 5.

    frmtmb now shares the owner’s generic instead of shadowing it. fixef(), ranef() and VarCorr() are imported from nlme, which is Recommended and is where lme4, glmmTMB and brms get them too, and refit() from generics. The rest resolve to the owner’s generic at run time when the owner is loaded, and to frmtmb’s own when it is not, so nothing new appears in Imports.

  • BREAKING, one call. posterior_summary()’s first argument is now x, not object, matching brms’s generic, so posterior_summary(object = m) fails and posterior_summary(x = m) or posterior_summary(m) is the spelling. That is the only caller-visible break: nvariables() and VarCorr() change too, but both only GAIN an argument (... from posterior, sigma from nlme), so no call that named their first argument breaks.

    refit() is a fourth changed signature and a subtler one: it is (object, ...) from generics normally and (object, newresp, ...) when lme4 is loaded, because lme4 defines its own refit generic rather than importing one, so args(refit) now depends on what else is loaded. No caller breaks on it.

    If you write an S3 method on any of these names, match the OWNER’s formals, not frmtmb’s. frmtmb.sample is updated here for that reason and its frmtmb (>= ...) floor moves with this release.

  • as_draws(), as_draws_array(), as_draws_df(), as_draws_list(), as_draws_matrix(), as_draws_rvars(), ndraws(), nchains(), niterations() and nvariables() now refuse a frmtmb_fit by name and point at frmtmb.sample::frm_sample(). They had no method for the class at all, which was R’s own “no applicable method” before and would have become posterior’s “All list elements must be lists themselves” after, since a fit is a bare list.

  • hyp_shadow_arm() and hyp_shadow_disarm() join the extension API on ?frmtmb-sampling-api. A hypothesis() method in another package must arm the reserved-name shadowing note itself, because the generic that dispatched to it may be brms’s; frmtmb.sample’s method for draws had relied on core’s generic doing it and lost the note in every session until it did.

  • posterior (>= 1.0.0) is declared in Suggests. frmtmb registers methods on as_draws_rvars() and as_draws_list() now, and a partial or pre-release posterior that lacks one of them stops frmtmb from loading. posterior 1.0.0, its first CRAN release, already exports both.

  • ?frmtmb-scales is new and says, per method, whether a number is on the link scale, the response scale or neither. Two places where frmtmb and brms disagree are written down there rather than left to be discovered: predict() returns the linear predictor where brms returns the response scale, and summary() prints sigma on its log link where sigma() back-transforms.

frmtmb 0.55.2

  • ?frmtmb-links documents a return value. pkgcheck reports any help page with no \docType, no datasets or internal keyword and no \value as a function with no documented return, and this was the only page in the package that matched. Written the way ?frmtmb-autocor and ?frmtmb-multimembership already answer it, since those are concept pages too.

frmtmb 0.55.1

  • ?frm_compat_features now says which column a caller may key on. name was documented as “how the feature is written in a formula”, which is false for the three _pred rows: a formula writes mi(x), never mi_pred(x). key is the parser name, and frmtmb.sample’s new pre-flight refusal depends on that, so it is a contract rather than an observed regularity now. No behavior changed.

  • ?frmtmb_register_compat states what a refused row promises: the row is a claim about what the code does, not a note for a reader, and it is enforced wherever the enforcing code can see the feature, with that code saying what it can see. Most of core’s refusals are hand-written guards the row describes, and those see everything. The other kind CONSULTS the registry at the point of use, and frmtmb.sample::frm_sample() is the first caller of that kind, so a package that owns a model the sampler cannot run declares one row and needs no code in the sampler. The qualification is where the two differ: deciding whether a model uses a feature usually means knowing WHERE its call sits in the formula, which the parser owns and nothing public carries out of core, so a consulting caller documents the rows it cannot decide and warns rather than refusing on those. Documentation only; the registry itself is unchanged.

frmtmb 0.55.0

Two densities were silently wrong and are fixed. The student-t log density cancelled its own digits as nu ran off, by 57 log units at nu = 1e50, which the grammar fuzz tier had recorded as two optima under a row permutation; and a Bayesian Cognitive Modeling family read a probability on the wrong scale, by 1.5716 nats per observation on the identity link. Neither is a regression: both have been wrong for longer than 0.54.0.

The accessors that make a density exact at a saturated link are now public, which is what a family in another package needs and had to write again. se() reads the family’s own declaration of which scale the known standard error replaces. whittle() stops refusing tapered periodograms. The hazard-container lint runs inside each extension’s own check rather than only at the release tally.

  • A custom family can now write a numerically exact density from another package. dpar_log(), dpar_log1m(), dpar_log_complement() and dpar_complement() are exported: they read a distributional parameter on the LINEAR-PREDICTOR scale, which is the only scale that survives a saturated inverse link. stats::plogis(eta) is exactly 1 in double precision from eta = 36.7368005696771 up, so a density that forms 1 - mu by subtraction gets exactly 0 there and returns NaN for the value and for the gradient; below that the complement is wrong by 1.0e-3 relative at eta = 30. Both regions are reachable in real fits. The accessors were internal, so a family defined outside frmtmb could only write the arithmetic again: frmtmb.coupling did exactly that and now calls the public API instead. They take the dpar’s OWN link, as a name or as a link object, because the linear predictor is on the link scale and reading it as a log odds when the link is a probit is a different, wrong density. See ?"frmtmb-robust-dpars".

    Take the one-sided dpar_log() or dpar_log1m() when the density needs one term. dpar_log_complement() returns both from one log odds, which is what a mixture gate wants, and records a second RTMB::logspace_add() over the whole response that the tape then replays on every gradient evaluation. At 1000 rows the accessor tapes 14002 AD nodes against 10002; one gradient sweep of the cross_wishart() density costs 633 us against 480 us, and 462 us for the hand-written arithmetic it replaces; the accessor measured on its own is 387 us against 205 us. Six interleaved blocks of 2000 sweeps, one process. The R-level density call is not the cost that matters here: it runs once per fit, when the tape is built.

    The .eta_<dpar> entry these read is documented as RESERVED, not as API. It is visible in dpars and it is not a supported thing to read: its meaning depends on the dpar’s link, and whether it is present depends on whether the objective is being taped. The accessors fold both branches in, which is the whole reason to have them. check_custom_family() supplies no linear predictors, so it checks the fallback branch only, and now says so.

  • bcm_contaminant() in inst/bcm/binomial-extras.R fitted a different density from the one it documented whenever link_phi was not the default. It read the stored linear predictor as a LOG ODDS regardless of which link put it there, so only a logit was right. It now calls dpar_log_complement(), which is told the link.

    At the default link_phi = "logit" nothing moves. Old and new are bit-identical at all 15 linear predictors tested from -700 to 700, maximum absolute difference 0, so no logit result anyone has fitted changes.

    Away from the default the old density was wrong, per observation, at eta = 2 with trials = 10, mu = 0.4, y = 0:

    link_phi phi old error, nats
    "logit" 0.88080 0
    "cauchit" 0.85242 +0.0303
    "probit" 0.97725 -0.0965
    "cloglog" 0.99938 -0.1174
    "identity" 0.05 (at eta = 0.05) +1.5716

    Every one of those constructs today: bcm_contaminant() passes link_phi straight to frmtmb_family(), which does not restrict it per dpar. "identity" is the worst case and also the one the framework’s own unit-interval link set allows, where the old code read a probability as a log odds: 1.57 nats per observation at phi = 0.05, falling to 0.19 at phi = 0.5. The file is a shipped example rather than an exported family, so no fitted model in the package’s own tests moves; the Bayesian Cognitive Modeling tier is 363 passing before and after.

The student-t log density loses its digits as nu runs off

RTMB::dt() sends a double straight to stats::dt(), which is accurate, and an AD number to a tape that forms lgamma((nu + 1) / 2) - lgamma(nu / 2) as written. Every fit runs on the tape, so the accurate branch was the one no fit used. The two values agree in every leading digit once nu is large: at nu = 1e10 each is about 2.3e11, where a double is spaced 3e-5 apart, and the difference they must produce is 11.5. Against a 300-bit reference the subtraction is wrong by 1.4e-5 at nu = 1e10 and by 57 at nu = 1e50.

Data with no heavy tails asks exactly this of the density, because a student-t reaches the gaussian only as nu goes to infinity. Past about nu = 1e7 the objective was noise, its gradient changed sign at points the true likelihood is monotone through, and the optimizer stopped at whichever sign change it reached first. That is what the grammar fuzz tier recorded at 0.54.0 as reml-ar1-se-two-optima: the same model fitted to the same data with its rows permuted landed 5.938 apart in parameter space with a logLik gap of 0.004862, 28 times the tolerance derived from the run. It was never two optima.

  • The difference is now formed without the cancellation, by pushing a up 12 steps with the Gamma recurrence until Binet’s remainder series holds and then taking the Stirling difference in a shape whose large terms are never built. Accurate to 1.3e-14 relative over 369 (a, s) points, and 6 ulp end to end over the (nu, z) grid up to nu = 1e50, rising to 2.0e-14 absolute by nu = 1e300, where the head and the normalizing term cancel down to the size of the answer.

    It is not free, and where it costs is the distributional case. With a scalar nu, which is nu ~ 1 and the common model, the objective measured between 0.75 and 1.33 times its old cost over three independent runs on this machine, with a control tape built from the same density putting the noise floor at 10 to 20 percent. The sign is not settled and the size is small either way. The mechanism is two effects that nearly cancel: the same formula written out by hand is already close to the cost of RTMB::dt() at a scalar df, and the recurrence then adds a little back.

    With nu ~ x it is 2.1 to 3.3 times the old cost, because a is then a vector of length n and the 12 fixed recurrence steps become 12 vector log1p nodes where the old code had two lgamma nodes. bf(y ~ x, nu ~ z) + student() is a supported model, so this is a real cost, not a hypothetical one. The recurrence length cannot be shortened by reading nu, which is a parameter the optimizer moves after taping. What does shorten it is the Binet series, which depends on no parameter at all: six terms reach 1e-16 at x = 12 where four need x = 28, so the shift is 12 and not 25. That halved the vector-path cost, and the sweep behind it found no accuracy ordering between the two. dev/reviews/2026-09-08-remlopt.md has the numbers.

    The permuted and unpermuted fits now agree: the log likelihood is BITWISE identical where it was 0.004862 apart, and the parameter vectors agree to 1.8e-11 where they were 5.938 apart. The residue is entirely in nu, the parameter that has no maximum; the two theta agree to 1.2e-16 and 0, and mu to 5.6e-17. frm_allfit() on the same fit went from a logLik spread of 90.1, with one optimizer reporting an impossible -53.05, to a spread of 4.2e-06 across all four. The reml-ar1-se-two-optima entry is gone from the fuzz tier’s pending list and the tier is green without it.

  • The multivariate-t carried the same subtraction in two more places, the gr(dist = "student") covariance blocks and the student() fit of ar(cov = TRUE). Both are fixed with the same helper, and the log(1 + q/nu) in the autocorrelation path is now log1p(), which its sibling in covstruct.R already argued for. A t block reaching its gaussian limit is how a user checks that it reduces to one, and at nu = 1e16 that check was off by 49 log units. It is now exact to the double that carries it.

  • No coefficient moved. Across the reported case the mu estimates agreed to 3.2e-09 before the fix and to 5.6e-17 after: the error was confined to nu, which no user reports, so no scientific answer changes. What changes is that the fit is reproducible.

diagnose() names a distributional parameter with no maximum

A student() fit on data with no heavy tails reports nu in the billions with a standard error in the thousands, and 0.54.0’s diagnose() answered “No convergence problems detected”. The likelihood has no maximum in nu there, so the number reported is where the optimizer stopped.

  • diagnose() gains unbounded_dpar, which names a distributional parameter whose estimate is far out on its link AND whose standard error is larger than the estimate itself, prints the natural-scale value alongside, and says to refit with gaussian() or to hold nu finite with a prior. The pair is the evidence, not either half: a dpar legitimately far out on its link keeps a small standard error. Measured over 84 student fits (seven error laws, n of 60 and 200, six replicates each), se(log(nu - 1)) ran from 4878 to 1.2e4 on the 35 fits whose nu ran off and from 0.35 to 22 on the 49 where it did not, with |log(nu - 1)| below 5.1 on every one of those. The two conditions together fired on none of them. Replicated independently on 10 further error laws and 120 fits: 0 of 99 identified fits flagged.

    READ THE SIGN. nu runs off BOTH ends of logm1, and the check catches both. A large POSITIVE estimate means no heavy tails and the fit is the gaussian one. A large NEGATIVE estimate, with a natural-scale value that prints as 1, means tails heavier than any identified nu can hold, and there gaussian() is the worst answer available: on Cauchy errors log(nu - 1) lands between -20.5 and -17.7 with a standard error of 4003 to 8533, on 8 of 8 fits that reached the boundary.

  • ?frmtmb-families gains a “Degrees of freedom that run off” section saying the same thing where a user of student() will find it, and vignette("diagnostics") gains “A distributional parameter with no maximum” beside its “NaN standard errors” section, which is where diagnose()’s own message already sends people. Both name the two directions and what each one means.

Three defects recorded against the 0.54.0 addition-term work, fixed. Two of them are one guard: the se() scale rule could not tell a second scale from a shape, and a dpar formula walked past it.

  • A family can now say which dpar a known standard error replaces: frmtmb_family(se_dpar = "tau") maps out tau exactly as the convention maps out sigma, and se_dpar = NA says it replaces none, because the known standard error IS the family’s whole scale. That is the case 0.54.0 over-refused. A family with no sigma, another free dpar, and no declaration is still refused, because the core cannot tell a second SCALE, which would be left free and unread, from a genuine SHAPE, which has to stay free. Measured on both: the same data, the same se() column, one family whose extra dpar is a second scale and one whose extra dpar is a skew. The second scale still refuses (before the guard existed it fitted with the scale frozen at its start value and all three standard errors NaN); the skew, declared with se_dpar = NA, fits at a log-likelihood of -285.459 with every standard error finite, the skew estimated at 2.956 (standard error 0.352) where the data were drawn at 3, and 80.06 log units above the same family with the skew pinned symmetric. The refusal now names se_dpar as a fourth way out. predict(dpar = ) reports a declared and mapped-out dpar as the zero it is, which it did for sigma alone before. sigma() is unchanged: it reports the dpar named sigma, and a family without one still gets 1.

  • A dpar FORMULA no longer walks past that guard. se() without sigma = TRUE says the residual scale is known, so the dpar it replaces stops being estimated, and only a CONSTANT pins it: sigma ~ 1 estimates an intercept the density never reads. The guard now reads every route a formula arrives by, bf(), lf(), nlf() and a family’s own default_forms, and refuses the pair by name. This was pre-existing and the BUILT-IN gaussian shared it: measured, bf(y | se(sev) ~ x, sigma ~ 1) fitted at exactly the reference model’s log-likelihood, -62.4977718, with the same two coefficients to seven figures and every standard error NaN. The fit itself said nothing; the flat direction surfaced only when vcov() or summary() was asked for it. The three documented escapes are unchanged and measured again: no formula at all, a constant (sigma = 1), and se(x, sigma = TRUE), which takes a sigma formula as it always did, because the density reads the dpar there.

    It is a breaking change for a family written against 0.54.0 that declares se(), has no sigma, and gave its extra dpar a formula: that model fitted before and is refused now. The refusal names the one-argument fix.

  • The discrete censoring convention announces itself ONCE per session. frmtmb reads a censoring bound on a count as INCLUSIVE, so right censoring at k is P(Y >= k) and every lower edge enters the CDF as F(k - 1); brms emits P(Y > y) for right and interval censoring of a count. On 200 poisson draws at lambda = 4 right censored at 6 the two readings differ by 21.8 to 33.3 log units, median 29.4 over 40 seeds, which is about 0.66 per censored row. (The 0.54.0 entry below gives 20.8 for the same setup; that figure is below all 40 of those draws and should not be quoted again.) The notice fires only on the combination that diverges, a DISCRETE family with RIGHT or INTERVAL censoring, and says the rule, the remedy and where the argument lives. Left censoring, a continuous response and truncation alone are silent, measured. It is a message, so suppressMessages() silences it, and options(frmtmb.notices = FALSE) silences every notice for a session. A call a CENSORING guard refuses, such as one with a half-integer bound, does not spend it; a call refused later, for a reason that has nothing to do with censoring, does. The package’s own suite sets the option off in tests/testthat/setup.R, so no test depends on which file censors a count first.

  • The warning from a capped importance correction now says which of two things happened, because they want opposite advice. A correction that is short of rounds shrinks its move by a factor of three to ten each round, and raising frmtmb_control(importance_rounds =) lands it: that message is unchanged. A STALLED correction takes the same step every round, and its total shift is that step times the round count, so more rounds buy a proportionally larger number rather than a better one. It now says so, and sends the reader to VarCorr(), because a variance component the Laplace fit has already collapsed is what does this. Measured: twelve groups of three Bernoulli rows with no variance component walk at 0.909073 for all five rounds and report a shift of 4.545, and the step is a property of the DRAWS rather than of the data. It changes to 0.317931 at importance_seed = 7, and at one seed it varies less between two datasets, or between core and a frmtmb.learn fit of an unrelated family with the same group and draw counts, than it does between the rounds of a single run.

  • New frm_hazard_reads(), a testing aid for frmtmb and its extensions. It reports every place in a package where $ is used on one of the containers whose slot names collide under partial matching, so an extension asserts the rule on itself, in its own suite and under its own R CMD check, from the one container list frmtmb owns. Until now the rule was policed only from frmtmb’s suite, and frmtmb.coupling passed its own check and its own tests with 34 such reads.

  • whittle() no longer refuses legitimate tapered periodograms. Its smoothness statistic compares ordinates THREE apart rather than neighbors. A Hann window’s transform is three bins wide, so it correlates neighboring log ordinates (0.31) and leaves ordinates three apart alone (-0.003); at lag one that correlation pulled the statistic from 3.29 to 2.27 and refused about 2 percent of Hann-tapered responses, and at lag three the rate is 0 in 2000 replicates at each of nine cells. Hamming and Blackman windows, which this package does not apply, went from 1.0 and 18.1 percent to 0 the same way. A single Slepian taper, which lag one refused 27 percent of the time at 127 ordinates and 56 percent at 255, is now accepted. The threshold curve is unchanged and the false-alarm rate on the flat null is still 0 in 20000 per cell. Detection of a SEGMENT-averaged periodogram declared raw is unchanged within Monte Carlo error. Detection of leakage costs 1.3 points at exponent 3 and 2.3 at exponent 2.5 with 127 ordinates, and under one point at 255 ordinates and above. What the wider step also costs is an estimate smoothed ACROSS FREQUENCY and then declared raw: a three-bin Daniell smooth of a short record is caught 18 percent of the time at 32 ordinates and 58 percent at 64, where the old step caught nearly all of them, and the two agree again above about 100 ordinates. The refusal message no longer suggests a taper the user may already have applied.

frmtmb 0.54.0

Every link in the registry can now be reached and found: a link on any distributional parameter, a reference page listing all seventeen, and the ordinal families going through the registry rather than a hand-written switch. Making them reachable exposed two silent density bugs. A family written in plain R reaches se() and discrete cens() on its own declaration rather than on its name. A family can declare two addition terms mutually exclusive, which is what an allow-list cannot say. And the frequency domain arrives: frm_periodogram(), whittle(), and a measurement of what an untapered periodogram cannot tell you.

Two gates that made a family written in plain R a second-class citizen of the addition-term grammar. Both were found by the Bayesian Cognitive Modeling port, which had to work around them; the workarounds are gone.

  • se() is no longer gated on the family NAME. A family gets the term by DECLARING that it reads it, with frmtmb_family(accepts_aterms = c(..., "se")) or required_aterms, and reads it as aterms[["se"]]. se() is the one core addition term whose entire effect is inside the density, so the declaration is the only thing that can answer whether the term does anything, and a family that does not declare it is still refused, now with a message that says how to opt in. The built-in gaussian and student already declared it and are unchanged. mixture() no longer inherits the declaration from its components: reading the term is half of what se() means, and the other half maps out a dpar named sigma, which sigma1 and sigma2 are not, so mixture(gaussian, gaussian) with se() fitted with both component sigmas at their starting value and every standard error NaN.

    The name test was also a silent wrong answer, which the capability test closes. A custom family that merely CALLED itself "gaussian" and ignored the term was accepted, its sigma was mapped out, and the se() column was discarded: a likelihood with a frozen residual scale and no message anywhere. Measured on main against this branch, same family, same data: ACCEPTED (sigma mapped out: TRUE) becomes a refusal that names the missing declaration.

  • A family that declares se(), has no dpar named sigma, and carries another free dpar is refused by name. se() without sigma = TRUE replaces the residual scale, and the core maps out only the dpar the convention names, so the other one would be left free and unread: measured, it sits at its starting value and every standard error is NaN. That is the condition mixture() is refused for, and opening se() to custom families opened a second route to it. A family whose whole scale IS the known one (no dpar beyond its primaries) is unaffected. Three ways out, all measured: name the scale sigma, pin it in the formula (bf(y | se(s) ~ x, tau = 1)), or write se(x, sigma = TRUE) if it stays estimated alongside the known standard deviation.

  • cens() no longer refuses a family for BEING discrete. It still needs a CDF, which is the gate that always made sense. A discrete family that has one is censored under an INCLUSIVE convention: a censoring bound names a value the response can take, so right censoring at k is P(Y >= k), an interval is P(k <= Y <= k2), and every lower edge enters the CDF as F(k - 1). That is the rule trunc(lb = ) has always followed, so one number means one thing on a response however it is bounded. poisson() is censored as a result. Verified against a hand-rolled likelihood for all four codes and composed with trunc().

    It DIFFERS from brms for RIGHT and INTERVAL censoring of a count, where brms emits poisson_lccdf(y | mu), that is P(Y > y). Left censoring agrees exactly, and so does continuous censoring. On 200 poisson draws at lambda = 4 right censored at 6 the two readings differ by 20.8 log units.

    The divergence is deliberate, because brms is internally inconsistent here and frmtmb cannot be both: brms’s own discrete truncation emits an INCLUSIVE lower bound (lb - 1), so in brms trunc(lb = 6) means Y >= 6 while a right-censored 6 means Y > 6. frmtmb’s trunc() reproduces brms bit for bit, so its cens() had to choose between matching brms’s censoring and matching its own truncation. It is also the only reading consistent with the pre-existing simulate(censored = TRUE), which records k when the latent draw is >= k: 4000 draws from a fit censored at 7 put 0.13250 of the mass at that point, against 0.12190 inclusive and 0.05763 exclusive. The migration vignette carries the argument and the remedy.

    Two smaller consequences. A one-point interval (y2 == y) is legal on a discrete response, where it is exactly P(Y = y); it stays refused on a continuous one. And a non-integer censoring bound on a discrete family is refused by name, because the F(k - 1) shift assumes the unit integer lattice.

  • residuals(type = "osa") is refused on a censored DISCRETE fit, by name. Inclusive bounds put an uncensored row’s support on [lo + 1, hi - 1] rather than the [lo, hi] the one-step window is built on. Every other residual type works, and dharma_residuals() covers the same ground. The link registry became reachable and findable. It has held seventeen links since the last round; almost none of them could be named from the grammar or found in the help.

  • New frmtmb-links help topic documents every link: what it maps, its inverse, the range it maps onto, which families accept it for the mean, which set each distributional parameter admits, and which links carry a robust field and what that buys. Nothing in man/ mentioned probit, softplus, cauchit, squareplus or softit before. Every family constructor’s link argument, frmtmb_family(links =) and the unknown-link error now point at it, and it is in the pkgdown reference index.

  • Family constructors take a link for EVERY distributional parameter, not only the mean, following brms: student(link_sigma = "softplus", link_nu = "identity"), zero_inflated_poisson(link_zi = "identity"), skew_normal(link_alpha = "log"). 23 exported constructors gained one argument per non-mean parameter, 34 arguments in all, and the accepted set for every one of the 34 is brms 2.23.0’s set exactly. Each is validated against what that parameter’s support admits, and an out-of-range link is refused with the parameter named. The density is now invariant to which link produced a given parameter value: over all 37 family-by-parameter pairs, holding the natural value fixed and feeding each allowed link’s own linkfun of it, 34 agree to a spread of exactly zero and the rest to 1.8e-15.

  • New frm_family() names a family and its links for the four families ‘stats’ owns, which have no frmtmb constructor to carry the new arguments: frm_family("gaussian", link_sigma = "softplus"), frm_family("poisson", link = "softplus"). It is the analogue of brms’s brmsfamily(). frmtmb still does not shadow gaussian(), poisson(), binomial() or Gamma().

  • cumulative(), sratio() and cratio() go through the link registry instead of a hand-written switch, so they now take cloglog, cauchit and probit_approx as well as logit and probit, and cumulative() also takes softit. This is brms’s roster exactly. acat() still takes logit alone, and it is the one place frmtmb takes less than brms. The reason is the density, not the link: probit, cloglog, cauchit and softit all map onto (0, 1) and brms accepts every one of them for acat, but off the logit brms computes a category probability from a SECOND expression, a product of distribution functions times a reversed product of survivals. That expression agrees with acat’s log-linear form when the distribution function is logistic, so it generalizes the same model rather than replacing it, but it has to be written and taped, and substituting a distribution function into the log-linear form does not reach it. acat()’s refusal says all of this, and the frmtmb-links topic records the gap in its table beside the families that do take those links.

  • The ordinal log-space density generalized from the logit to every link that carries an exact log odds. Without it cloglog and probit_approx would return a log density of -Inf with a dead gradient past a linear predictor of 2. Against a 500-bit reference the generalized form holds 1e-13 or better out to a linear predictor of 25. The logit path is arithmetically unchanged.

  • Ordinal thresholds now start on the fitted link’s own scale rather than always on qlogis.

  • summary() and print() name the link of every distributional parameter on a Links: line under Family:. A coefficient is reported on the link scale and the family name no longer says which scale that is. An ordinal fit reports the distribution function its thresholds are read through, not mu = identity, because the distribution function applies to tau - eta and never to eta. A multivariate fit names each response, and the line wraps between entries rather than inside one, which a mixture needs: two components run to 106 characters unwrapped.

  • BEHAVIOR CHANGE, cumulative(link = "probit"). It now takes the log-space path the logit already took, so a saturated threshold pair is scored exactly instead of through a difference of two rounded distribution functions. Estimates move by the size of that error, which is 1.6e-04 in the log density at a linear predictor of 8 and unbounded beyond it.

  • BEHAVIOR CHANGE, cratio()’s robust branch read its distribution function at tau - eta and relied on 1 - F(-x) = F(x). Measured over x = -2, -0.5, 0, 0.5, 2, that identity holds to 1.4e-16 for the logit, 5.6e-17 for the probit, 1.3e-16 for probit_approx and exactly for the cauchit, and FAILS for the cloglog at 2.6e-01: at x = 0 it gives 0.36788 against 0.63212. It now reads at eta - tau, which is where the density actually evaluates. No logit result changes. Against a 500-bit reference on a four-category cratio, the naive generalization that keeps reading at tau - eta has relative error 1.000 on the cloglog and is exact on every other link; the form that shipped is 1.1e-16 on all of them, at linear predictors where the plain path returns negative infinity.

  • Two latent bugs were fixed on the way, and they are the reason this work is more than a grammar addition. Neither could fire before, because the argument that triggers each did not exist.

    log_dpar() handed back the linear predictor AS the log of a positive parameter, which is true only on a log link. With link_shape now an argument, negbinomial(link_shape = "softplus") would have read a softplus predictor as a log and returned a wrong density with no warning and no error: at eta_shape = 0.5, so shape = 0.974077, the old code is off by up to 0.339 nats at one point and 0.608 over y = 0:6, and its answer is reproduced exactly by the misread it makes. The shipped version agrees with dnbinom() to 8.9e-16.

    gate_logs() had the same flaw for a unit-interval parameter, and read a probability as a log odds: zero_inflated_poisson(link_zi = "identity") with zi = 0.3 used 0.5744 instead, an error of 0.533 nats. It is now exact to 2.2e-16. What a family declares about the addition terms it reads: the exclusivity the 0.53.0 allow-list could not express, the compatibility cells those declarations already settle, and a structure slot that had no reader.

  • frmtmb_family(exclusive_aterms =) declares that a set of addition-term values say the SAME thing to the density, so at most one of them may be supplied. An allow-list cannot express this, because both spellings are legitimately on it: wiener() reads its boundary indicator from dec() and falls back to vint1, so rt | dec(u) + vint(1 - u) ~ 1 passed every guard and fitted with a log-likelihood bit-identical to the dec()-only model, the two columns contradicting each other and nothing saying so. Frame assembly now refuses the combination by name, says which spelling the density reads and which to drop.

    Written in VALUES, as required_aterms is, so the two compose: an any-of group in one and the same set in the other read together as “exactly one”. It is opt-in rather than implied by an any-of group, because gddm() genuinely reads dec() and vint1 together, as the boundary and the condition index. Declaring a set exclusive whose values required_aterms demands TOGETHER is refused at construction, because no model could then satisfy the family. A mixture() keeps a set only if every component declares it, which is the intersection where the allow-list is the union.

  • frm_compat() derives its addition-term refusals from frmtmb_family(accepts_aterms =) instead of leaving them untested. A term outside a family’s allow-list is refused BY NAME at frame assembly, so untested was never right for that cell: nothing is missing, the guard exists, and the reason is the declaration. With frmtmb alone, 104 cells move from untested to refused, all of them trials(), vint() and vreal() against families that do not read them; with frmtmb.eam also loaded it is 147, the extra 43 being dec() and the eam families’ own cells. Nothing else moves at either size: audited before the change, no hand-written row disagreed with any declaration.

    Only the refusal is derived. A family that ACCEPTS a term has said nothing about whether the pair works, so untested stays there. And a pair the hand-written rules already refuse keeps its own note, which usually says more than the declaration does; compat_aterm_rules() is exported so a contributing package derives its own rows the same way, deferring to both its rules and the core’s.

    r("trials()", "kind:family", "untested") is gone, superseded: the four families that take trials() are named and the other 32 refuse it by declaration, so the kind-level rule won no pair.

  • A structured family’s generic refusal names the family’s own unit: “its likelihood factorizes no finer than a hidden-Markov sequence” rather than “does not factorize over the rows of the data”. That sentence is what frmtmb_structure(unit =) was documented for and had no reader for; it was also false of a structure that carries no loglik, which is a capability declaration on a family whose likelihood IS rowwise, and those now get a sentence that is true of them.

Four usability defects of hierarchical NONLINEAR models, found while fitting bf(..., nl = TRUE) with random effects on the nonlinear parameters.

  • A nonlinear parameter named after one of the family’s own distributional parameters is refused BY NAME. bf(I ~ mu - chi * logw, mu ~ 1 + (1 | id), chi ~ 1 + group, nl = TRUE) used to die inside model.frame() with R’s own “object ‘mu’ not found”, which names neither the parameter nor the collision: mu is subtracted out of the nonlinear parameters because the family already owns it, so the body’s reference to it was asked of data as a column. The refusal says which name, which family, and what the family reserves, and it comes from the parse, so par_template() refuses it too. brms 2.23.0 on the same formula silently discards the nonlinear body and fits an intercept-only model.

  • A nonlinear body that names its OWN parameter with no column behind it is refused the same way, wherever the name comes from, including nlf(sigma ~ sigma + 1). A real data column of that name still wins, so a body reading a column called mu keeps fitting, and so does a body reading ANOTHER dpar’s per-row value (nlf(sigma ~ ls + th * log(abs(mu)))), which is a deliberate extension and not a collision.

  • start and newparams name the collision when a nonlinear parameter is named after a parameter-template component (beta, betad, b, theta, thetaac, thetar, miss). Such a model FITS; only the start list is ambiguous, and it was resolved silently in favor of the component. start = list(b = 1) on a model with a nonlinear parameter b reported “start$b must have length 120”, which describes the random-effect vector, an object the caller never meant. It now says so, and gives the spelling that reaches the parameter (start = list(beta = c("b_(Intercept)" = 1))).

  • NEW fixef(object, flatten = TRUE): one named vector in the vcov() / confint() / par_template() spelling, dpar_column with the location parameter’s own coefficients unprefixed. hypothesis() is a third vocabulary and is NOT included in that claim: it strips parentheses, so it takes sigma_(Intercept) only backquoted and spells it sigma_Intercept itself. unlist(fixef(fit)) is NOT that vector and never was: it is base R’s composite of a list KEY and a design-column name (mu.x), differing from the model’s own name both in the separator and in naming mu where the model does not. ?fixef said the two agreed. The unlist() spelling does not move: 19 files in this repository index literal dpar.column names out of it, across core and five extensions, and it is base R’s composite rather than a name this package assigns.

  • BEHAVIOR CHANGE, and what changes is the NAMES of frm_bootstrap()’s default statistic, from mu.x to x: the default FUN is now fixef(f, flatten = TRUE), so apply(bs$t, 2, sd) and sqrt(diag(vcov(fit))) line up by name, which is what that comparison is for. confint(bs) row names change with it. A FUN you supplied yourself is untouched.

  • Every ranef() block is addressable. A nonlinear model with (1 | id) on three parameters returns three blocks all named id, and ranef(fit)[["id"]] reached the first one silently. $ and [[ now also take the BLOCK LABEL, ranef(fit)[["chi: 1 | id"]], which is the "term" attribute each matrix already carried, VarCorr()’s key and the grp column of as.data.frame(). A bare grouping-factor name that several blocks share is refused and names the labels instead of answering with the first. What breaks is ranef(fit)$g on a factor carrying more than one block, which returned the first block and now errors. Three ordinary spellings do that, so this is not a nonlinear-model corner: an uncorrelated slope (1 + x || g), which desugars to the two blocks 1 | g and 0 + x | g; the same two written out as (1 | g) + (0 + x | g); and a random effect on more than one distributional or nonlinear parameter, such as bf(y ~ x + (1 | g), sigma ~ (1 | g)) or (1 | id) on three nonlinear parameters. A CORRELATED slope (1 + x | g) is one block and still answers to $g. The 0.52.0 grouping-factor keying is unchanged, positional indexing is unchanged, and frmtmb.sample’s ranef() on a draws object, which indexes by position, needs no change.

  • diagnose() names FLAT DIRECTIONS: outer parameters with zero gradient and an empty Hessian row. A Gaussian peak exp(lamp) * exp(-0.5 * ((w - pk) / exp(lsig))^2) whose centre starts at 0 while w runs from 1 to 45 underflows to zero everywhere, so its amplitude, centre and width have no gradient at all; the covariance then fails and EVERY standard error is NaN. The old report listed all nine parameters as having a bad standard error, which names none of them, and the warning asserted overparameterization. The model is not overparameterized. The emptiness of the row is measured rather than assumed, by perturbing the parameter and seeing whether the gradient moves at all (fit$obj$he() is unavailable on a model with random effects), and the check is gated on a covariance that already failed, so a healthy fit pays nothing. The vcov(), summary() and fit-time warnings name those parameters in place of the overparameterization guess. vignette("frmtmb") gains the starting-value rule for bump-shaped terms and vignette("diagnostics") the two causes of NaN standard errors.

The frequency domain. A spectral fit was already possible, because the Whittle likelihood IS an exponential GLM on the periodogram with a log link and exponential() is mean-parameterized, but nothing said so and nothing prepared the data. vignette("spectral") carries the worked AR(1) and power-law fits.

  • NEW frm_periodogram() turns one or more series into the data frame a spectral model reads: one row per retained ordinate, with the frequency in Hz. The zero and Nyquist ordinates are DROPPED, so a series of length m gives floor((m - 1) / 2) rows. Checked against stats::spec.pgram() at taper = 0, detrend = FALSE, fast = FALSE: every ordinate agrees to a ratio of exactly 1 and every frequency to 0, which pins the scaling and the frequency axis together. It offers detrending, Hann and split-cosine tapers, Bartlett segment averaging, and takes a matrix of series or a group vector of unequal lengths.

  • NEW whittle(tapers = ) names the assumption a spectral fit makes: ordinates independent, exponential about the spectrum. It is exponential(log) at one taper and Gamma(log) with the shape FIXED at tapers above it, through a new family slot that reaches the existing constant-dpar mechanism, so an averaged periodogram spends no parameter on a number the data preparation already decided. On an AR(1) of 1024 points at phi = 0.7 it returns 0.708617 (se 0.02218) against arima(method = "ML")’s 0.707178 (se 0.02204).

  • whittle() refuses what the response alone can prove, and states what it cannot see. A response that is not positive is log power or power in dB, which is the common wrong response and is caught exactly. A response too SMOOTH to be raw is caught because var(diff(log(I))) is 2 * trigamma(k) for ordinates of shape k WHATEVER the spectrum is, and both smoothing and reordering only add to it, so a value far below the model’s is one-sided evidence. It refuses 98 to 100 percent of periodograms that were segment averaged without saying so, and 89 to 100 percent of leakage-dominated ones, against a false-alarm rate measured at 0.000 percent under the flat null over 20000 samples per cell and 0 percent on AR(1), AR(2) and 1/f-with-a-peak spectra. Two limits are documented rather than hidden: four averaged ordinates below about 200 rows are only caught 46 percent of the time, and a near-unit-root AR(1) fires occasionally because its spectrum really is leakage-dominated. A Hann-tapered periodogram is legitimately raw but its neighbours correlate, so it is refused about 1 to 2 percent of the time; the message says so rather than prescribing a taper the user may already have applied.

  • NEW frm_series_draw() draws a time series from a fitted spectrum. simulate() is refused on a whittle fit instead of returning ordinates that nothing in the object identifies as ordinates.

  • MEASURED, and the practical result of the work: an untapered periodogram cannot estimate a spectral exponent steeper than about 2, at any length. On a cut power law of 300 replicates, true exponents of 2 and 3 both come back between 1.91 and 1.97, because the leakage floor of an untapered transform falls like f^-2 and anything steeper sits under it. A LONGER recording is monotonically worse: the bias at exponent 3 goes from -1.033 at 256 points to -1.094 at 1024. A Hann taper removes it (0.087 and 0.029). At an exponent of 1, flatter than the floor, the raw estimate is unbiased, which is why this is a taper argument and not a taper default. Whittle’s own small-sample bias in phi is separately smaller in magnitude than exact Gaussian ML’s in every cell measured at 64 and 128 points, and comparable above.

frmtmb 0.53.0

set_prior()’s own class names mean what they mean in brms; the structured-family protocol factorizes a whole-response log-likelihood per row and per group, so the importance correction, loo() and deviance residuals reach hmm() and the learning families; every brms link name is in the registry; car(type = "esicar") prediction errors are exact; a Bayesian Cognitive Modeling tier fits fifty-two of the book’s models against their Stan programs; and frmtmb.learn, the sixth extension, brings reinforcement-learning families.

  • frmtmb_register_compat(expects = ) accepts a name another package already supplies. It used to refuse one, so frmtmb.sample, which expects hmm, failed to load in every session that had loaded frmtmb.latent first. An expectation the session has already met is met; only a name the same call supplies in features = is refused, since that declaration contradicts itself.

  • BUG FIX. mixture() started every component at a quantile of the raw response. For a bounded mean that is a count outside the logit’s range, so every component fell back to the link origin, where they are the same distribution, and mixture(beta_binomial, beta_binomial) on the malingering data of Lee and Wagenmakers sat at two identical components (logLik -64.26) with no warning where a separated start reaches -58.71. A component whose mean link is on (0, 1) now starts from the quantile of y / trials, clamped to (0.02, 0.98) the way the component families clamp their own start, because eight of those twenty-two respondents score 45 of 45 and put the two-thirds quantile at exactly 1. Found by the Bayesian Cognitive Modeling port.

  • residuals(type = "deviance") on a structured family that declares loglik_row but no fitted_mean now says which half is missing (the sign of each row’s departure from its conditional mean) instead of claiming the family has no unit deviance and listing thirty built-in families that do.

  • car(type = "esicar") standard errors no longer carry con_sd. expand_b() centers the field each connected component contributes, so the linear predictor sees P b with P the centering projection, but the delta method paired the design columns with b through dc/db = I. Every predict(se.fit = TRUE) standard error and every ranef(condVar = TRUE) conditional SD therefore carried the inert component means, exactly con_sd^2 in the variance: measured 1.348e-5 relative in a standard error at the 1e-3 default, 1.347e-3 at con_sd = 0.01, and 7.5 to 12.7 percent at con_sd = 0.1, growing a hundredfold per decade. The Jacobian is now the projection itself, so an esicar block’s contribution is Z P V P' Z' and P annihilates that coordinate whatever its scale. On a 4 by 4 lattice the standard errors drop by 1.348e-5 relative and the variance by exactly con_sd^2; a singleton component’s conditional SD was exactly con_sd and is now exactly zero, which is the uncertainty of a field value that is exactly zero. logLik(), the estimates, ranef()’s values and VarCorr() are bit-identical, and so is every reported number for escar, icar and bym2. This closes the residual the 0.52.0 entry below documented and the esicar review recorded as R2. Prediction standard errors for a reduced-rank (rr) block are unaffected: they already went through this Jacobian.

  • conditional_effects() on a fit reads its display quantity from a new ce_pred_dpar() rather than deriving it inline. Same answer, same models: the expression moved, and mean_display is now is.null(pred_dpar), which it always was. The point of moving it is that a sampling extension drawing the same curves per posterior draw has to make the same choice, and could not: mean_is_mu() is a structural test of a family’s mean_fn and stays private.

  • The conditional-effects seam of ?"frmtmb-sampling-api" gains eight exports, so that a sampling extension can return the frame this package returns instead of assembling a second one: ce_frame() (the column set and its order), ce_display_kind() (which of the three displays a call asks for, and the refusals categorical = owes), ce_group_vars() (the grouping variables a re_formula = NULL call blanks), and ce_new_level_spec(), ce_boot_grids() and ce_draw_new_levels() (the placeholder level and the draw that makes an unobserved group carry its own effects), ce_pred_dpar() (what gets predicted, which is not always the dpar the display is labeled with) and ce_dots() (the allow_new_levels spellings a conditional_effects() call accepts). All eight were already what the fit method used; frmtmb.sample was reimplementing five of them, four badly. See that package’s NEWS for what the exports bought.

  • frm_lp_basis(newdata = ) now makes the same statement about a ps() knot span that predict(newdata = ) makes. The two doors evaluate the same closures at the same points, and only one of them said that the basis stops being a partition of unity past its frozen outer knots; a curve drawn through the basis seam was silent about a cliff the fit-end report exists to warn about. The warning fires ONCE per ps() term per call, whatever the grid width or the coefficient count, and its text is byte-identical to predict()’s on the same fit and grid. In sample (newdata = NULL), fitted() and simulate() stay quiet, as before.

  • FIX: the span check could not run on a taped body at all. Its guard was is.numeric(), which is TRUE for an RTMB advector, so a ps() term whose argument names a nonlinear parameter would have raised “Comparison is generally unsafe for AD types” as soon as any taped caller armed it. The guard is inherits(x, "advector") now, and the basis seam additionally evaluates the body once OFF the tape, so that case is checked on numbers rather than skipped.

  • The ps() span warning carries the condition class frmtmb_ps_span_warning, so a consumer can catch, muffle or escalate this one warning without matching on its text. frmtmb.spline uses it to surface the warning once under its own function names and to refuse a feature search whose bracket leaves the span.

  • BEHAVIOR CHANGE. set_prior()’s own class names now mean what the same names mean in brms. A distributional parameter’s own name is a class here as it is there: set_prior("student_t(3, 0, 2.5)", class = "sigma") is a density on sigma ITSELF, on its natural scale, with the inverse link’s log-Jacobian applied. The two brms spellings are mutually exclusive by model shape, measured off brms::make_stancode(), and frmtmb now refuses each on the other’s model, by name, saying which one applies:

    the model the spelling it takes
    bf(y ~ x) class = "sigma", a density on sigma
    bf(y ~ x, sigma ~ 1) or sigma ~ x class = "Intercept", dpar = "sigma", a density on the log-scale intercept

    What changes in your numbers. Before this release, set_prior(..., class = "Intercept", dpar = "sigma") was accepted on a model with no sigma formula and meant a density on LOG sigma. On a gaussian y ~ x with 200 rows whose maximum-likelihood sigma is 2.0103, normal(0, 0.5) gave 1.9965 that way; the same script now stops, and class = "sigma" gives 1.9429, which is what brms::prior(normal(0, 0.5), class = "sigma") gave all along. To reproduce the OLD placement, write sigma ~ 1 in bf() (that model has a log-scale intercept, and the same spelling addresses it), or reach the raw parameter with prior = list(betad = ). A class = "b" prior on a dpar’s slopes is unaffected: it was link-scale before and is link-scale in brms.

  • get_prior() lists a distributional parameter under whichever of the two spellings its model offers, which is the row brms lists: class "sigma" where sigma has no predictor, class = "Intercept" with dpar = "sigma" where it has one. A parameter with NEITHER spelling, because its class is one frmtmb refuses by name, is left out rather than advertised: a mixture proportion with no predictor of its own was briefly listed as class "theta1", which set_prior() refuses.

  • set_prior() refuses class = "b", dpar = <name> where that parameter’s predictor is an intercept only. There are no slopes for the row to address there, so it used to be accepted and applied nothing at all, silently; brms refuses the same row.

  • set_prior() refuses a coef or a group on a distributional parameter’s own class. The class names one parameter, so neither argument had anything to narrow to, and both used to be accepted, printed, and then dropped while the prior applied anyway.

  • print() on a prior list, and the slot names inside prior error messages, spell a density on a distributional parameter with that parameter’s own class: student_t(3, 0, 2.5) class=sigma scale=natural where a translated row used to print class=Intercept dpar=sigma scale=natural. What is printed is what set_prior() accepts on that model, which is the promise the printer makes. frm_sample()’s default-prior announcement uses the same spelling.

  • The brms translation is now a pass-through rather than a second set of rules: a translated row keeps its class and set_prior() decides the placement, so frm(prior = brms::prior(...)) and frm(prior = set_prior(...)) are one code path and cannot drift. The priors tier fits the same model both ways and asserts the objectives are identical to 1e-12, class by class.

  • set_prior() refuses a coef on class "sd" or "cor", which it used to accept, print, and then apply to the whole block. The translated route already refused it; both do now.

  • A prior class frmtmb keeps somewhere else (sds, sdgp, lscale, simo, sdcar, car, brms’s mixture theta1) is refused by name from set_prior() too, with the same sentence the translated route gives, rather than being read as a distributional parameter that the model does not have.

  • No natural = or scale = argument was added: after this change the placement follows from the class, which is brms’s own rule, so there is nothing left for such an argument to override.

  • New vignette("bayesian-cognitive-modeling"): a reference list of the models in Lee and Wagenmakers, Bayesian Cognitive Modeling, each with its frmtmb spelling and how it was checked against the Stan port of the same model. tests/testthat/helper-stan.R is the harness the checks run through: give it a Stan program, its data and a map from an frmtmb estimate to its parameters, and it asserts that log_prob equals frmtmb’s log density there plus a stated constant. The families the book needs and the core does not ship are in inst/bcm/, alongside inst/rl/, and inst/COPYRIGHTS carries the BSD-3 notice of the Stan programs the tier adapts.

Three seams in the structured-family protocol: a family can say how finely its likelihood factorizes, a family can say which addition terms it takes, and the compatibility vocabulary is built once instead of once per registration.

  • frmtmb_structure() gains loglik_row and loglik_group. loglik returns one number for the whole response, which is what the objective needs and less than every other consumer needs, so a structured family was refused frm(importance = ), loo(), waic() and deviance residuals whether or not its likelihood factorized. The two slots carry the pieces a family HAS, at the finest granularity it has them: one value per row, or one per level of the block’s new reserved group entry. unit is unchanged and still declares what may honestly be left OUT, which is a different question: the worked rw_delta() family factorizes per trial and leaves out a whole subject.

  • frm(importance = ) corrects a structured family that declares either slot, instead of refusing every one of them. The refusal that remains names the missing declaration. The family’s own units and the model’s grouping levels must be the same partition of the rows and are checked: summing a per-subject likelihood into a per-item proposal is silently meaningless, so it is refused by name. A family’s handling of the stacked design the correction evaluates is verified against the plain objective, per group, by the check that was already there.

  • residuals(type = "deviance") works for a structured family that declares loglik_row and attaches its saturated log-density to what that slot returns. Without the saturated half the refusal names it, rather than assuming a value that is right for a Bernoulli trial and wrong for a Poisson count.

  • frmtmb_family() gains accepts_aterms, the allow-list that complements required_aterms. An addition term a family never reads used to be parsed, stored on the fit and ignored in silence: lba(3) fitted rt | dec(two) + vint(choice) ~ 1 with fixed effects bit-identical to the model without dec(). Frame assembly now refuses such a term by name and lists the ones the family takes. The built-in families declare theirs. NULL, the default, accepts every registered term, so a custom family written before this keeps its behavior, and the check runs after every guard that can say something more specific, so no existing message changes.

  • The frm_compat() feature vocabulary is cached and the cache is extended by each registration rather than rebuilt. An extension registering four addition terms from its .onLoad() paid four 16 ms builds and now pays one.

frmtmb 0.52.0

A brms prior means what it means in brms; conditional_effects() plots the expected response and conditions on a new level; car(type = “esicar”) is the exactly constrained model; the spline seams a curve needs, and ps(), a penalized curve a nonlinear body can consume; and the drift-diffusion extension becomes frmtmb.eam with two more accumulator families.

  • New vignette("reinforcement-learning"): a worked example that writes a Rescorla-Wagner delta-learning family for a two-armed bandit with frmtmb_family() and the structured protocol’s whole-response loglik slot, then fits it hierarchically through the ordinary grammar. The family is not exported. It lives in inst/rl/rw-delta.R, which the vignette reads with knitr::read_chunk() and tests/testthat/test-rl-example.R fits, so the page and the test cannot drift apart. The example checks itself three ways: against an independent scalar reference for the recursion, against a Stan program’s log_prob at frmtmb’s own estimates (gated on FRMTMB_BRMS_FIT_TESTS), and by parameter recovery over 100 simulated datasets. It also measures what the Laplace approximation costs at 20 trials per subject, and names the one protocol seam it wanted and did not find: loglik returns one scalar, so frm(importance =) and a pointwise log-likelihood are out of reach even for a sequential family whose likelihood does factorize. See dev/rl-findings.md.

  • The drift-diffusion extension is renamed frmtmb.eam (evidence accumulation models); see its NEWS.

  • The AD-safe link registry completes the brms 2.23.0 roster. New probit, probit_approx, cauchit, softit, softplus, squareplus, sqrt, log1p and 1/mu^2 links, each reproducing brms’s own definition, so bernoulli(link = "probit") is now the everyday spelling of a probit regression rather than a custom link list passed through get_link(). 1/mu^2 is the inverse Gaussian canonical link and is unrelated to power12, the tweedie power link. Every link name brms accepts is now accepted here.

    Six of them carry a robust field. Four of the six move a saturation boundary, and each boundary is measured rather than assumed. probit’s log-odds is a difference of two pnorm logs, which holds to |eta| = 38 where the plain round trip gave out at 6.4; probit_approx’s is the cubic itself and never saturates, where the plain path gave out at 5.9; squareplus’s log mean is asinh(eta / 2), exact where the sum (eta + sqrt(eta^2 + 4)) / 2 has cancelled to zero; softit’s log-odds is the softplus’s log. softplus and sqrt carry a log_eta for a different reason: it selects the family’s robust log-density, and without it negbinomial() forms mu + mu^2 / shape, whose excess over mu is lost to rounding at a mean near zero. cauchit has no robust field and needs none: its tails are polynomial, so the plain round trip is still accurate at eta = 3e9.

    stats::binomial(), stats::poisson() and stats::Gamma() validate their link string through stats::make.link() before frm() sees it, so the four names stats does not know (probit_approx, softit, softplus, squareplus) have to come in through one of frmtmb’s own family constructors. See vignette("brms-migration") and dev/links-findings.md.

  • BUG FIX. conditional_effects() returned an INVERTED band on every decreasing link. inverse and 1/mu^2 send the lower linear predictor to the upper response value, and one of the three places the band’s endpoints are transformed did not order them afterwards, so Gamma(link = "inverse") came back with lower__ above upper__ on every row. The endpoints are now ordered by the link’s direction rather than sorted, which also keeps a missing bound on the side it belongs to.

    A bound the link’s domain does not reach is now NA instead of NaN, with one warning naming the link and the count. Two cases: 1/mu^2 is 1 / sqrt(eta) and has no value once the band reaches eta = 0, so such a band has no upper bound at all; and inverse with the band straddling zero has a pole between two finite ends, where neither end bounds the response. The second was previously reported as an ordinary interval.

    This surfaced through the new 1/mu^2 link, which makes frm(family = stats::inverse.gaussian()) fit for the first time, but the inversion was not new: it reproduces on Gamma(link = "inverse") with the link registry untouched.

The ten defects the brms post-fit method tier recorded in dev/brms-methods-tests.md, repaired. Nine of them are in conditional_effects(), which now returns brms’s frame, brms’s grid and brms’s numbers wherever the two packages mean the same thing.

  • BEHAVIOR CHANGE. conditional_effects() draws the EXPECTED RESPONSE on every family. Its default method = "epred" took the point estimate as the inverse link of the mu predictor, which is the expected response only when the family’s mean is that. On a zero-inflated fit it plotted exp(eta) where the mean is (1 - zi) * exp(eta): 15% high at the first grid point and 268% high at the last, with no warning and a smooth curve of the right shape. On a hurdle fit the error changed sign along the curve, 14% below the mean at one end and 237% above at the other. fitted(), predict(type = "response") and conditional_effects(method = "predict") were right all along, so the package disagreed with itself on the plotting path alone.
  • Which fits move, in full. The gate is mean_is_mu(), so the default curve changes for fourteen families: asym_laplace, beta_binomial, binomial, cox, hurdle_gamma, hurdle_lognormal, hurdle_poisson, lognormal, shifted_lognormal, zero_inflated_asym_laplace, zero_inflated_beta, zero_inflated_binomial, zero_inflated_negbinomial and zero_inflated_poisson, plus any response carrying trunc() and any mixture(). Every other family is unchanged, bit for bit, which was checked family by family.
  • lognormal was a defect of its own and nobody had reported it: its mu link is the identity, so the old path plotted the LOG-SCALE LOCATION as if it were the response. On a fit whose smallest observation is 0.077 the drawn curve ran from -1.489 to 1.454, negative for a strictly positive response, where the mean exp(mu + sigma^2/2) runs 0.286 to 5.423. shifted_lognormal, asym_laplace and cox move for the same reason.
  • The band on that curve is the delta method over EVERY distributional parameter’s coefficients jointly, so the cross-parameter covariances are in it rather than dropped. It is symmetric on the mu link’s scale when the mean lives there, on the log scale when the mean is positive, and on the response scale otherwise, which reduces exactly to the old link-scale band wherever the mean is the inverse link of mu. On a zero-inflated fit its width is within 13% of a 200-refit bootstrap band’s.
  • BEHAVIOR CHANGE. conditional_effects() returns brms’s columns: the varied predictor, then every other model variable at the value it is held at, then cond__ (always, not only under conditions =), effect1__ and, for a two-variable effect, effect2__. brms’s own plot() facets on cond__, so ported faceting code has something to facet on, and the held values are no longer invisible.
  • BEHAVIOR CHANGE. A two-variable effect holds its moderator at the EXACT mean +/- sd, not at signif(mean +/- sd, 3). Display rounding moved to the effect2__ label, where brms puts it. The curve was the model evaluated at a covariate value nobody chose, and how wrong it was depended on the coefficient rather than on anything visible in the plot.
  • BEHAVIOR CHANGE. The two-variable grid varies the FIRST effect slowest, as brms’s does, so the two frames now agree elementwise and a script that indexes rows positionally ports.
  • int_conditions = is implemented, with brms’s meaning: a named list of the values (or a function of the observed column) that one or both variables of an effect are evaluated at. It was accepted, ignored, and reported as an unknown argument to predict(), a function the user had not called.
  • BEHAVIOR CHANGE. re_formula = NULL conditions on a NEW group, as brms does, and the grouping column of the returned frame says NA. It used to take the first observed level silently: on sleepstudy that was Subject 308, a curve 86.5 away from the population one at its furthest point, with nothing in the frame to say whose it was and the choice depending on factor level order. A new group’s conditional modes are zero, so the curve is the population curve and the random-effect variance goes into the band instead; brms draws that group’s effects afresh per posterior draw, which is the paradigm difference. To condition on an observed group, name it: conditions = list(g = "3").
  • BEHAVIOR CHANGE. categorical = is honored on an ordinal or categorical fit, and the per-category layout is keyed "x:cats__" as brms keys it. The argument was accepted and did nothing, so the other layout could not be asked for at all, and ce[["x:cats__"]] was NULL on a frmtmb result. The default is unchanged (one curve per category, which is what brms’s own message asks the user to switch to); categorical = FALSE gives brms’s default summary, the expected category number sum(k * p_k), with a delta-method band of its own.
  • BEHAVIOR CHANGE. A mo() predictor gets one grid point per LEVEL, as brms does, instead of the 100-point continuous grid it shared with every other numeric predictor. A monotonic effect is defined at the ordered levels of its variable and nowhere between them, so 96 of those 100 points were the model evaluated where it has no meaning.
  • BEHAVIOR CHANGE. A trials() variable is held at 1 on the effect grid unless conditions = pins it, which is brms’s rule, and the call says so once. A grid row is one artificial observation, so the mean number of trials was not a whole number and the expected count over it was not a quantity anyone asked for. method = "predict" used to refuse the same model outright.
  • conditional_effects() finds a covariate that sits on a distributional parameter rather than on mu. On bf(y ~ 1, theta1 ~ x) + mixture(...) it refused with “No plottable predictors found for dpar ‘mu1’”, naming the one linear predictor it had looked at rather than the model the user fitted. It falls back to every parameter of the response only when the selected one has nothing to plot, so a model whose dpar does have terms enumerates that parameter’s terms exactly as before.
  • An unknown argument to conditional_effects() is reported against conditional_effects(), once per call. It used to be forwarded to predict() and reported there, and only from the branches that forward: on an ordinal or categorical fit an unknown argument was discarded in complete silence.
  • conditional_effects(band = "boot", re_formula = NULL) draws the new group’s effects once per bootstrap replicate, so the interval carries the group variance the way the wald band does. It did not: every replicate predicted the new level’s zero modes, so the band was the POPULATION band under another name: on sleepstudy 1.00x the population band and 0.24x the wald band for the same stated quantity, and on an ordinal fit bit-identical to the population frame in estimate AND width, while the NA grouping column claimed otherwise. It matters most there: the ordinal per-category delta method is refused with re_formula and the message sends the user to band = "boot", so that was the only band available and it was the broken one. The bootstrap band is now 3.6 to 4.2 times the population band on sleepstudy and lands within 10% of the wald band at 100 refits. Blocks whose levels ARE the structure (gr_cov, gr_prec, car, spde) get zeroed rather than drawn, which is the same assumption the wald band makes for them.
  • effect2__ keeps one level per distinct moderator value. It took its levels from round(v, 2), so two values that rounded together became ONE level. plot() groups on that column, so it drew two curves as one series, and the names on an int_conditions entry were dropped with them. The rounding is the label only; labels widen past two decimals just far enough to stay distinct.
  • conditional_effects() accepts brms’s method = spellings ("posterior_epred", "posterior_predict") as aliases; "posterior_linpred" is refused by name, pointing at dpar =.
  • The expected-response display refuses an addition term whose value on a grid row would be a reference value rather than a real one (trunc(lb = v) with a variable bound), with the message method = "predict" has always used. It used to average the bound silently.
  • BEHAVIOR CHANGE. A mixture’s mixing weights have a response scale that is the softmax over the component predictors, so predict(type = "response", dpar = "theta1") returns brms’s posterior_epred(dpar = "theta1") exactly. It returned the linear predictor: not a probability, and on the tier’s own fit outside [0, 1] on 1.25% of the rows, reaching 1.112. The likelihood is untouched, the softmax having always been applied inside the density, and predict(type = "link", dpar = ) still returns the predictor. Its standard error under se.fit = TRUE is the delta method through that weight’s OWN predictor, p (1 - p), which is exact for two components and conservative for three or more, where the softmax also moves with the other components’ predictors: measured 5.5% to 26.1% wider than the joint delta method on a three-component fit, never narrower. ?mixture says so and a test pins it.
  • BEHAVIOR CHANGE, and what breaks is ranef(fit)[["Days | Subject"]], which now returns NULL: a NULL flows on into arithmetic as numeric(0) rather than erroring. Code that indexed a random-effect list by the BLOCK label needs the grouping factor instead (or [[1]], or the "term" attribute). ranef() keys its list by the GROUPING FACTOR now, as brms and lme4 do and as this package’s own coef() already did: ranef(fit)$Subject used to be NULL in a model where coef(fit)$Subject was a data frame. The block label rides along in each matrix’s "term" attribute, is what as.data.frame() puts in its grp column, and is still VarCorr()’s key, so two terms on one factor stay distinguishable. frmtmb.sample’s ranef() on a draws object follows core and is re-keyed with it; that package’s own NEWS says so.
  • BEHAVIOR CHANGE. predict(type = "response", dpar = "sigma") on a y | se(s) model with no sigma = TRUE reports 0, as brms does and as this package’s own sigma() already did. It reported the log link’s inverse of the mapped-out coefficient, 1, which reads as an estimate of a parameter the density does not have.

Behavior change

  • car(type = "esicar") is the exactly constrained intrinsic CAR brms fits, and no longer an alias for car(type = "icar"). brms declares Nloc - 1 free values and sets the last to minus their sum; frmtmb keeps Nloc coefficients and removes each connected component’s mean on the way to the linear predictor, which is the same model and keeps one coefficient per location for ranef(), predict() and simulate(). The normalizer is the pseudo-determinant, (Nloc - c) log tau + log|K|. A fit spelled esicar changes: refit it, or spell the term icar to keep the softly constrained model it used to select. The new fit sits ABOVE the old one, and not by much: on a 4 by 4 lattice the log-likelihood rises by 4.0e-4 and sd(car) by 3.1e-5 relative, the bias the soft constraint was always documented to carry. The constraint is what changes exactly, not the numbers: the field now sums to zero to machine precision instead of to about 1e-9.
  • con_sd does not change what an esicar term FITS. The constraint is exact, so logLik(), the estimates and VarCorr() are invariant to it. It is not inert everywhere, though: the coordinate it scales reaches every delta-method standard error (predict(se.fit = TRUE)) and every ranef(condVar = TRUE) conditional standard deviation, as con_sd^2 in the variance. At the 1e-3 default that is 1.3e-5 relative in a standard error, but it grows a hundredfold per decade, so con_sd = 0.1 inflates them by 7 to 13 percent. Leave con_sd alone on an esicar term. On icar and bym2 it means what it always did. (The standard-error half of this is no longer true; see the development version’s entry.)

Improvements

  • The brms log-density tier gains row 19c-esicar, an identity with brms’s own sparse_icar_lpdf program at the same parameter vector, with the CAR normalizer admitted as a closed form in the data (0.5 Nloc log(2 pi) - 0.5 log det K, residual 3.0e-13). The tier’s exemption list drops from four divergences over three rows to three, one per row; bym2 is what is left of row 19c.
  • esicar on a disconnected adjacency matrix constrains each connected component rather than the global sum alone, so the field stays proper where brms’s own normalizer does not apply. The two agree whenever the graph is connected.

A brms prior specification now means in frmtmb what it means in brms. Four behavior changes, each measured against the Stan program brms generates: on every shape whose optimum is a real mode, frmtmb’s penalized objective is now brms’s posterior density up to one log(2) per lower-bounded parameter, which is a constant and moves no mode.

  • BEHAVIOR CHANGE: frm(prior = ) applies what a brms prior row SAYS, keyed on the prior string rather than on the table’s source column. frm(prior = brms::get_prior(...)) used to apply NOTHING and say so in a message; it now fits a penalized model. The sharpest form of the old behavior was that a row the USER had edited into a get_prior() table in place, the ordinary gp$prior[i] <- "normal(0, 20)" workflow, was dropped too, because brms does not update source after such an edit, and the message said brms had filled the row in. What to change: a script that passed a get_prior() table for its slot listing and expected an unpenalized fit must now blank the prior column, or not pass the table. A table containing a class frmtmb keeps elsewhere (sds, sdgp, lscale, simo, brms’s mixture theta) now stops the call with a named refusal where the row used to be dropped in silence; blank that row, or write the frmtmb class the message names.
  • BEHAVIOR CHANGE: a brms prior on a distributional parameter’s own class (sigma, shape, phi, nu, kappa, sigma1, …) is translated instead of refused, and it lands on that parameter ITSELF through the dpar’s inverse link with that map’s log-Jacobian, which is where brms puts it. On a nonlinear fit this reproduces brms’s mode to nine figures where the row previously stopped the call. A bound on such a row travels with it, so brms’s lb = 0 on a log-linked dispersion becomes no constraint rather than a floor of 1. The brms classes ar, ma, cosy, cortime and rescor are carried over too; set_prior() already spelled them the same way. What to change: nothing in a frmtmb script. set_prior("...", class = "Intercept", dpar = "sigma") still means a density on LOG sigma and is deliberately left alone, so the two spellings now mean different things: write brms::prior(..., class = "sigma") for brms’s meaning. ?set_prior records the divergence and its measured size.
  • BEHAVIOR CHANGE: a class = "Intercept" prior is evaluated at the intercept at the MEAN of the predictors, which is the intercept brms constrains, rather than at the intercept at zero. On Reaction ~ Days, where mean(Days) is 4.5, brms’s own default moved the slope by 0.068 standard errors under the old placement and moves it by 0.00003 under this one. Every sub-formula with an intercept is centered separately, as in brms; nonlinear sub-formulas, a smooth’s unpenalized columns and mo() terms are not centered on either side. What to change: an existing class = "Intercept" prior on a model with uncentered predictors now constrains a different quantity and its fit moves. To keep the old meaning, name the intercept as a coefficient instead: class = "b", coef = "Intercept".
  • BEHAVIOR CHANGE: an ordinal family’s class = "Intercept" prior addresses its THRESHOLDS, as the same row does in brms, instead of failing with “Prior target not found”. The density sits on the threshold vector at the predictor means, with the log-Jacobian of the map from frmtmb’s internal tau_raw, and it reproduces brms’s density exactly. get_prior() lists the slot. What to change: a script relying on that error will now fit; prior = list(tau_raw = ) still reaches the same parameters on the internal scale.
  • frm_simulate(prior = ) follows the same placements. A draw from a distributional parameter’s own class is a draw of that parameter, and a draw from class = "Intercept" is a draw of the intercept at the predictor means; the pars attribute reports the value that was WRITTEN, so it still round-trips through newparams =, whose Intercept is the intercept at zero. On a model with uncentered predictors that column now holds a different number.
  • BEHAVIOR CHANGE: a translated class = "sd" or class = "cor" row carrying a coef is refused rather than applied to the whole block. brms narrows such a row to one coefficient and writes exponential_lpdf(sd_1[2] | 1), keeping its default on the rest; frmtmb resolves those classes per BLOCK and never read coef, so every one of coef = "Intercept", coef = "x" and no coef gave a bit-identical objective. Applying a narrowed row is a WIDER prior than the one asked for. What to change: drop the coef to prior the whole block (group still selects which one), or address the one parameter with class = "theta". coef on class "b", and group on "sd"/"cor", are unaffected: those do narrow, and are now pinned.
  • logistic(mu, s), gamma(shape, rate), inv_gamma(shape, scale) and beta(shape1, shape2) parse, with prior_logistic(), prior_gamma(), prior_inv_gamma() and prior_beta() exported beside the other prior constructors. Those are brms’s defaults on a mixture proportion, on shape/phi/nu/kappa, and on a zero-inflation zi or hurdle hu. Adding them takes every negbinomial, zero-inflated and hurdle model out of the set of tables that stop: of 20 shapes a brms user would actually write, 6 stopped and 3 do now, all three for a CLASS frmtmb keeps elsewhere (s(), gp(), mo()) rather than for want of a density.
  • Every refused row of a brms table is named in one message instead of only the first. A y ~ gp(x) table carries both an lscale row and an sdgp row and used to cost two edit rounds; it costs one now.
  • The refusal for brms’s mixture theta no longer names class = "Intercept", dpar = "theta2", a spelling that then failed with “Prior target not found”. sds, sdgp, lscale, sdcar and car are refused by name as well, each saying which frmtmb class reaches the same parameters, instead of being offered the generic dpar advice. brms’s shrinkage priors horseshoe(), R2D2() and lasso() now reach that same “unsupported distribution” message; R2D2() used to fall to a generic parse failure because its name is upper-case and its argument list empty.
  • tests/testthat/test-brms-priors.R grew from 61 assertions to 83 and every divergence it used to pin is now an identity.

Two exported seams an extension needed and could not get, a log survivor function so that right censoring is exact rather than floored, a penalized spline a nonlinear body consumes by VALUE, and two defects found on the way.

  • NEW frm_joint_cov() returns the joint covariance of everything the fit estimates: beta, betad, theta and the random-effect coefficients b together. vcov(full = TRUE) cannot return it, and is documented not to: its row names are exactly confint()’s, so b is absent under both of its branches. A penalized smooth’s wiggly part is a random-effect block even when the smooth is a population term, so a covariance that stops at the fixed effects covers none of it. The result is memoized on the fit and carries a labels element naming every row. It is also the only route to the covariance of an AUTOSCALED fit: a fresh RTMB::sdreport(getJointPrecision = TRUE) goes round the reparameterization and returns a covariance built on the unscaled Hessian.
  • NEW frm_lp_basis() returns the design of a linear predictor over the coefficient vector: eta, A (d eta / d coef), coef_pos, V, coef_names and extra_var. predict(se.fit = TRUE) builds all of that internally and keeps only the diagonal of A V A'; every delta-method quantity over a fitted curve - a contrast between two grids, a simultaneous band, a derivative, the time of a peak - needs the whole thing. predict(se.fit = TRUE) is now written as a two-line consumer of it, which is the test that the shape is right.
  • frm_lp_basis() serves the two cases nothing else could. For a NONLINEAR body A is a Jacobian, taped from the body against the coefficients it reaches through; predict(se.fit = TRUE) stays refused there and this is the route. For an rr() block at re.form = NULL the loadings live in theta, so a design over (beta, b) alone is incomplete; A carries the loading columns and coef_pos names their theta rows. An exact gp()’s kriging variance and a new grouping level’s marginal variance come back in extra_var, separately, because neither is coefficient uncertainty.
  • NEW frmtmb_family(lccdf = ), an optional LOG SURVIVOR function. Core formed a right-censored row’s contribution as log(1 - F), and a double cannot represent the complement of a probability that has rounded to one, so past that point the contribution was not merely inaccurate: it was CONSTANT, with a gradient of exactly zero. An optimizer prices such a row the same however far it moves. A family that declares lccdf is scored from it instead. Measured on a standard normal tail, log(1 - pnorm(z)) is -Inf from z = 8.3 and already 0.068 wrong at z = 8; pnorm(z, lower.tail = FALSE, log.p = TRUE) is exact to z = 500, value and derivative alike.
  • gaussian(), lognormal(), exponential(), weibull() and cox() declare lccdf. poisson() does not, because cens() is refused for discrete families; inverse.gaussian() does not, because RTMBdist’s upper tail is computed on the probability scale and reaches -Inf at the same log S = -34 that log(1 - F) does. lccdf fixes RIGHT censoring and leaves left censoring, interval censoring and truncation on the old footing, so a LEFT-TRUNCATED survival model meets the same problem from the other side.
  • A family that supplies only lccdf and no lcdf now accepts right censoring, and refuses left censoring, interval censoring and trunc() by name.
  • NEW ps(expr, k =, degree =, pad =, center =), a penalized coefficient block whose VALUE a nonlinear body consumes, admitted only inside bf(..., nl = TRUE) or nlf(). Every other penalized smooth in frmtmb ends up multiplied by Z, which is right for s(x) and useless for a body that needs the spline’s value inside an expression
    • exp(amp) * ps(age + shift) - because the argument is itself a function of parameters and there is no fixed Z to build. The second-difference penalty is eigensplit exactly as mgcv::smooth2random() splits s(): the null space joins the fixed coefficients and the range space becomes one random-effect block with a single variance in theta, so smoothness is estimated jointly with every other variance component.
  • The ps() basis is evaluated ON THE TAPE, as divided differences of truncated powers spelled branch free with 0.5 * (e + abs(e)). RTMB refuses pmax() on an advector and exports no CondExp, so the recursive Cox-de Boor form is unavailable and this is the only construction there is. It agrees with splines::splineDesign() to 3.1e-13 absolute at k = 12 and 1.8e-11 at k = 40, with an AD input giving the same values bit for bit and the taped derivative matching splineDesign(derivs = 1) to 4.5e-13. k above 50 is refused, because a divided difference cancels terms of order (range / spacing)^degree.
  • ps() refuses REML = TRUE, quadrature = TRUE, frmtmb_control(profile = TRUE) and multivariate models by name, and reports at fit end how many rows the fitted transformation pushed outside its frozen knot span, where the basis is exactly zero.
  • FIX a nonlinear body no longer loses a variable that appears only inside a call in FUNCTION POSITION. all.vars(quote(a * curry(tv)(zv))) is c("a", "zv"): it treats the whole function-position subtree as the callee and drops it, arguments included. Core collected a body’s data variables that way, so tv was never asked of data and the body failed on it with R’s own “object not found”, which names neither the argument nor the fault. bf(y ~ a * curry(tv)(zv), a ~ 1, nl = TRUE) now fits, to the same coefficient as bf(y ~ a * curry2(tv, zv), ...).
  • BEHAVIOR CHANGE, and a fix: a nonlinear parameter whose design has NO columns (b ~ 0 + (1 | g), a parameter that is purely a random effect) no longer adds a phantom entry to the parameter vector. paste() recycles to its longest argument, so a zero-column design came back from the coefficient-naming lines as the single name "b_". The parameter entered no likelihood and no linear predictor indexed it, and the outer Hessian was singular in exactly that direction, so vcov(), confint() and every standard error came back NaN with “the model is probably overparameterized”.
  • What changes for existing code: beta loses ONE entry per such parameter, so a script with a hand-written start = list(beta = ) for a model that has one now fails with start$beta must have length <n>. Drop the element that stood for the parameter with no fixed design; it was never estimated, and par_template(fit) names what is left, in order. Fits do not move: the phantom entered no likelihood, so the optimum, the log-likelihood and every coefficient are unchanged, and what was lost before and is recovered now is the standard errors. Two of this package’s own tests carried such a vector and were edited for it.
  • NEW a family may declare post$fit_check(fit, resp), run once when a fit finishes. logLik() reads the optimizer’s own value, so a family whose likelihood is floored in some region of the parameter space had nowhere to say so and no extension could gate logLik() or AIC(). This is the hook that closes it.
  • frm_compat() gains ps() as a special and frm_lp_basis as a method, with the rows for both.

frmtmb 0.51.0

One simplex per monotonic term, matching brms; importance sampling over several random-effect blocks; registration seams that refuse what they cannot describe; a simulator for every family but one, checked against its own density; and three brms comparison tiers that pin what still differs.

  • BEHAVIOR CHANGE, and the reason the 0.50.0 entry below withdrew its claim rather than its behavior: a mo() variable that appears in more than one term now gets one simplex per TERM instead of one per variable. y ~ mo(x) * z fits a shape for the main effect and a second for the interaction, so it gains the simplex’s free coordinates, D - 1 of them, for every term beyond the first. Such models change: the shared simplex was a constraint on the fitted surface, so removing it can only raise the log-likelihood, and the coefficients, standard errors, information criteria and predictions all move with it. A formula in which every mo() variable appears once is unaffected.
  • Where the interaction in such a model is near zero its simplex is barely identified, and maximum likelihood runs it to the boundary. The shape estimates stay finite, but their standard errors and intervals can come back enormous and carry no information: on the brms tier’s own y ~ mo(inc) * z data the second simplex reports standard errors in the thousands. Read that as the data not supporting a second shape, and write the additive model. There is no option to restore the sharing.
  • The v0.18 entry justified the sharing as the “brms convention”. It is not, the claim was withdrawn in 0.50.0 below, and the behavior now follows brms too. brms builds one simplex per special TERM and enumerates those terms in stats::terms() order, which lists every main effect before any interaction whatever the formula wrote. The frame re-sorts into that order before it numbers the simplexes, so the j-th monotonic term of a predictor is the one brms calls simo_<j> and names in the simo rows of its get_prior(). The monotonic design columns follow the same order, so y ~ mo(x) * z now reports mox ahead of mox:z. What lines up with brms is the ORDER, not the zeta<j> number: simplexes continue the numbering of any parameters the family declared first, so an ordinal or cox() fit spends slot 1 on its thresholds or baseline hazard and its first simplex is zeta2. That numbering is unchanged from earlier versions.
  • The brms log-density tier pinned the divergence by asserting the structural difference at y ~ mo(inc) * z. That row now asserts the identity, with the flat Dirichlet admitted once per simplex, and the tier’s exemption list no longer holds a mo() entry.

Two seams an extension had to work around: a compatibility rule that named a feature nobody had, and a family that needs either of two addition terms.

  • frmtmb_register_compat() now REFUSES a rule whose sides the registry cannot resolve, rather than accepting it and matching no pair. A rule that matches no pair is dropped without a word, so the contributing package’s table reads as complete while covering less than it claims, which is the worst thing a compatibility surface can do. It happened: a drift-diffusion extension wrote mixture() where the vocabulary says mixture, and named dec() before that term was a feature at all, and lost two wiener rows for a release. The refusal names the rule, the spelling, and the nearest entry in the vocabulary when one is close. Unknown kinds ("kind:fmaily"), unknown groups and statuses outside the declared five are refused the same way, as is a rule naming one feature on BOTH sides, which cannot match either: the resolved table holds unordered pairs of distinct features. Nothing is committed until every rule resolves, so a refused registration leaves the vocabulary as it found it.

  • frmtmb_register_compat(expects = ) is the one legitimate exception: feature names the rules refer to but the registering package does not supply, because another package does. They are exempt from the check and are NOT added to the vocabulary, so such a rule lies dormant until the package that owns the feature is loaded, which is what makes the rule true. frmtmb.sample needs it for its hmm x frm_sample and lca x frm_sample rules, which it only suggests the owner of; before this, those two rows were silently dropped in every session that did not also load frmtmb.latent.

    The argument declares an expectation, so it is held to one at both ends. A name the vocabulary already holds is refused, because it exempts nothing. A name still unresolved when frm_compat() is called is REPORTED there, in an unresolved attribute the print method names under the rows, rather than leaving the rule to match no pair in silence. It cannot honestly become a row: there is no such feature in the session, so there is no pair, and inventing one would put a feature in the table that nothing implements. The report is also what makes a misspelling inside expects findable, which registration cannot do, since the whole point of the argument is that the owner is absent.

  • frmtmb_register_aterm() now contributes the term to the compatibility vocabulary as well, as the feature "<name>()" of kind "aterm". A registered term that frm_compat() cannot describe is a gap by construction: frm() would accept a term the table refuses to answer about. A package that also declares the feature in frmtmb_register_compat(features = ) meets a no-op rather than a duplicate, and one display name registered under two kinds is an error. A name the vocabulary already holds under another kind is refused, and neither registry keeps anything: s() is a smooth, so frmtmb_register_aterm("s") would give one spelling two meanings in one formula, and thirteen names (s, t2, mo, ar, ma, mm, mmc and the rest) are taken this way.

  • frmtmb_family(required_aterms = ) now takes ALTERNATIVES as well as a conjunction. A character vector still names the terms a density needs all of. A list adds the choice: an element of length one is required, and an element of length more than one is a set of spellings, any one of which will do, so required_aterms = list(c("dec", "vint1"), "vreal1") reads as one of dec or vint1, and vreal1. The refusal names every alternative in an unmet group and writes the first into its example formula. A family whose per-row datum arrives under either of two terms had to hand-roll that refusal inside valid_y(), where it fires after the frame is built and where nothing about the framework can check it.

    Two smaller changes to the same argument. required_aterms now refuses a missing or empty entry, c("vint1", NA), which used to be accepted and could then name no term; and list() is now accepted as no requirements at all, which the character-vector-only check used to refuse. Neither should reach a family that declares its requirements correctly today.

  • The brms log-density identity tier covers eight more shapes of the plan’s matrix: multivariate responses with set_rescor(TRUE) and with (1 | ID | g) merged across responses (rows 8 and 9), mi() imputation together with mi(sd) measurement error (row 4), a nonlinear model with a random effect on an nlpar (row 6), s() and t2() (row 11), the Hilbert-space gp(k =) (row 10), ar(cov = TRUE), cosy() and unstr() residual correlation (row 18), and car() in its escar and icar forms (row 19). Four divergences over three rows are recorded and asserted rather than skipped: the exact gp() nugget, brms’s ar(cov = FALSE) likelihood, and the esicar and bym2 CAR parameterizations. See dev/brms-likelihood-tests.md.

  • tests/testthat/test-brms-methods.R is a new opt-in tier that compares what the post-fit methods RETURN against brms, rather than what the objective evaluates to. It builds a brmsfit whose draws ARE frmtmb’s estimates, by feeding the log-density tier’s translated parameter list to Stan’s Fixed_param algorithm, so brms’s posterior mean is a point estimate and the comparison is exact rather than toleranced. posterior_epred(), posterior_linpred(), log_lik() per row, fixef(), ranef(), VarCorr(), hypothesis() point estimates and the one-way conditional_effects() grid and estimate all agree to the last bit across the shapes the translator handles. Where they do not, the difference is recorded and asserted in dev/brms-methods-tests.md rather than tolerated, each with a verdict: 10 are frmtmb defects, 8 are paradigm differences that are right for a maximum-likelihood fit, and 8 are open design choices. The sharpest is conditional_effects() on a zero-inflated fit, which plots the conditional mean exp(eta) instead of the expected response (1 - zi) * exp(eta). On the tier’s own data the plotted curve is 15% above the expected response at the first grid point and 268% above it at the last. The same defect is live on the hurdle families, where the error changes sign along the curve and only method = "predict", which draws responses, reaches the expected response, and neither case needs brms to see: fitted() and predict(type = "response") on the same fit already disagree with the plot.

The simulators are now tested against the densities they are supposed to match.

  • New test tier, tests/testthat/test-simulate-density.R. For every family that declares a simulator it draws through the family’s sim slot on a fixed two-cell design and tests the draws against that same family’s own lpdf, evaluated numerically: a chi-square goodness of fit over cells whose probabilities are summed or integrated from the density, and the first two moments taken from the same measure. This is the one place in the suite that deliberately closes the loop. Everywhere else a validation reference is written by hand precisely so that it cannot share a convention error with the code under test; the cost of that rule was that frm_simulate() had never been checked as a generative process, and a shape/scale swap or a zero-inflation gate read the wrong way round would have cancelled between the simulator and the density without anything noticing. The tier covers the addition terms that change a draw (trials(), trunc(), se()) and the ones that must not (weights(), cens()), the ordinal, categorical, multinomial and mixture families, both entry points, and the refusal a family without a simulator has to raise.

  • tweedie(), compois() and hurdle_poisson() have simulators, so simulate() and frm_simulate() work for them, and so does posterior_predict() through frmtmb.sample. Each states a generative process rather than inverting the density: the Tweedie is drawn as the compound Poisson sum of gamma jumps it is on 1 < p < 2, which is also where its point mass at zero comes from; the hurdle Poisson clears its hurdle and then draws a zero-truncated Poisson by inverse transform above the Poisson’s own zero, rather than by a rejection loop that would spin at the small mean these models are used at; and the COM-Poisson solves for the rate that puts the distribution’s mean where it was asked for, then inverts its own defining weights.

  • Only cox() now declares no simulator, and it says why: inverting a cumulative baseline hazard identified only on the observed time window has no answer beyond the last event. The tier reads the list of families with no simulator off the registry rather than carrying it, skipping the deferred entries whose simulator arrives with the data, so a family that gains or loses one changes the test’s answer instead of drifting away from it.

  • Bug fix: frm_simulate() refused a family whose simulator is installed by its family_finalize() slot, reporting that the family had no simulator, while simulate() on a fit of the same model worked. A family that derives itself from the response is not itself until frame assembly has run, and frm_simulate() was reading the family as written; it now carries the finalized responses over from the frame, as frm() already did. This is what made frmtmb.eam::gddm() unreachable from frm_simulate().

  • frm(importance = ) now corrects a model with SEVERAL random-effect blocks over one grouping factor, which is what distributional regression produces: (1 | g) in mu and (1 | g) in sigma, (1 | g) + (0 + x | g), or a block in mu and one in a distributional parameter. A grouping level’s coefficients from every block are drawn together from one joint proposal, so the blocks stay coupled through the group’s rows the way the likelihood couples them. Blocks over different grouping factors are still refused, and the message names the blocks and the factors: crossed or nested factors make the marginal likelihood one integral over every factor at once, which does not split into the per-group integrals the correction resamples.

  • Validated against a brute-force reference that shares no code with the package. On 12 groups of 10 rows with (1 | g) in mu and (1 | g) in sigma, each group’s marginal likelihood was integrated by two-dimensional Gauss-Hermite quadrature over (b_mu, b_sigma) against dnorm() directly. The reference is 193.161829; the correction at 2000 draws gives 193.137101, which is 0.61 of its own Monte Carlo standard error of 0.0404, while the Laplace approximation gives 193.396458, or 5.8 of that error. The same comparison under a likelihood no identity fixes, 30 groups of 8 Bernoulli rows over (1 | g) + (0 + x | g) against a two-dimensional quadrature on dbinom() directly, puts the correction 0.26 of its Monte Carlo standard error from the reference at 8000 draws, where the Laplace approximation is 56 of them away.

frmtmb 0.50.0

The Laplace approximation, corrected by importance sampling; and get_prior() answers for one route, whatever is attached.

  • frm(importance = ) replaces the Laplace approximation with an importance-sampling correction of it (Skaug and Fournier 2006, the ADMB -is option). Each group’s marginal likelihood is re-estimated by drawing from the Laplace Gaussian and reweighting the draws, which is unbiased for the exact integral at every parameter value rather than accurate to O(n^-1) near the optimum. A positive integer is the number of draws per group; 0, the default, is off.

  • This is the correction for the case quadrature = TRUE cannot reach. TMBad’s marginal_gk transform is one-dimensional in its C++, so a block of dimension two returns NaN and the dim in the spec is never read. The importance correction takes any block dimension. On the package’s probe design, 60 groups of 8 Bernoulli rows with a correlated random intercept and slope, the Laplace negative log-likelihood of 299.302 and intercept of -0.691 become 297.915 and -0.707 at 2000 draws, against GLMMadaptive’s adaptive Gauss-Hermite values of 297.919 and -0.705. The gap is 0.12 of the fit’s own Monte Carlo standard error, and 0.015 of the intercept’s standard error.

  • trunc() and cens() are corrected rather than refused. This is a real difference from quadrature, which must refuse trunc() because its far nodes drive the truncation normalizer to underflow; importance draws stay within a few conditional standard deviations of the mode. Both were measured, not assumed.

  • The fit reports how far it can be trusted. fit$importance records the draw count, the seed, the rounds taken, the Monte Carlo standard error of the corrected log-likelihood, and the effective sample size of every group; print() and summary() give the summary line. Below an effective sample size of 0.25 of the draw count the fit warns and names the groups. That threshold is placed by measurement, and against the anchor the fit reports from: ordinary designs report 0.93 to 1.00, a proposal displaced by half a log standard deviation falls to 0.009, and a genuinely hard design (forty groups of three binary rows) lands anywhere from 0.08 to 0.62, where firing is the point.

  • The corrected log-likelihood is reported from a proposal frozen at the estimate, not at the round’s starting point. The optimizer minimizes the exact objective plus a Monte Carlo error that is exactly zero at the anchor and grows away from it, so the value it stops at is optimistic by construction and by an amount no draw count removes.

  • A fit whose iteration diverges refuses rather than reporting a collapsed variance component. An unidentified covariance turns the fixed-point iteration into a wander along a ridge, ending where every importance weight is trivially equal and the effective sample sizes therefore look perfect. The invariant that catches it is that the correction must not make the fit worse than the correction at the Laplace estimates.

  • logLik(), AIC(), vcov() and confint() come from the corrected objective through the ordinary sdreport() path; ranef(), fitted() and predict() work as usual, because the conditional modes come from the Laplace inner solve at the corrected optimum, as they already did under quadrature = TRUE.

  • confint(method = "profile") on a corrected fit profiles the frozen proposal rather than refitting, which is deliberate: refreezing at every grid point would make the profiled objective a different random function at each one and destroy the smoothness the profile’s own interpolation depends on, and it was measured at three times the cost besides. What it does now is check the weights at the bound it returns and warn, naming the parameter and the effective sample size, when the bound lands where the proposal no longer covers the integrand. A covariance parameter can walk far enough to get there; a fixed effect generally does not.

  • Each round now starts its optimizer at the current estimate instead of at the cold start template. The reported numbers are unchanged (the probe gives the same negative log-likelihood and the same coefficients to four decimals) and the fit is about 15 percent faster, but the rounds no longer wander: one unidentified design that used to trip the divergence guard now converges to within 0.013 of its adaptive Gauss-Hermite reference.

  • frmtmb_control() gains importance_seed, importance_rounds and importance_ess. The draws come from a private random stream, so a fit neither reads nor disturbs the session’s random state, and the same seed gives the same answer to the last bit.

  • Everything outside the correction’s scope is refused by name at the earliest point, with the remedy in the message: more than one random-effect block, a block with no grouping factor, a covariance that correlates the grouping levels, a multi-membership term, a second response, rescor, a residual correlation term, a family that supplies its own log-likelihood, a nonlinear predictor, cs(), mi(), REML = TRUE, profile = TRUE and quadrature = TRUE. frm_compat("importance") lists the rules.

  • vignette("diagnostics") gains a section on when the Laplace approximation is the problem, what the correction costs, and how to read the effective sample size.

  • Behavior change. get_prior() gains route, and its prior column no longer depends on which packages are attached. Until now the column was filled from the prior-defaults registry on every call, so the same call on the same fit reported (flat) in every slot with frmtmb alone and brms’s weakly-informative defaults once frmtmb.sample was loaded. route = "fit", the default, reports the defaults frm() applies, which are flat, and reads no registry: its table is identical whatever else is loaded. route = "sample" reports the defaults frm_sample() applies, and refuses, naming frmtmb.sample, when no loaded package states them. Readers who want the sampling defaults, which is what brms’s get_prior() describes, must now ask for that route. frm_sample()’s own precedence of call over fit over defaults is unchanged; only where get_prior() reads from has moved. The returned table gains class frmtmb_prior_rows and a route attribute, and print() names the route on its first line so that a copied table stays unambiguous.

  • A new opt-in test tier checks that frmtmb’s objective is the same function of the parameters as the Stan program brms generates from the same formula with flat priors. At frmtmb’s estimates the two log densities agree to 1e-6 of the log-likelihood, and brms’s gradient there is zero, which is what catches a parameter-map error rather than an arithmetic one. Random-effect models are compared on the joint density at the conditional modes, since brms has no marginal likelihood. The comparison against brms posterior means is gone: the two estimands differ, so its tolerance was wide enough to hide a real divergence. tests/testthat/test-brms-likelihood.R, gated by FRMTMB_BRMS_FIT_TESTS=true and run weekly in its own workflow.

  • That tier found one shape the two packages do not spell the same way, and the reason recorded for it in the v0.18 entry below is wrong. mo() interactions share their variable’s simplex, which that entry justifies as the “brms convention”. It is not the brms convention: brms builds one simplex per special term, so under y ~ mo(inc) * z brms gives the interaction its own monotonic shape while frmtmb reuses the main effect’s, and frmtmb’s model has two fewer free parameters. Only that claim is withdrawn here. The behavior itself is unchanged and stays the maintainer’s call.

frmtmb 0.49.1

Internal hygiene and the documentation site. No user-facing behavior changes.

  • Every $ read of a parameter template, an estimate list, an aterm container, a random-effect, linear-predictor, autocor or eta-design block, a family object, a simulator context, a distributional- parameter list or the model frame is now a [[ read, in core and in all four extensions. $ on a list falls back to partial matching, so an absent slot returns a neighbor rather than NULL; that is what once made pars$b read beta, ctx$mix read mix_g, and par_template$theta read thetaac. Comparing the two trees with every $ normalized to [[ shows no collateral edit in any of the swept files. dev/bracket-sweep.md holds the inventory, separating the containers whose collision was already reachable from those that were only prone.
  • VarCorr() no longer reads the residual-autocorrelation parameters where the random-effect covariance parameters belong, on a model with an autocorrelation structure and no random effects. The value was discarded before it reached any output, so nothing printed changes.
  • A source-scanning test keeps those containers clean. It is keyed on the container’s name rather than on $, so the user-facing spellings (fit$obj, x$fit, object$draws) are untouched, and it polices core and all four extensions from one list.
  • The reference sites for frmtmb and its four extensions are now one site: each extension builds into a subdirectory of docs/, and a reference to a function or a vignette in another of the five packages becomes a link. Build them with dev/build-docs.R.
  • Three published reference pages have been broken since the sampling surface moved out at 0.47.0. Redirect stubs left by that split shadowed real topics, and loo.html redirected to its own URL; pkgdown will not overwrite a file it did not write, so the real pages were unreachable. All three are regenerated. An orphaned copy of the ODE article, which belongs to the extension’s site, is removed from the core site.

frmtmb 0.49.0

One way to bound a thing, a hardened family API, vignettes divided along the package split, and a shipping case study.

Bounds and the last prior classes

  • Hard bounds now speak the prior vocabulary and nothing else. A bound addressed by nlpar, dpar or resp was previously refused (Unknown parameter(s) in bounds: (Intercept)) because it was keyed by the design-matrix column name of the parameter’s own sub-formula; bounds are now keyed by the parameter-template position, so a bound lands where the distribution beside it lands.
  • set_prior() gains brms’s class names for the residual-correlation surfaces: "ar", "ma" and "cosy" carry a density on the natural coefficient with the transform’s Jacobian (the composite Levinson-Durbin determinant derived in closed form and pinned against numeric differentiation in the suite), "cortime" takes lkj() on an unstr() time correlation, and "rescor" takes lkj() on a multivariate model’s residual correlation. class = "theta" spans all three covariance components by internal name. A first-order ar, ma or cosy bound maps exactly; at order two and above lb/ub are refused with the reason and the class = "theta" alternative.
  • frm(lower =, upper =) and frm_sample(lower =, upper =) are removed, not aliased: every outer parameter they could reach has a set_prior() class. A stale call fails as an unused argument, and frm_sample() refuses the names explicitly because its ... would otherwise have passed them to the sampler and sampled unbounded in silence.
  • get_prior() no longer offers a random-effect theta row for a model with a residual autocorrelation structure and no random effects (a $ partial match of exactly the kind the codebase polices, now bracketed with a regression test).

The family API, hardened

  • frmtmb_family() gains required_aterms (an absent required addition term is refused at assembly instead of reaching the density as NULL and returning a fit with a log-likelihood of zero), and the objective refuses any log-density that returns nothing for a non-empty response. family_finalize(fam, y, aterms) lets a family derive link bounds from the data, and the slot call order it relies on is documented from measurement.
  • New frmtmb_register_aterm(): a package adds an addition term of its own, with an arity and a coercion, instead of routing per-row data through vint(); the unknown-term refusal names both routes.
  • Custom link objects are validated at family construction; a non-finite init_dpars value through its link warns instead of dropping silently; anova() warns when two models’ fixed effects are non-nested by name and by column space; the compatibility registration seam has its own documented page.

Vignettes and the case study

  • The posterior diagnostics and the posterior half of the brms migration live in frmtmb.sample as documents of their own (vignette("posterior-diagnostics", package = "frmtmb.sample"), vignette("brms-posterior", package = "frmtmb.sample")); core keeps the maximum-likelihood halves and points across. The case-studies wiener section is replaced by a shifted-lognormal custom family cross-checked against the built-in to machine precision, with the drift-diffusion model deferred to frmtmb.eam.
  • New habit_prep data and vignette("habit"): a case study replicating the response-preparation model of Hardwick, Forrence, Krakauer and Haith (2019, Nature Human Behaviour) with nlf() on a categorical() family, the full per-subject sweep run live, and a hierarchical (1 | p | participant) fit replacing 58 separate ones. Data under the MIT license of the authors’ OSF deposit; see inst/COPYRIGHTS.

frmtmb 0.48.0

The structured-family protocol completes, nonlinear bodies get an AD-safe scope, and the extension API passes its first outsider test.

  • A nonlinear formula body is now a scope of its own: a function it CALLS resolves in RTMB before the formula environment and the search path, so a bare pnorm(), qgamma(), plogis(), besselK() or matrix() in an nl = TRUE body or an nlf() is the tape-capable version, with no RTMB:: prefix. The prefixed spelling gives the identical fit; stats::pnorm() is the escape hatch; RTMB wins over a same-named function of your own; and only names a body calls are shadowed, so a data frame named df still reads as your object. The shadowed names are the RTMB exports that collide with base or stats and are numerically identical to them (qchisq is excluded because RTMB reimplements it), which keeps the taped and numeric evaluations of one body the same function. One narrow caveat: splinefun() in a body now supports only the fmm, periodic and natural methods; spell stats::splinefun() for monoH.FC or hyman. Extension-author surfaces (family log-densities, structure log-likelihoods) keep explicit qualification.

  • New companion package frmtmb.eam: the Wiener drift-diffusion family with brms’s parameterization, agreeing with RWiener to better than 1e-12 through a tape-safe two-series blend. It was built against the exported extension API alone, with no core change and no reach into internals, which makes it the API’s first acceptance test from an outsider’s position; its findings ledger drives the next round of interface hardening.

  • hmm() and lca() moved to the companion package frmtmb.latent, with hmm_probs(), hmm_viterbi(), lca_probs(), lca_profiles(), their tests and the HMM case study. Install it with remotes::install_github("aforren1/frmtmb", subdir = "extensions/frmtmb.latent"). Nothing about either family changed in the move.

  • mixture(), mixture_mvn() and mixture_probs() stay in core, and mixture_probs() still reports the class probabilities of an lca() fit. The two meet through the family object’s component interface rather than through a class, so neither package has to know about the other.

  • ?"frmtmb-extension-api" gains the four accessors an out-of-tree structured family needs and could not reach: frame_block_of() (the read half of a structure’s frame_block slot), structure_supports_all() (the starting point for a family whose likelihood IS rowwise and which declares a structure only to carry its refusals), mixture_posterior() (one posterior implementation for every family that implements the component interface) and mixture_multimodal_refusals() (the REML and profiling refusals a component-permutation-invariant family owes, issued from one place so that core and an extension cannot word the same fact differently).

  • The core boundary test is no longer a ratchet. Every policed token now maps to an empty list of allowed homes, so no core file may name a structured family or an ODE symbol anywhere at all. Its positive control moved to the extension’s sources, and it is the control alone that skips when extensions/ is not in the tree.

  • Suggests: depmixS4, hmmTMB and poLCA dropped. They were the reference implementations those two families were checked against, and they travel with them.

frmtmb 0.47.0

The monorepo split: the ODE seam and the sampling surface leave core for companion packages in this repository.

  • The sampling surface moved to the companion package frmtmb.sample: frm_sample(), as_tmbstan(), check_laplace(), and every frmtmb_draws method, including log_lik(), loo(), posterior_epred() and conditional_effects() on draws. Install it with remotes::install_github("aforren1/frmtmb", subdir = "extensions/frmtmb.sample"). Absent it, frm_sample() is not found, and the fit-side methods that need draws name the package and the call in their refusal. Core keeps every generic it still defines a method for, so one loo() serves both packages in a session.
  • Ordinary differential equation dynamics moved to the companion package frmtmb.ode (frm_ode(), its tests, its vignette). With it go the RTMBode suggestion and the Additional_repositories line: core’s dependencies now resolve entirely from CRAN.
  • New public interface ?"frmtmb-sampling-api": the internals a sampling extension is written against, as one documented contract. New extension seams: frmtmb_register_frame_check() (a frame check an extension registers at load), frmtmb_register_compat() (compatibility-matrix rows), a prior-defaults registry behind get_prior(), and frmtmb_ad_overload() (the tape-safe wrapper for user functions, previously internal).
  • get_prior() reports the default a slot actually has: (flat) with frmtmb alone, which is what frm() does, and the sampling defaults once frmtmb.sample registers them.
  • ?frm gains “The Laplace approximation, and how to check it”, naming the in-package remedies and pointing at frmtmb.sample::check_laplace() for the direct measurement.
  • Core’s boundary test now asserts zero: no core file names a structured family or an ODE symbol outside the two family homes that remain until they move out in turn.
  • Suggests: coda and bridgesampling dropped; no core code registers on them any more.

frmtmb 0.46.0

The structured-family protocol completes in-package: hmm and lca ride fam$structure alone, and the extension API is exported.

  • New exported latent_probs() returns the posterior latent-state probabilities of any family that declares them. mixture_probs(), hmm_probs() and lca_probs() are unchanged and now reach it; hmm_viterbi() stays its own decoding pass.
  • New extension API on one page, ?"frmtmb-extension-api": single_response(), eval_dpars(), fit_extras(), dpar_linpred(), response_mean() and as_frmtmb_family() are the read-only accessors a frmtmb_structure() slot may use, with a stability promise. uni_resp() was internal and is now single_response().
  • frmtmb_structure() gains check_fit, unit and a cluster_robust capability flag, and loglik becomes optional. A structure with no loglik is a capability declaration: the family keeps its rowwise likelihood and carries only the refusals that belong to it, and its capability flags default to TRUE so it names what it refuses instead of enumerating what already works.
  • No core file branches on hmm() or lca() any more. fit$frame$hmm_g is gone; a hidden-Markov structure’s data is fit$frame$blocks[[response]]. The compatibility matrix takes both families’ rows from the families themselves through a registration the boundary test polices.
  • One wording change: each mixture-type family now states the REML and profile = TRUE refusals in its own name. The message previously listed mixture(), mixture_mvn() and lca() whichever one was fitted; everything after the colon is unchanged.
  • Fits are bit-identical to 0.45.0, and frm_compat() resolves every feature pair to the same status.

frmtmb 0.45.0

The structured-family protocol: the first step of the core and extensions split.

  • New exported frmtmb_structure() declares a likelihood that does not factorize over the rows of the data. It carries the non-rowwise log-likelihood, the frame block that likelihood reads, the frame variables and NA policy it needs, a conditional mean and variance, a structured simulator, and twelve capability flags, each with the message its refusal shows. frmtmb_family() gains structure =. A structured family can now be written outside the package; dev/structured-family-protocol.md is the contract.
  • mixture(groups = ) and hmm() reach the core through that one slot. The objective’s response loop has one branch for both instead of one each, the model frame carries one blocks slot, and predict(), fitted(), residuals(), simulate(), conditional_effects() and the fit-time REML, quadrature and profile gates read capability flags instead of naming a family. Fits are bit-identical to the previous branches, and no user-facing message changed; the only deleted refusals were unreachable behind generic guards, which an existing test asserts.
  • fit$frame$mix_g is gone. A group-level mixture’s grouping is fit$frame$blocks[[response]], and a structured simulator reads ctx[["block"]] where it read ctx[["mix_g"]].
  • Internal reorganization toward the split: the prior and bound machinery the fit route reaches moved from R/interop.R to R/priors.R, and a new boundary test pins where core still names the structured families and the ODE seam, as a one-way ratchet the remaining protocol steps shrink to empty. No user-visible change.

frmtmb 0.44.0

Sampling a fit gets the default priors, conditional-effects plots get real facets, and parameter names become discoverable.

Default priors on the fit route

  • frm_sample(fit) now applies the same brms 2.23 default priors the formula route applies. Both routes sample a posterior, and a prior-free brm() call translated onto a fitted model no longer pins the chain at whatever boundary mode maximum likelihood found. On the kidney model of dev/brms-vignette-audit.md, sd(patient) goes from 3.1e-4 with a 3e-6-wide interval to 0.49 with a [0.04, 1.14] interval, and the largest R-hat from 1.57 to 1.10. This is a behavior change for existing scripts that sampled a fit: prior = "flat" restores the bare likelihood.
  • Priors on a sampling call now stack, most explicit winning per slot: prior = on the call, then the fit’s own prior, then the defaults. Previously an explicit prior = on a MAP fit discarded that fit’s prior entirely.
  • Because the defaults cover every standard deviation and correlation, the non-centered parameterization is now offered on the fit route as well; reparameterize = FALSE still asks for the centered chain.
  • prior = "flat" is the opt-out on both routes, and frm_sample(fit, prior = "flat") is no longer an error. On a MAP fit it also removes that fit’s own penalty, which is taped into its objective, and the call names the prior it dropped. A MAP penalty can never be applied twice: every prior-carrying path rebuilds the objective from the bare likelihood and adds the merged priors once, and a regression asserts it in objective values.
  • check_laplace() is unchanged in what it measures: it samples the density the fit maximized, with no default priors, centered. It now asks for that explicitly rather than inheriting frm_sample()’s default.

Faceted conditional-effects plots

  • plot() on a conditional_effects() result with several condition sets draws one faceted page of small multiples with a shared scale, as brms’s facet_wrap("cond__") does, instead of one full page per condition that overwrote its predecessor on a normal device. The panels use tinyplot when it is installed; without it the fallback is a grid of base-graphics panels, still one page. ncol = is honored; it was previously absorbed by ... and dropped.
  • plot(ce, points = TRUE) shows in each panel only the observations belonging to that condition, following brms’s make_point_frame(): a factor condition splits the data per panel, while a numeric condition is a reference value that names no observation and keeps every point. One deliberate divergence: brms pins factors the conditions frame does not mention to their first level, silently discarding the other levels’ observations; frmtmb subsets only on the variables the user passed.

Parameter names, discoverable and usable

  • New par_template(): the parameter vector’s names and starting values, for a fitted model or for a formula and data alone. It prints as a compact component listing, and the object it returns is edited and handed straight back as frm(start =) or frm_simulate(newparams =). brms has no counterpart: brm(init =) takes Stan program names and brms initializes at random instead.
  • frm(start =) matches by name. A named vector inside a component overrides those entries and leaves the rest at their defaults, with parentheses optional as everywhere a parameter is named. An unnamed vector keeps the positional contract exactly. Mixing the two in one vector, an unknown name, a name given twice, and a non-numeric vector are each refused by name. Note the edge: a full-length named vector previously had its names silently ignored; names are now interpreted, and a vector whose names match nothing is refused with a pointer at unname().
  • A normal(), student_t() or cauchy() prior on a nonlinear parameter now places its starting value at the prior location, which is how brms uses the same information. The insurance-loss growth curve of the brms nonlinear vignette fits from its priors with no start at all. A message names what was placed; start still wins. Other parameters keep the starts they had.
  • Fixed on the way: autoscale = TRUE silently recycled a too-short start vector into the pre-fit’s full-length start. It now goes through the same validation every other start does, and refuses.

frmtmb 0.43.0

The prior interface speaks brms, ar1() evaluates in linear time, the brms vignettes are measured call by call, and parallel chains work on Windows.

The prior interface

  • frm(), frm_sample() and frm_simulate() take prior =, brms’s spelling. The priors = of earlier releases is renamed, not aliased: no ecosystem spells it priors, and the package is pre-release. A stale frm(priors =) fails as an unused argument; frm_sample(), whose ... would otherwise pass the name to tmbstan and fit with no priors at all, refuses it by name. The fit object’s field is fit$prior, as brmsfit$prior is.
  • New prior(), prior_() and prior_string(), brms’s constructors. prior(normal(5000, 1000), nlpar = "ult") quotes its first argument and reaches the same machinery set_prior() does. tag and check are deliberately absent: both are Stan-program concerns.
  • A brmsprior object built by brms itself is translated row by row wherever a prior is accepted, so a specification copied out of a brms script works even when brms is attached and its prior() masks frmtmb’s. Rows with no faithful frmtmb meaning are refused by name rather than turned into a different density: a tag, brms’s mixture theta (frmtmb’s theta is the raw covariance vector), and the per-dpar classes such as sigma, whose density brms puts on the parameter and frmtmb’s nearest spelling puts on the link scale. brms’s own default rows are dropped with a message, because frmtmb chooses its own defaults.
  • set_prior() gains nlpar and resp, and its argument order now follows brms’s (prior, class, coef, group, resp, dpar, nlpar, lb, ub). Under nlpar, class "b" covers a nonlinear parameter’s whole coefficient vector, intercept included, because its sub-formula is not centered; that is what makes the brms nonlinear vignette’s insurance-loss priors land. nlpar also narrows classes "sd" and "cor", separating two blocks on one grouping factor that group alone cannot tell apart. get_prior() gains an nlpar column.
  • The formula route of frm_sample() no longer invents a location default for a nonlinear parameter’s intercept. brms leaves those flat, and the response’s median and mad describe nothing about a rate or a shape inside a nonlinear body; the disclosure message names the parameters left flat.

Linear-time ar1()

  • ar1() and hetar1() evaluate their density at linear cost in the block dimension instead of building a dense d x d covariance and factorizing it on the tape. The density, and therefore every fitted value, standard error and prior, is unchanged: the new form agrees with the old computation to 2.3e-12 in value and 2.2e-9 in gradient, gated by a test that keeps the old dense code inline. The speedup is orders of magnitude once blocks are large (milliseconds where the dense form took seconds at d = 800, and tens of seconds at d = 2000); dev/sparsear1-bench.R measures it on the host at hand.
  • A long time series is now an ordinary ar1() block. The multivariate stochastic-volatility model of Skaug and Yu (2014) fits at its published 945 time points in seconds, agreeing with the upstream TMB reference to 8.5e-9, with a slightly better optimum than the reference’s own cold start reaches.

The brms vignettes, measured

  • dev/brms-vignettes/ translates the brms vignettes call by call, each model down both paths: the maximum-likelihood fit with its post-processing surface, and frm_sample() with the posterior workflow. Every one of 567 calls carries a label (clean, spelling, behavior, missing, or refusal), and dev/brms-vignette-audit.md holds the measured scoreboard: about 7 of 10 model calls port unchanged, about 4 of 10 of all calls. README’s port claim now states both figures.
  • The audit’s cheapest finding is applied: the draws and fit surfaces refuse with named replacements where they previously failed bare. loo(), waic(), bayes_R2(), LOO() and WAIC() on a maximum-likelihood fit now say to sample first or use AIC(); loo(a, b) says comparison belongs to loo_compare(); plot() on draws names mcmc_plot(); update() on draws says to update the fit and re-sample; rescor_matrix() on draws names the fit; expose_functions() on a fit gets the same refusal draws already had.
  • conditional_effects(method = "predict") now works on a nonlinear predictor. The guard that refused it asked for a standard error that method = "predict" never uses: its band is a quantile of simulated responses. This is what the brms nonlinear vignette’s facet-per-year display calls; the same call is also about a third faster on every model, because the dead standard-error computation is gone.

Parallel chains on Windows

  • frm_sample(cores = ) parallelizes chains on Windows instead of falling back to sequential sampling. The old guard claimed socket workers cannot evaluate the RTMB tape; in fact tmbstan rebuilds the tape on each worker from the serialized objective closure, and frmtmb’s generated closures are self-contained, so the rebuild works and the draws are identical to a sequential run at the same seed. The remaining cost is a fixed several seconds of startup per worker, which a message now states; short chains gain nothing, long chains approach a per-chain speedup. One caveat: a devtools::load_all() development namespace cannot be rebuilt on a worker, so parallel chains need the installed package.

frmtmb 0.42.0

The TMB-examples replication audit, and REML’s semantics documented and validated beyond the gaussian case.

The tmb_examples audit

  • Audited all 35 models in the upstream tmb_examples directories of RTMB and adcomp: 13 replicate through frm() at 1e-8 or better, including the SPDE Weibull survival model with censoring, orange_big’s nonlinear growth with 5000 latent effects, socatt’s ordinal GLMM, lr_test’s map = restrictions written as plain formulas, and a million-row regression; 3 are expressible but pinned or cost-capped; 19 name the capability that blocks them. dev/tmb-examples-audit.md carries the table, and every replicating example is a self-contained regression in test-tmb-examples.R with its reference likelihood written inline.
  • The Kalman-filter node the roadmap held open is closed by measurement rather than opinion: the filter and the Laplace route agree to 1e-13 on the multivariate random walk, both are linear in series length, and MakeTape()$atomic() collapses the filter to nine tape nodes with no adjoint to derive, so the node is free if ever wanted. What blocks state-space models is the formula grammar. The ranked roadmap that falls out puts a sparse ar1() covariance first: the dense block is cubic in its dimension, and it is the one thing keeping the multivariate stochastic-volatility example from fitting at its published size.
  • Two RTMB traps recorded for nonlinear formula authors: a bare pnorm() or qgamma() in an nl body resolves lexically to stats (qualify with RTMB::), and Vectorize() strips the automatic-differentiation class from its arguments.

REML beyond gaussian

  • The REML documentation now states what the flag computes: exact Patterson-Thompson REML for gaussian models, covariate-dependent sigma included, and for every other family the Laplace-approximated integrated likelihood under a flat prior on the mean coefficients, first-order equivalent to the Cox-Reid adjusted profile likelihood. Distributional-parameter coefficients are deliberately not integrated, matching nlme’s varFunc estimation and the double-GLM literature.
  • Validated three ways in test-reml-nongaussian.R: glmmTMB agreement for poisson and bernoulli GLMMs at 1e-5, the Cox-Reid identity verified by hand at the pinned REML optimum, and gls(method = "REML") exactness at 1e-5 for varIdent- and varExp-shaped sigma models. The anova() design rule is asserted for non-gaussian REML fits too.

frmtmb 0.41.0

The tmbstan wrong-density defense, both argument dialects on the draws surface, and the pkgcheck container as a local pre-push gate.

The tmbstan defense

  • frm_sample() and as_tmbstan() refuse, before any sampling, a tmbstan installation that silently samples the wrong density. tmbstan’s install-time generator patches only the first std_normal_lpdf placeholder in the stanc output; stanc 2.39.0 (shipped by StanHeaders 2.39.1, on CRAN 2026-09-02) emits two log-density overloads, and HMC reads the unpatched one, so every chain samples a standard normal instead of the model, likelihood and priors alike. The check is one cached read of the installed model.hpp, and the refusal names the defect and the remedies (a prebuilt tmbstan binary, or a rebuild against StanHeaders 2.32.10). Affected in the wild: Linux CRAN installs of tmbstan built against StanHeaders 2.39, and source installs elsewhere; prebuilt binaries are safe. dev/prior-dropping-investigation.md carries the proof and the upstream issue text.
  • Three CI cycles of chain-agreement failures in pkgcheck’s container were this bug, not chain luck: the container reproduces them deterministically, and a one-line patch to tmbstan’s generator makes it reproduce this machine to every printed digit. The gates rationale in the test helpers is corrected accordingly; the ungated correctness assertions are what caught it.

Both dialects on the draws surface

  • posterior_epred(), posterior_linpred(), posterior_predict(), predictive_interval(), predictive_error() and pp_check() gain brms’s re_formula alongside the re.form they have shipped with. Either spelling alone is the setting, both together is refused naming the pair as one setting, and every default is unchanged. brms’s own posterior_epred.brmsfit() carries the same two spellings. One consequence for positional calls: re.form now sits one position later, so a value landing there by position meets the both-set refusal loudly rather than silently changing meaning.
  • The random-effect switch now takes effect IN-SAMPLE for posterior_predict(), predictive_interval(), predictive_error() and pp_check() on draws: it used to be consulted only under newdata, so re_formula = NA on training rows silently kept every random effect. A structured family (hmm, group-level mixtures, residual correlation) refuses the switch by name, since the structured draw is the group-level content it would remove.
  • pp_check() on draws previously accepted neither spelling and leaked re.form into bayesplot as an unknown argument.
  • set_rescor() takes brms’s rescor; rescor_value stays as an alias. New dev/brms-api-diff.md: the formals of all 92 exports shared with brms 2.23.0, diffed and triaged.

The pkgcheck container, locally

  • dev/run-pkgcheck-docker.sh and dev/pkgcheck-docker.md run rOpenSci’s pkgcheck-action container against the source tree on Windows docker, mirroring CI line for line, with a gates on/off switch for the sampler-agreement tests and a token-scrubbed dependency cache. This is the pre-push gate, and it is the instrument that found the tmbstan defect.

frmtmb 0.40.1

  • conditional_effects() gains re_formula, in brms’s spelling and with brms’s default: NA draws the population-level curve, NULL conditions on the grid’s reference group levels (pick them with conditions =), and a one-sided formula keeps the named terms, on the fit method and the draws method alike. Previously the setting was hard-coded internally, so the lme4 spelling re.form died with a matched-by-multiple-arguments error on a fit and both spellings vanished silently into the dots on draws. A user who reaches for re.form here is now told which spelling this function takes: conditional_effects() is brms’s function and speaks brms, while the fit surface’s predict() and simulate() keep lme4’s re.form. band = "profile" exists only for the population-level curve and says so.
  • The message-uniqueness test failed open on one CI layout that offered an existing-but-empty ../../R; it now requires positive identification of the package source tree and skips otherwise.

frmtmb 0.40.0

Input validation across the export surface after an rOpenSci autotest sweep, the submission materials themselves, and the retirement of hand-maintained counts in favor of an enforced uniqueness property.

Input validation

  • Scalar arguments across the export surface now refuse a wrong value by name instead of quietly doing something else. isTRUE() reads a length-2 logical, a string, an integer and NA all as FALSE, so bf(y ~ x, nl = "yes") used to build a linear model and frmtmb_control(profile = 1L) used to turn profiling off. Nineteen flags are affected, along with the count, coverage and named-list arguments beside them, across frm(), frmtmb_control(), bf(), mvbf(), set_rescor(), predict(), simulate(), confint(), conditional_effects(), frm_simulate(), frm_bootstrap(), frm_multiple(), diagnose(), vcov_cluster(), ranef(), lca(), cox(), frmtmb_family(), prior_normal(), prior_t() and check_custom_family(). Every refusal is exercised by test-input-validation.R.
  • confint(level = ) and conditional_effects(prob = ) refuse a length-2 coverage. They used to recycle it: one confint() table came back with its odd rows at 90 percent and its even rows at 95, with nothing in the output recording it.
  • A link must be named by a single string: bernoulli(link = c("log", "x")) returned the string as if it were a link, and link = 1L selected the identity link by position.
  • frm(start = ) refuses anything that is not a fully named list; an unnamed value used to be silently ignored, so the fit ran from the defaults and reported them as the user’s. frm(dry_run = ), frm(data = NULL), frm(control = ), frm(na.action = ), predict(newdata = ) and predict(re.form = ) all name their contract instead of failing downstream.
  • One behavior change worth noting: ranef(fit, condVar = 1) used to work through a truthy if () and now refuses; write condVar = TRUE.

Condition messages: property, not count

  • Every condition message template in R/ (stop(), warning() and message() alike) is asserted unique by test-message-uniqueness.R, which parses the sources at test time and fails on the first duplicate. The hand-maintained count and its ledger are gone from the standards prose: a number there was stale the commit after it was written, and the test fails at the moment of drift instead. What the walk certifies is template uniqueness (a reported message resolves to one line of source); the shared validation helpers interpolate the argument name at run time and say so.

rOpenSci submission materials

  • An audit of every @srrstats tag against the code it sits on corrected 30 claims that had drifted over 39 versions, including ten internal functions that had lost their roxygen and a not-applicable standard that was actually met and tested. The Bayesian and Monte Carlo category argument is rewritten around the package’s documented inferential surface, with every citation verified against source.
  • README gains the required statement of need and an installation section; codemeta.json is regenerated; dev/submission-draft.md fills the submission template, with the r-universe dependency precedent verified from brms, bayesplot and bridgesampling’s own DESCRIPTION files.
  • ?frm documents what data accepts: a tibble, a data.table and a plain named list all work, a matrix column is a supported model variable, and a list column is not one.

frmtmb 0.39.0

The LKJ correlation prior closes the last default-prior gap against brms and brings correlated blocks into non-centered sampling; population-level prediction becomes exact on models with random smooths; and a mechanical lint pass lands with its refusal policy written down.

Breaking: population-level prediction with random smooths

  • predict(re.form = NA) and conditional_effects() now give a true population-level prediction on models with random smooths. A smooth whose basis is indexed by a grouping factor (bs = "fs", a factor bs = "re", a tensor product with an re margin, classified from the smooth object rather than the bs string) is dropped along with the (x | g) blocks; population smooths, gp() and hsgp() keep their wiggle. The result matches mgcv::predict.gam(exclude = ) on the factor-smooth term to 3e-9. Previously all smooth blocks were kept, so re.form = NA on such a model returned the per-subject curve. This deliberately follows mgcv rather than brms, whose re_formula = NA keeps factor-smooth curves; the divergence and the reason are documented in ?predict.frmtmb_fit.
  • predict(newdata = , re.form = NA) no longer needs the grouping column of a factor-smooth term at all, since the dropped term is never rebuilt. When the column is needed and absent, a named error lands before mgcv’s internal one, and any other missing smooth column is named too.
  • An unseen level of a factor-smooth term errors by default like any other new grouping level; allow_new_levels = TRUE predicts it at the population level for an fs basis (equal to re.form = NA exactly), and is refused by name for a factor bs = "re" basis, whose design has no zero row to hand a new level.
  • conditional_effects() on a fit whose only predictors are matrix columns (scalar-on-function regression) names those columns instead of claiming nothing is plottable, and no longer offers a factor-smooth’s grouping factor as an effect.

The LKJ prior on random-effect correlations

  • prior_lkj(eta) and set_prior("lkj(eta)", class = "cor") put an LKJ prior on a random-effect block’s correlation. Class "cor" addresses a whole block, by group exactly as class "sd" does, which is brms’s spelling. The density is the LKJ one carried onto frmtmb’s unconstrained row-normalized Cholesky parameters with the exact Jacobian of that map, so on those parameters it means what lkj(eta) means on the correlation matrix. It covers us() and gr(cov = ) blocks of two or more terms, and cs(), ar1() and hetar1(), whose single bounded correlation takes the LKJ marginal (1 - rho^2)^(eta - 1) with that structure’s own Jacobian. toep() is refused by name: its parameterization is not positive definite everywhere, so it has no correlation matrix to put a density on.
  • The formula route of frm_sample() now defaults to lkj(1) on every correlation, which is brms’s own default and the last class where the two disagreed. Seeded draws differ from v0.38 for any model with a correlated random-effect block. priors = "flat" opts out, the fit route is unchanged (it is a likelihood diagnostic and keeps flat priors), frm() and check_laplace() are untouched, and the disclosure message gains a cor line.
  • Correlated blocks are now sampled NON-CENTERED on the formula route. The gate did not move: a block is non-centered when every parameter it has carries a prior, and correlations now do. (x | g), cs(), ar1(), hetar1() and gr(cov = ) join (1 | g) and the smooths.
  • Measured on sleepstudy (Days | Subject), one chain of 2000, three seeds: the flat-correlation median of 36 min-ESS/s with a 154 divergence storm on one seed becomes 120 (centered) and 116 (non-centered) with no divergences, against ~122 for the matched brms model. The prior is what closed the gap; non-centering the block is a wash on this model. dev/benchmarks.md has the table and the distribution-level validation of the density.
  • A MAP fit takes the same prior: frm(..., priors = set_prior("lkj(2)", class = "cor")) penalizes the correlation toward zero. Maximum likelihood is unaffected, because the default is a sampling default and never enters frm().

Housekeeping

  • A mechanical lint pass: the three on.exit() calls gain add = TRUE (none was clobbering another handler), one any(duplicated()) becomes anyDuplicated(), one literal pattern gains fixed = TRUE. dev/lint-policy.md records what was swept and what is deliberately not chased, with reasons, ahead of the rOpenSci submission.

frmtmb 0.38.0

Non-centered sampling with the funnel diagnosed as a prior, and four case studies: hidden Markov, functional regression, Wiener diffusion, and circular.

Non-centered sampling

  • frm_sample() gains reparameterize (default TRUE): a random-effect block whose every parameter is a standard deviation carrying a prior is sampled non-centered, b = L(theta) z, and mapped back per draw. The draws matrix is unchanged in every respect (same b[i] columns, same names, same order), so log_lik(), loo(), posterior_epred(), ranef(), conditional_effects() and hypothesis() cannot tell the routes apart. Seeded formula-route draws differ from v0.37; reparameterize = FALSE reproduces the old chains exactly, and the fit route, and with it check_laplace(), is unchanged.
  • Where each group’s own data say little (the regime the funnel lives in) the gain is decisive: a binary GLMM with 80 groups of 2 observations goes from a min bulk-ESS of 5 to 236. Where groups are informative it is a wash, and it is not offered as a blanket speed-up.
  • A block is non-centered only when every parameter it has is a standard deviation carrying a prior, and frm_sample() names every block it left centered, with the reason. This is measured, not conservatism: under a flat prior the reparameterization hands the chain a flat tail the centered geometry was blocking.
  • The correlated-slopes sampling deficit against brms is diagnosed, and it was not the centering: the flat prior frmtmb puts on a correlation’s unbounded parameter is (1 - rho^2)^(-3/2), improper, and a proper prior on that parameter fixes it in the CENTERED parameterization, beating the matched brms throughput. brms is ahead there because lkj(1) is proper, not because it non-centers. An LKJ default on correlations is the queued follow-up; dev/benchmarks.md carries the measurements.
  • check_laplace() reports each parameter’s bulk effective sample size and says so when the chain mixed too poorly to judge the approximation, instead of letting a sick chain read as a Laplace problem.

Case studies

  • vignette("case-studies") gains four sections: a two-state gaussian hidden Markov model on an animal track, validated against hmmTMB from generic starts (5.9e-10 in the log likelihood) and depmixS4 (9.7e-8); function-on-scalar regression through s(t), s(t, by = x) and a bs = "fs" factor smooth, plus scalar-on-function regression through a matrix-column linear functional term, both validated against mgcv::gam() under ML; the Wiener first-passage density written as a custom_family() (RWiener agreement 8.7e-11 over 162 settings), with a data-bounded non-decision time through a link object; and von Mises circular regression with a cyclic-basis mean and distributional kappa, against the closed-form circular MLE to 6 decimals.
  • Documented along the way: a smooth model’s logLik() equals mgcv’s ML smoothness-selection score -gam$gcv.ubre (not logLik.gam); frmtmb_family(links = ) accepts a link object, which is how a bounded parameter keeps its support without a tape branch. RWiener joins Suggests, used only by the vignette and its density test.

frmtmb 0.37.0

Leave-one-out cross-validation and the brmsfit method surface on draws, the frm_ode() pharmacometrics tier, influence plots and naming ergonomics, and the root cause of the ODE sampling defect, with patches for upstream.

Leave-one-out cross-validation

  • log_lik() on frm_sample() draws gives the ndraws x nobs matrix of per-observation log-densities, each row at that draw’s own parameter vector and conditional on its own group-level values, brms’s convention (tmbstan samples the random effects too). Addition terms enter through the objective’s own per-row density code, factored out for exactly this purpose, so cens(), trunc(), weights() and trials() cannot drift from what was fitted.
  • loo() and waic() hand that matrix to loo::loo.matrix() and loo::waic.matrix(), with r_eff from loo::relative_eff() on the chain structure. loo_compare() ranks the criteria and is loo’s own comparison when handed criterion objects. psis() returns the smoothed weights. Validated against brms on a matched model: elpd_loo within 4 percent of one standard error, log_lik column means correlated at 0.9999.
  • bayes_R2() implements the residual-based estimator of Gelman, Goodrich, Gabry and Vehtari (2019) on expected-prediction draws, exactly as brms computes it.
  • These are posterior quantities: ?loo documents that frm_sample(fit)’s flat improper priors leave them unregularized and recommends the formula route or priors = for model comparison.
  • A likelihood with no per-observation column refuses rather than inventing one: R-side autocorrelation, hmm(), mixture(groups =), in-model imputation, and laplace-marginalized draws. Each names AIC() or frm_bootstrap() as the replacement.

brmsfit method surface on draws

  • Draws objects gain as.array(), as.mcmc(), as_draws_array/df/list/matrix/rvars(), posterior_summary(), posterior_interval(), predictive_interval(), predictive_error(), ndraws(), nchains(), niterations(), nvariables(), nobs(), formula(), family(), getCall(), ngrps(), coef(), pp_mixture(), and the bayesplot delegations mcmc_plot(), pairs(), nuts_params(), log_posterior(), rhat(), neff_ratio(). 65 of the 96 brmsfit methods now resolve.
  • conditional_effects() on draws evaluates the effect grids once per posterior draw and bands the curves with their own quantiles: no band = or method = to choose, nonlinear predictors and nominal per-category displays work without a delta method, and it runs on formula-route draws that have no maximum-likelihood fit behind them. ndraws thins the draws for a cheaper curve.
  • The default effects of conditional_effects() now include one "a:b" display per fitted interaction, as in brms; a term of order three or more contributes its leading pair, with the remaining variables at reference values until conditions = pins them.

Living beside brms

  • Attaching brms (or rstantools, nlme, lme4, loo, posterior, bridgesampling) after frmtmb no longer breaks dispatch: every frmtmb method on a generic those packages also export is additionally registered into the foreign generic’s own table, so conditional_effects(), hypothesis(), fixef(), log_lik(), posterior_epred() and forty-odd others resolve regardless of attach order. Verified against the full surface with brms attached.
  • bf() is a plain function and IS masked by library(brms); a brms-built formula reaching frm() now says exactly that and names the two ways out (frmtmb::bf(), or attach brms first).

Tape-safe scope for user code

  • An ODE dynamics function and a custom_family() density no longer need the "c" <- RTMB::ADoverload("c") boilerplate: RTMB’s tape-safe c(), [<- and diag<- are spliced into the function’s own body (unless it already binds them), and nonlinear formula bodies evaluate with the same names in scope. A helper such a function CALLS still needs its own bindings, because lexical scope does not travel into other functions.
  • The brms methods frmtmb does not have fail with the reason and the replacement instead of “could not find function”: loo_moment_match(), loo_subsample(), reloo(), kfold(), bridge_sampler(), bayes_factor(), post_prob(), stancode(), standata(), expose_functions(), restructure(), and the deprecated spellings posterior_samples(), nsamples(), parnames(), LOO(), WAIC().
  • Suggests gains loo (the estimators are its own, so loo::loo_compare() and loo::pareto_k_table() read the results unchanged) and coda (as.mcmc() gives gelman.diag() one component per chain).

Pharmacometrics in frm_ode()

  • Time-varying covariates. frm_ode(tv = ) takes dynamics inputs that change WITHIN a group, as a step function of time: the solve splits at each change point and each segment’s dynamics see that segment’s value as an extra parameter. The values may be estimated (a covariate-dependent clearance that changes at a known visit); the change points must be data, and tv_break names the column they come from. Last observation carried forward, rxode2’s rule for covariates, and the state is continuous across a change because a covariate moves the derivative, not the state. A gradient through an estimated step rate matches central differences to 1e-11.
  • Steady-state dosing. An events row marked ss = TRUE with an ii says its cycle has already settled: every compartment is zeroed, as NONMEM and rxode2 read such a record, and the cycle is repeated n_ss times (default 20) before the record’s time. This is an approximation, not a closed form, and it says so: off the tape the last two cycles are compared and a shortfall past ss_tol warns. Exact against the analytic one-compartment superposition to 2.4e-10.
  • Reset events. method = "reset" sets every state to value, which at zero is NONMEM’s EVID = 3 and rxode2’s evid = 3. A reset beside a dose at one instant is EVID = 4; the reset goes first, so the table’s row order does not decide the answer.
  • ii and addl columns on events write a repeated schedule compactly, expanding to exactly the hand-written rows.
  • output may be a column instead of a scalar: one state per ROW, over one shared solve. A parent and its metabolite in one assay column, or two species stacked long, is one call rather than two, at half the solves and the identical likelihood.
  • Proportional-plus-additive error is documented in ?frm_ode and vignette("ode") through nlf(sigma ~ ...) over the ODE mean. No new machinery, just the spelling.
  • parms may be empty when every dynamics input is time-varying.

Influence plots and naming ergonomics

  • plot() on an influence() result draws the Cook’s distances with the top cases labeled and one dfbetas panel per coefficient, each with the conventional 2/sqrt(n) reference band (Belsley, Kuh and Welsch 1980). which and ask follow plot() on a fit. Not car::influencePlot() compatibility, and the man page says why.
  • hypothesis() says so, once per call, when a fixed-effect coefficient shadows a natural-scale name: a covariate literally named sigma hides the residual SD, and a coefficient spelling out sd_<group>__<term> or ar1 hides its summary. The shadowed quantity is reachable under a leading dot (.sigma, .sd_g__Intercept), which variables() lists exactly when the collision exists.
  • vignette("inputs") gains a naming-collisions section: which vocabularies can meet, which meetings are refused, and the actual resolution precedence in a nonlinear body (nonlinear parameter, then data column, then distributional parameter).
  • frm() and frm_sample() bounds and confint(parm =) accept the bare name of an intercept-only nonlinear parameter (la for la_(Intercept)). A nonlinear parameter with several coefficients is refused, naming the full spellings.

The ODE sampling defect, root-caused

  • The v0.36 finding that frm_sample() on an frm_ode() fit fails at warmup iteration 1 is now fully explained, and it is not frmtmb’s to fix: RTMBode’s solver wrapper lets deSolve failures escape as R errors into Stan’s C++ (a hard error when a rate reaches infinity, and an early return with fewer rows than requested). dev/upstream/ carries the analysis, a three-patch series against the RTMB repository (verified: the Lotka-Volterra model samples end to end with 0 divergences on the patched build), reproduction scripts, and issue text ready to file. The same investigation found the more-than-8-states ceiling to be an integer overflow in deSolve’s lsoda workspace formula.

Fixed

  • diagnose() no longer relays “NaNs produced” warnings from the pathological fits it exists to inspect; the NaN standard errors are the finding, reported through bad_se.
  • The formula-vs-fit sampler agreement test compares the two taped densities pointwise (deterministic) instead of leaning on platform-dependent chains.

frmtmb 0.36.0

Robustness in three senses (numerical, distributional, inferential), per-parameter nonlinear formulas, one simulator per family, direct sampling, and t2() newdata prediction.

Behavior changes

  • Draws objects use parenthesis-free parameter names throughout (Intercept, not (Intercept)), matching brms and what variables() and hypothesis() already spoke. This changes summary(ds), fixef(ds), as_draws(ds) and check_laplace() output; fits are unchanged, and both spellings still resolve in hypothesis() and frm_sample(priors =).

Nonlinear formulas per parameter

  • nlf() declares a nonlinear formula for one parameter, as in brms: bf(y ~ a) + nlf(a ~ exp(b * x)) + lf(b ~ 1), and any parameter can carry one - nlf(sigma ~ a + b * z) is a nonlinear model for the residual SD with a linear mean, which nl = TRUE cannot spell. Nonlinearity is now a property of one parameter; bodies chain to any depth in dependency order, cycles are refused by name, and the old nl = TRUE models are bit-for-bit unchanged.
  • A body may read another distributional parameter’s per-row value: the spelling for nlme’s varPower(form = ~ fitted(.)). A data column still wins over a parameter name, so ported brms bodies keep their meaning. frm_ode() works inside nlf() bodies.

Simulation and sampling

  • simulate(), posterior_predict() and frm_simulate() now share one simulator per family. Three previously silent wrongs are fixed: mixture(groups =) drew a class per ROW instead of per group in posterior_predict() and frm_simulate(), and residual autocorrelation draws were independent there. hmm() and mixture_mvn() gained simulators on all three paths; cox() explains its refusal at each entry point.
  • frm_sample() accepts a formula and samples without a maximum- likelihood fit first: frm_sample(bf(y ~ x + (1 | g)), data = dd, family = gaussian()) tapes the model and starts from random inits. The formula route defaults to brms 2.23’s weakly informative priors (replicated against brms::default_prior() exactly, zero-shift rule included), announces every default and every deliberate gap (thresholds, shapes, correlations) in one message, and priors = "flat" opts out. frm_sample() on a FITTED model is unchanged - flat, likelihood-shaped - which is what keeps check_laplace() meaningful.
  • frm(dry_run = "objective") returns the taped model unfitted.

t2() smooths

  • predict(newdata = ) works for t2() smooths, including function-on-function terms and t2(..., by = ). A t2 basis carries a second, prediction-only constraint that the fit does not; smooths are now built with smoothCon(modCon = 3), which drops it and leaves the design, penalties and log-likelihood bit-identical. The refusal is gone.

Student-t random effects

  • (x | gr(g, dist = "student")) gives a grouping term a Student-t latent instead of a gaussian one, brms’s spelling. An outlying group then costs the variance component far less: with one group of 30 displaced by 10 standard deviations, the gaussian latent SD inflates to 1.85 against a truth of 1 while a t(5) latent holds at 1.25, and the intercept’s RMSE falls from 0.298 to 0.180. Correlated and diag() blocks are one multivariate t with a single mixing variable per level, which is brms’s construction and is verified against mvtnorm::dmvt() to 1e-10.
  • The degrees of freedom are FIXED, at dist_nu (default 5), not estimated. brms estimates nu under a gamma(2, 0.1) prior, and the prior is carrying it: by maximum likelihood the whole grid from 2.1 to 500 sits inside the 95% profile interval at 20 groups, and joint ML runs to a boundary in 24% to 41% of replicates there. This is the frequentist analogue of brms’s own prior(constant(3), class = "df"). Compare a few values with logLik() instead.
  • The reported quantity is the t’s SCALE, not its standard deviation - which is also what brms’s sd_<group>__<term> is. VarCorr() stores the scale matrix, tags it with nu, and prints a Scale column, a converted Std.Dev. column (scale * sqrt(nu/(nu-2))) and the fixed nu, so the convention is visible instead of silent.
  • quadrature = TRUE accepts scalar t blocks and marginalizes them EXACTLY, agreeing with adaptive Gauss-Hermite quadrature to 1e-6. It is the recommended check: the Laplace approximation over a t latent biases the estimated scale UPWARD, by under 2% of a standard error at 8 observations per group but by a factor of three where a near-null variance component meets two-observation groups. dev/tre-feasibility.md has the measurements.
  • simulate() and frm_simulate() draw a multivariate t; predict(allow_new_levels = TRUE) inflates the unseen level’s variance by nu/(nu-2); REML = TRUE, ranef(), confint() and frm_bootstrap() work unchanged. Refused with their own messages, and documented in ?frmtmb-student-re and frm_compat(): gr(cov = )/gr(prec = ), mm(), |ID| keys, and every covariance structure but us and diag.

Cluster-robust (sandwich) standard errors

  • vcov_cluster(fit, cluster = ~ g, type = "CR0") gives the cluster-robust covariance of the estimates: the inverse observed information of the marginal likelihood sandwiching the outer product of the per-cluster scores. type takes "CR0", "CR1", "CR1p" and "CR1S", spelled and defined as in clubSandwich. vcov(fit, cluster = ~ g, type = ) is the same thing in the sandwich::vcovCL() spelling.
  • cluster_scores(fit, ~ g) returns the per-cluster scores themselves, one row per cluster, so any other sandwich can be built from them. This is what sandwich::estfun() would return if a marginalized objective had per-observation contributions; it does not, which is why frmtmb still ships no estfun() method.
  • confint(), hypothesis() and summary() take vcov =: a covariance over the whole outer parameter vector, or a function of the fit returning one. A matrix from vcov_cluster() carries G - 1 reference degrees of freedom, so those methods switch from a normal to a t reference automatically.
  • The estimator is refused, with the reason, wherever the marginal likelihood does not factor over the clustering factor: a random effect whose level spans two clusters (crossed effects, mm() pooled levels, a global smooth, gp(), car(), spde()), a group-level mixture whose groups span clusters, autocor(), hmm(), rescor = TRUE, mi()/me(), REML = TRUE, frmtmb_control(profile = TRUE), quadrature = TRUE, and any fit made with priors. frm_bootstrap() is the documented fallback.
  • "CR2"/"CR3" are refused rather than approximated: the Bell-McCaffrey family is defined through the hat matrix of a linear or GLS model, which a Laplace-marginal likelihood with a nonlinear link does not have.

Numerical robustness of the family log-densities

The log-densities now evaluate from the LINEAR-PREDICTOR scale wherever an exact form exists, instead of undoing the link and recomputing. An inverse link saturates - plogis(40) is exactly 1 in double precision, and 1 - exp(-exp(4)) is too - so a density written over 1 - mu returned -Inf with an unusable gradient in a region the linear predictor describes perfectly well. A fit that converges never visits it; an optimizer step that overshoots, a separated predictor, a wide quadrature node and a frm_sample() tail draw all do.

  • bernoulli(), binomial() and zero_inflated_binomial() use RTMB::dbinom_robust(), the log-odds parameterization glmmTMB fits with. They were wrong in the second decimal at a linear predictor of 30 and gave -Inf at 40; they are now exact over the whole line. With the cloglog link the old form had no gradient past a SINGLE-DIGIT linear predictor.
  • negbinomial(), nbinom1() and geometric() use RTMB::dnbinom_robust(), which takes log(mu) and never forms mu / var. The old form gave NaN at a linear predictor of -40.
  • Every zi and hu gate, and asym_laplace()’s quantile, run in log space, so a separated zero-inflation predictor stays differentiable.
  • Beta(), zero_inflated_beta() and beta_binomial() take both shapes off the log-odds, so the second one no longer collapses to zero.
  • hurdle_poisson()’s zero-truncation normalizer log(1 - exp(-mu)) uses expm1, which keeps it at a tiny mean.
  • cumulative(), sratio() and cratio() with the logit link compute their CDF differences in log space. acat(), categorical() and multinomial() accumulate their denominators with logspace_add() instead of summing exp(), which used to overflow at a linear predictor of 709 / (K - 1).
  • Values and gradients at ordinary linear predictors are unchanged to 1e-15, and every fitted model in the regression set lands on the same estimates (largest coefficient change 9.5e-9, largest log-likelihood change 4e-12). cumulative(probit) keeps the plain CDF difference, which has no exact log-space form; it is fragile past a linear predictor of 8, so prefer the logit link. New file tests/testthat/test-numerical-robustness.R pins the behavior.

New family

  • huber() fits Huber’s least-favorable distribution: gaussian within k residual standard deviations of mu and Laplace outside, so an outlier pulls on the fit with bounded influence. It is a proper normalized density, so this is ordinary maximum likelihood and logLik() and AIC() mean what they say. Huber’s tuning constant is a fixed argument, huber(k = 1.345), not an estimated parameter, matching how MASS::rlm() treats it. Point estimates track MASS::rlm(psi = psi.huber); the remaining gap is the scale (rlm() fixes it at a MAD-type estimate, huber() estimates sigma by ML), and holding sigma there reproduces rlm() to 1e-5. The asym_laplace() working-likelihood caveat applies: Wald standard errors under a misspecified error distribution are not calibrated, so use frm_bootstrap().

frmtmb 0.35.0

Hidden Markov models, latent class analysis, multi-membership random effects, three new families (nominal, circular, proportional hazards), profile and bootstrap effect bands, and a brms-portability batch measured against the brms vignettes themselves.

Behavior changes

  • frm() defaults to family = gaussian() when neither the family argument nor a + attachment supplies one, the brms/lme4/glmmTMB convention. frm(y ~ x, data = d) was an error and is now a linear model. The family argument overrides a family attached to a univariate bf() and fills only the empty responses of an mvbf().
  • Documentation and examples now lead with the separate-family spelling frm(bf(y ~ x), family = gaussian(), data = d); the + attachment stays valid and documented as the alternative.

Portability (measured against the brms vignettes)

  • A scorecard audit ports every model call of the brms vignettes through the mechanical brm to frm transform (dev/brms-vignette-port.md): with this release’s fixes the measured tally is 30 of 42 model calls running mechanically, the remaining spelling changes documented in the porting guide.
  • hypothesis() accepts brms’s directional form ("a > b") with one-sided p-values and bounds across every method, plus the class=/group= naming shorthand. update() speaks formula./newdata and dotted deltas (. ~ . + z). lf() composes dpar formulas onto bf(). One formula may name several parameters (a + b ~ 1). A bare constructor (family = cumulative) works.
  • One parameter-addressing vocabulary across hypothesis(), profile(), confint(parm =) and bounds: parenthesized and paren-stripped spellings are interchangeable, and one-to-one natural-scale names (sd_g__x, ar1) reach their internal parameter with the scale stated. Ambiguous natural-scale names are refused with the alternatives named.

Multi-membership and effect bands

  • Multi-membership random effects: (x | mm(g1, g2, weights =, scale =)) and mmc() member-specific covariates, following brms; the pooled-level Z construction matches brms::make_standata() exactly, and every post-fit method reads the block unchanged. New-level prediction variance distinguishes a shared new level (one draw, weights add) from distinct new levels (independent draws).
  • conditional_effects() gains band = c("wald", "profile", "boot"): likelihood-ratio bands inverted per grid point, and pointwise percentile bands from one shared parametric bootstrap (reused across effects and verified against the grid it was run over). Effect discovery now works on nonlinear and mo()/mi() fits, and frm_bootstrap() works on ordinal fits (previously all NA).

New families

  • categorical() fits a multinomial logit to an unordered factor, brms’s spelling of the likelihood multinomial(K) already carried on a count-matrix response. The first level is the reference and each remaining level gets its own linear predictor named mu<Level>, as in brms; the main formula applies to all of them unless a dpar formula overrides one, so bf(y ~ x, mustout ~ w) gives one category its own predictor. fitted() and predict(type = "response") return the n x K matrix of category probabilities with the response’s own levels as column names, the same convention the ordinal families follow, and simulate() draws factor levels. Validated against nnet::multinom (4e-10 in the log-likelihood) and against multinomial(K) on the one-hot response (1e-8). The categories are read off the data by frm() before the formula is parsed; categorical(levels =) or categorical(K =) states them for the paths that have no data. residuals() is refused: a nominal response has no scale for one.

  • von_mises(link = "tan_half") for a circular response in (-pi, pi], with the mean direction mu through the tan-half link and the concentration kappa through a log link - brms’s parameterization. The normalizing constant’s log I0(kappa) is differentiated exactly by RTMB’s own besselI method, so nothing is a series approximation. kappa ~ x distributional models work, and the simulator is a vectorized Best-Fisher sampler that varies both parameters per row. Validated against a hand-rolled RTMB likelihood (1e-6) and circular::mle.vonmises().

  • cox() fits proportional hazards with brms’s flexible baseline: an M-spline baseline hazard over a simplex of weights, with the cumulative baseline hazard the I-spline integral of the same basis (df = 5 cubic splines with an intercept, brms’s bhaz() default). Censoring runs through the ordinary cens() addition term - an event contributes the density and a censored row the survivor function - so right, left, and interval censoring all work, and frailty models come free through the Laplace approximation: time | cens(c) ~ x + (1 | g). New cox_baseline() returns the fitted baseline weights. Validated exactly against a hand-rolled M-spline PH likelihood (1e-6 in the log-likelihood, 1e-4 in the coefficients) and against survival::coxph() to 2e-2 on the log hazard ratio, which is the honest claim: coxph() leaves the baseline fully nonparametric. fitted(), predict(type = "response") and simulate() are refused - a survival time has no mean the censored rows identify, and brms refuses the same question. Unpenalized maximum likelihood often puts a baseline weight on the simplex boundary, which the optimizer reports as singular convergence even at the optimum; ?frmtmb-families explains it and lowering df is the remedy.

  • New tan_half link, for von_mises().

Internal

  • A family may declare aterm_data(y, aterms), family-level data that no addition term supplies, built once at frame assembly from the validated response. cox()’s spline bases use it.

  • A family’s lcdf() may take the family-level extra parameters as a fourth argument, which is what lets a survival family’s survivor function reach its baseline. The three-argument contract is unchanged for every other family.

Hidden Markov models as a first-class family.

New

  • hmm(K, family, time =, group =, init =, trans =) fits a K-state hidden Markov model. The response at each time point comes from one of K state-dependent copies of family, and the unobserved state follows a first-order Markov chain along time within group. The state sequence is summed out EXACTLY by the forward algorithm, evaluated on the same AD tape as everything else, so nothing about the Laplace machinery changes: a random effect in a state’s linear predictor is integrated outside the exact state sum. Each of the wrapped family’s parameters is copied per state and suffixed (mu1, sigma1, mu2, …) with the full formula grammar, random effects included, exactly as mixture() does. Transition probabilities are a row-wise multinomial logit named tr{i}{j} with state 1 the reference cell of every row, and trans = ~x gives every cell a predictor at once. Validated against depmixS4 (logLik to 1e-8 or better and every parameter to five decimals, on gaussian, poisson and categorical emissions with and without transition covariates) and against hmmTMB (1e-12 on the stationary fixed-effect model).
  • init = "stationary" (the default) solves the chain’s stationary distribution on the tape and costs no parameters; "estimated" adds K - 1 free logits, "uniform" fixes them. Stationary is refused when a transition cell carries a predictor, because a time-varying chain has no single stationary distribution.
  • hmm_probs() returns the smoothed state occupancies P(S_t = k | y) from a forward-backward pass - the mixture_probs() analog - and hmm_viterbi() the maximum-a-posteriori state path. fitted(), predict(type = "response") and the response and pearson residuals all route through hmm_probs(), so they report the occupancy-weighted mean sum_k P(S_t = k | y) mu_k(x_t) rather than any single state’s. simulate() walks the chain forward per sequence and then emits, so DHARMa and pp_check() see the fitted persistence.
  • A missing response is a time point the chain passes through without emitting: hmm() keeps the row and masks its emission instead of letting na.action drop it, which would shorten the chain and bias the transition matrix. nobs() counts every row; fitted() and residuals() are NA there while hmm_probs() is not.

Refusals

  • REML, quadrature = TRUE and frmtmb_control(profile = TRUE) are refused on an hmm() fit, each with the reason. REML in particular used to run and produce a partial restricted likelihood matching no standard definition.
  • weights(), cens(), trunc(), se() and mi() on the response, multivariate models and rescor, residuals(type = "osa"), residuals(type = "deviance"), predict(se.fit = TRUE) and predict(newdata =) on the response scale, and conditional_effects() are refused, all naming the per-sequence likelihood as the reason.
  • A grouping in which every sequence has length 1 is refused: the transition parameters are then flat directions the reported df would still count, and the model is a mixture(). Holding every transition dpar at a constant lifts the refusal, and the fit then reproduces mixture() to 1e-12.
  • A start with every state’s location predictor at the same value warns: it sits on the label-symmetry axis, where the optimizer cannot separate the states. The default quantile-spread starts never do this.

Known limits

  • Multimodality is real and no convergence diagnostic reports it. On a random-effect model the default cold start has been measured converging 8.1 log-likelihood units below the global optimum with convergence == 0, max|grad| == 3.5e-4 and a positive-definite Hessian. Compare starts before reporting.
  • With random effects the Laplace approximation is genuinely approximate even for a gaussian response, because the integrand is a mixture over state sequences: the measured bias is -0.126 in the log-likelihood (8.9e-5 relative) and 4.4e-4 in the parameters against adaptive quadrature. ?hmm says so.
  • The tape build grows slightly faster than linearly in the number of rows: about 1.9 s at 20 000 rows, against milliseconds to evaluate it. Below 5 000 rows nothing is noticeable.

New

  • Latent class analysis, the poLCA measurement model, as the family lca(K). The response is a matrix of polytomous item codes, one row per subject and one column per item; the items are conditionally independent given a subject’s latent class, and each class carries its own item-response profile. Class membership is the theta1 ... theta{K-1} dpars with full linear predictors, so a covariate on the model formula gives poLCA’s latent class regression for free, with fixef(), confint(), hypothesis(), set_prior() and frm_sample() on the gating coefficients. Items may have different numbers of categories; ncat declares them, and by default they are inferred as the largest observed code per item, as poLCA does. The item profiles are family extra parameters, one vector pi<j> per item holding its K * (C_j - 1) reference-category logits, so they appear per item in summary() and as pi<j>_<i> in confint(). Validated against poLCA on its own shipped data: the carcinoma 3-class model agrees to 4.2e-8 in log-likelihood, 2.8e-8 in item profiles, 6.2e-9 in class sizes and 9.9e-8 in posterior membership, and the election latent class regression agrees to 1.1e-7 in log-likelihood, 1.1e-7 in item profiles and 9.9e-7 in gating coefficients. A hand-rolled optim() reference on simulated data agrees to 1.5e-9, and a one-item fit reaches the saturated single-categorical likelihood to 5.6e-11.
  • lca_profiles() returns the class-conditional item-response probability tables (poLCA’s probs) with the estimated class sizes attached, and prints them. lca_probs() returns posterior class membership per subject (poLCA’s posterior) with the relative entropy of the classification attached; it is mixture_probs() under an LCA-specific name and check.
  • lca(na.rm = FALSE) keeps subjects with missing items and masks each missing item out of that subject’s likelihood, poLCA’s na.rm = FALSE behavior. The default drops incomplete subjects through the usual na.action, which is poLCA’s default.

Notes

  • lca() starting values are deterministic: subjects are scored by the mean of their item codes rescaled to [0, 1], cut into K equal-count slices, and each slice’s smoothed category proportions seed one class. Class 1 is the low-score end, so a data set always gets the same labeling. Multimodality is unchanged; ?lca shows the perturbed-start loop that replaces poLCA’s nrep.
  • lca() v1 refuses random effects, smooths and gp() anywhere in the model (that is the growth-mixture shape, which mixture(..., groups = ~g) fits), REML, profile = TRUE, quadrature, every addition term, mvbf(), and residuals(type = "osa"). fitted(), residuals() and predict(type = "response") are refused because a matrix of nominal item codes has no mean; predict() returns the gating linear predictor.

frmtmb 0.34.0

Within-group residual correlation (R-side effects), quantile regression completions, and plot conveniences.

New

  • Within-group residual correlation, brms’s R-side autocorrelation, as formula terms ar(), ma(), arma(), cosy() and unstr() for gaussian() and student(). The residuals of a group become one multivariate draw, y_g ~ N(mu_g, D R D), with a sigma ~ x distributional model entering through the diagonal. The ARMA autocorrelation function is exact and orders above one are supported (brms caps its covariance form at one). Validated against nlme::gls and nlme::lme under ML and REML to 1e-9 or better across all five structures, on balanced and ragged data, and with random effects alongside the correlated residual. New autocor_matrix() returns the fitted correlation matrix; the parameters appear in summary(), confint(), confint_varcorr() and hypothesis() under brms’s names. See ?frmtmb-autocor for the deliberate divergences: sigma is the MARGINAL residual SD (brms uses the innovation SD for ar/ma/arma; the migration vignette gives the conversion), lags count gaps in the global time-level set (nlme semantics), and brms’s default cov = FALSE residual-regression form is refused rather than reinterpreted. simulate() draws correlated residuals, so DHARMa and pp_check() see the fitted autocorrelation. Combinations that stop the likelihood factorizing over rows (weights, cens/trunc, se, mi, rescor, mixtures, quadrature, nl, OSA residuals, other families) are refused with the alternative named.
  • zero_inflated_asym_laplace() (brms spelling): a point mass at zero mixed with the asymmetric Laplace, for zero-inflated quantile regression. A new “Quantile regression inference” section on the families page states plainly that ALD-based Wald intervals are not calibrated (a property shared with brms) and points at frm_bootstrap().
  • plot(conditional_effects(fit), points = TRUE) overlays the raw observations, the brms argument, previously ignored. Points are drawn under the bands; displays where raw observations are not meaningful (per-category ordinal, non-mean dpars, matrix responses) say so instead of failing silently.
  • Hidden Markov models: a feasibility study (dev/hmm-feasibility.md) establishes that the forward algorithm tapes in RTMB, composes with the Laplace approximation over random effects, and is expressible today through custom_family() with vint() payloads, validated against depmixS4 and hmmTMB. No user-facing grammar yet; the design for a first-class hmm() family is recorded.

frmtmb 0.33.0

Two residue fixes, a documentation dark mode, and CI repairs.

Behavior changes

  • posterior_epred() on an ordinal fit returns a draws x observations x categories array with dimnames list(NULL, observation names, category levels), replacing the flattened draws x (n * K) matrix of v0.31/v0.32. This is brms’s documented shape (an S x N x C array for categorical and ordinal models). The array is the old matrix reshaped: matrix(as.vector(ep), nrow = ndraws) recovers the previous value and column order exactly, and ep[, , "high"] or ep[k, , ] replace the old naming recipe. Scalar-response families, posterior_predict(), and posterior_linpred() are unchanged. A documentation error is also corrected: multinomial() response-scale predictions are a vector, not a category matrix, and the docs no longer claim otherwise.

New

  • A bf(nl = TRUE) body can name an object of its formula environment that could never be a column of data (a data.frame, a list, an environment, a formula), so frm_ode(..., events = doses) takes a dosing table by name instead of needing an inline data.frame() or a wrapper function. The boundary is exactly what stats::model.frame() refuses: vectors, factors, and matrices are legal model-frame variables and still resolve through the model frame, and a column of data still wins over a same-named object. A body that fails to evaluate reports which names were resolved outside the data.
  • The documentation site has a light/dark/auto theme toggle.

CI

  • The pkgcheck and dependency workflows resolve RTMBode from r-universe: extra-repositories on the setup-r action, and an R_PROFILE_USER profile for the pkgcheck container, whose pak cannot read Additional_repositories from DESCRIPTION.

frmtmb 0.32.0

The merged-|ID| Kronecker path, the ordinal prediction surface, ODE dosing, and vignette figures.

New

  • frm_ode() gains events, a per-dataset dosing table (time, value, state, optional group, method of add/replace/ multiply, duration for constant-rate infusions) supporting repeated doses, and event_scale, an estimated multiplier on every dose amount, so bioavailability is an ordinary nonlinear parameter with covariates and random effects. Dosing splits the solve at the event times instead of using deSolve’s events argument: deSolve events do not differentiate correctly through RTMBode’s augmented system (replace and multiply measured 42 and 59 percent relative gradient error; reported upstream). A branch on time inside a derivative function is refused by RTMB, and an approxfun() forcing table inside one is silently wrong; both are documented. vignette("ode") gains a repeated-dosing section.
  • Terms sharing an |ID| key whose grouping is gr(g, cov = A) or gr(g, prec = Q) now merge into one Kronecker block instead of being refused: mvbf(bf(y1 ~ (1 | q | gr(id, cov = A))), bf(y2 ~ (1 | q | gr(id, cov = A)))) is the same joint density as the long format (0 + trait | gr(id, cov = A)), verified to 7e-10 on the log likelihood under ML and REML and on both the covariance and precision sides. The compatibility rows move from refused to conditional.
  • Vignettes carry figures where a picture beats a table: a forest plot for the meta-analysis, the monotonic step shape, the location-scale band with the mgcv overlay, growth-mixture trajectories by recovered class, the sleepstudy small multiple, and concentration-time curves in the ODE vignette. Figures use the suggested tinyplot package and are skipped when it is not installed.

Behavior changes

  • fitted() on an ordinal family returns the n x K matrix of category probabilities (the brms convention), exactly as predict(type = "response") does; it previously returned the latent linear predictor. The invariant predict(type = "response") == fitted() now holds for every family with no documented exception. predict(type = "link") keeps the latent predictor.
  • residuals(type = "response") and type = "pearson" on an ordinal fit score the categories by their codes: y - sum_k k * P(y = k), standardized by that distribution’s own sd. "response" previously returned the observed code minus the latent predictor without saying so, and "pearson" errored.
  • One |ID| label spread over more than one grouping specification is now an error. Previously the merge key included the deparsed grouping call, so (1 | q | g) next to (1 | q | gr(g, cov = A))) landed in separate blocks and silently did not correlate at all, which is not what the shared label asks. |ID|-linked gr() terms whose matrices resolve to different objects are refused at frame assembly, comparing the resolved matrices.

Corrected behavior

  • emmeans() on an ordinal fit no longer fails with “Non-conformable elements in reference grid”; the basis is built by column name (marginal means stay on the latent scale, the emmeans convention for clm-like models). pp_check() and dharma_residuals() work on ordinal fits (simulated ordered factors are compared on their integer codes; DHARMa’s rank transform runs on simulated categories with integerResponse = TRUE). plot() works on an ordinal fit.
  • conditional_effects() on an ordinal fit draws one probability curve per response category (a cats__ column), with delta-method standard errors over the joint covariance of coefficients, thresholds, and cs() terms, and bands on the logit scale so they cannot leave [0, 1]. method = "predict" is refused there; dpar = "mu" gives the latent display.

frmtmb 0.31.0

ODE models, an exotic-models case-study vignette, and the defect wave the vignette work exposed.

New

  • frm_ode() fits ordinary differential equation models inside a bf(nl = TRUE) body. Dynamics parameters and initial states are ordinary nonlinear parameters, so they take fixed effects, random effects and covariates, and the Laplace approximation is exact through the solver’s adjoint. It solves one small system per group and scatters the solution back into row order; ragged designs, unsorted rows, repeated times and an observation at t0 all work. Needs the optional RTMBode package (not on CRAN; Additional_repositories now names https://kaskr.r-universe.dev), and every code path, test, example and vignette chunk degrades cleanly without it. New vignette("ode") works a population pharmacokinetic model on datasets::Theoph.
  • frm_ode() refuses the ways an ODE model goes quietly wrong: a dynamics parameter that varies inside a solve group is rejected by name at frame assembly; fixed-step integrators such as rk4 are refused because they return a different likelihood, not a noisier one; a system near the Laplace ceiling of about eight states warns. A failed solve is reported, not absorbed: on numeric evaluation paths (predict(), simulate(), residuals(), and a body with no estimated dynamics inputs) the group’s rows get a penalty value and a warning naming the group, and frm_ode_failures() reads the record back. During fitting most failures cannot be detected at all (RTMBode returns an AD object), so they surface as the optimizer’s NA/NaN gradient; the help page says which is which.
  • New vignette("case-studies"): eight worked models from the showcase literature of brms, MCMCglmm, metafor and mgcv. The animal model and its multi-trait form, phylogenetic regression, random-effects meta-analysis and meta-regression, monotonic ordinal predictors, location-scale regression, growth mixtures, measurement error, and sequential ordinal models with category-specific effects. Every section cross-checks its fit against a reference package or a closed form, the multi-trait section proves its pedigree matters with an identity refit, and tests/testthat/test-case-studies.R pins the agreements. Suggests gains ape and metafor for the cross-checks, and data.table and tibble for tabular-input tests.

Breaking

  • A gr(cov = ) or gr(prec = ) term whose |ID| key is shared with another term is refused at parse time. The cross-formula merge hardcoded an unstructured covariance and silently discarded the matrix, so earlier fits of that construct ignored the structure they named. The error points at the supported long format, (0 + trait | gr(id, cov = A)), which takes the verified Kronecker path. A lone gr() term with an unshared key keeps its structure and still fits.

Corrected behavior

  • hypothesis() and variables() expose sd_<group>__<term> and cor_ names for gr(cov = ), gr(prec = ) and equalto() blocks, so heritability-as-ICC is one line on an animal model. Smooth, gp(), car() and spde() blocks stay excluded; ?hypothesis says which and why.
  • vcov(full = TRUE) works under REML and profile = TRUE, taking the outer block from the joint precision instead of warning and returning only the fixed effects. Its rows are confint()’s rows.
  • The observation-level-random-effect check no longer fires on gr(cov = ), gr(prec = ) or equalto() blocks with one row per level: a fixed relationship matrix identifies the two variances, which is exactly the animal model with one record per individual.
  • predict(type = "response") on the four ordinal families returns an n x K matrix of category probabilities named by the response’s levels, honoring cs() on newdata; se.fit there is refused rather than faked. marginaleffects gets the categorical group convention (also fixing multinomial() row alignment), posterior_linpred(transform = TRUE) stays on the latent dpar as documented, and posterior_epred()’s flattened categorical shape is named and documented.
  • anova() and drop1() report NA instead of < 2.2e-16 *** when the compared models have equal df.
  • confint_varcorr() no longer reports a zero-width interval at its internal clamp for a correlation estimated at plus or minus 1 or an sd at zero; those rows keep the estimate, get NA bounds, and warn. VarCorr() and ranef() handle two blocks sharing a term label (one printed twice, the other silently dropped a block).
  • A Date, POSIXct or difftime predictor is reported at frame assembly with its unit and 1970-01-01 origin plus the centering fix; the epoch-scale magnitude can stop the optimizer converging. Responses and grouping variables are exempt (srr G2.5, G2.9).
  • A nonlinear formula body that fails to evaluate reports which of its names were resolved to functions instead of found in data, so a misspelled column sharing a name with a base function (t, c) is named rather than surfacing as a coercion error.
  • srr completeness: srr_stats_pre_submit() reports no missing standards (seven were untagged; six were already satisfied and are now tagged with pinning tests, G2.9 is satisfied by the datetime reporting above).

frmtmb 0.30.0

data2, ODE feasibility, and the remaining rOpenSci runway.

New

  • frm(..., data2 = list()), the brms spelling for structural objects: the matrices of gr(cov =)/gr(prec =), equalto(), car() and spde() resolve from data2 before the data and the formula environment (compound expressions over data2 objects work, a documented permissive divergence from brms). data2 is stored on the fit, so saveRDS() carries the matrices and refit()/update()/drop1()/influence()/frm_multiple() re-assemble in a session where the calling environment is gone.
  • Population pharmacokinetic models work today through nl = TRUE bodies calling RTMBode::ode() per subject (RTMBode installs from kaskr.r-universe.dev): the Laplace approximation is exact through the adjoint solver, and a one-compartment population fit agrees with nlmixr2’s FOCEi to three decimals with no compiler. The feasibility study, the sharp edges (never stack subjects into one system; adaptive integrators only), and the planned frm_ode() helper are recorded in dev/ode-feasibility.md.

rOpenSci runway

  • Every exported help page has executing examples (35 pages added; the as-cran check runs them all in ~14s).
  • Every internal function (288) is documented with its contract under @noRd; a new “Inputs and preprocessing” vignette covers the formula-to-design pipeline, accepted predictor classes, argument types, attribute survival, terminology, and reproducible runtime claims. srr standards: 105 of 116 tagged in code, docs, and tests; the other 11 documented as not applicable.
  • Nine standards tests added (degenerate inputs, noise invariance, exact-fit behavior, row-name retention, runtime scaling).
  • CI gained the rOpenSci pkgcheck action (the editors’ submission checks, srr included) and a coverage workflow.

Corrected behavior

  • Rows dropped by na.action are reported with a count (a message(), so suppressMessages() silences it); fitting zero rows says so instead of blaming missing values.
  • Eleven duplicated error messages across the package were disambiguated with call context.
  • When a structural expression fails under the data2 mask (for example solve() of a singular matrix), the error now reports that cause instead of a misleading “not found” from the fallback lookup.
  • An unordered factor response to an ordinal family warns with the level order about to be used, then fits (the brms behavior); mo() keeps its stricter ordered-factor requirement.

frmtmb 0.29.0

Review fixes for the spatial wave and rOpenSci review preparation.

Breaking

  • spde() grouping values are now explicitly MESH ROW INDICES: integer, whole-number character, and integer-level factor spellings are accepted and land identically; any other labels error stating the contract. Previously a non-mesh-order grouping (integer node IDs included, which sort lexicographically) fit a silently permuted field with a wrong likelihood and no warning. Code that passed non-numeric node labels must now map them to row indices.

Corrected behavior

  • car()/spde() accept call-valued grouping variables (gr = factor(node)).
  • The Windows sequential-chains guard in frm_sample() also covers options(mc.cores =), which rstan reads as its default.
  • Optimizer error wrapping no longer claims a numerical cause for non-numerical errors; the underlying message leads.
  • The escar log-determinant is finite at rho near 1 (the adjacency eigenspectrum is clamped against floating-point overshoot).
  • The extreme-parameter heuristics in diagnose() and frm_sample() only test log-SD components and name the parameter; a car/bym2 mixing proportion at its boundary no longer trips a misleading singularity warning.
  • car() adjacency validation: NA entries and negative weights error informatively; one-sided dimnames work (the rownames-only path was broken); duplicate names error.
  • Non-finite covariance warnings fire once per fit instead of once per vcov() call.

rOpenSci preparation

  • Review infrastructure: CONTRIBUTING, code of conduct, CITATION, codemeta, Language field, return-value documentation, README currency fixes.
  • srr statistical-software standards: General and Regression categories scaffolded with 77 standards tagged at their code locations, 11 marked inapplicable with justifications, and the 28 remaining gaps recorded as the submission roadmap in the standards file. Test coverage measured at 87.6% (full suite).

frmtmb 0.28.0

Spatial GMRF grammar, the mclust covariance taxonomy, and the close-out of the fuzz tier.

New

  • Spatial conditional autoregressions with the brms spelling: car(M, gr = g, type = "escar"/"esicar"/"icar"/"bym2") over an adjacency matrix, with analytic log-determinants throughout (no on-tape factorization) and brms’s per-component soft sum-to-zero constraint, whose distance to the hard-constrained ML is measured and documented (4e-4 logLik at the default, shrinking quadratically). Validated against hand-rolled direct ML to 1e-11.
  • spde(fem, gr = node): an SPDE-Matern field from fmesher/INLA FEM matrices, Q(kappa, tau) assembled on the tape from fixed sparse matrices; sd and range reported through the planar identities.
  • gr(g, prec = Q) supports correlated slopes (sparse Kronecker precisions; agrees with the dense gr(cov = solve(Q)) equivalent to 0 ulp).
  • mixture_mvn(K, D, model =) gains the mclust covariance taxonomy (EII, VII, EEI, VEI, EVI, VVI, EEE, VVV) with per-model parameter templates; matches mclust::Mclust to 1e-12 log-likelihood and 1e-15 posteriors on intercept-only fits, while keeping covariate-dependent means, which mclust cannot fit.
  • anova(refit = TRUE) refits REML models with ML for comparison (warm-started, with a message naming what was refit); getME() on the lme4 generic with the commonly consumed vocabulary.

Corrected behavior

  • Non-finite covariance matrices warn at vcov()/summary() and at fit time under se = TRUE, pointing at diagnose() (a positive-definite Hessian can still be numerically singular to invert; previously silent NaNs).
  • Optimizer failures recover automatically where a better start provably exists (the plain-Laplace optimum under profile = TRUE; a cold start when a warm start is itself the failure), and every optimizer error carries the model label and remedies instead of a bare RTMB message.
  • One quadrature configuration is rescued by displaced calibrations; the six irrecoverable thin-data configurations refuse with messages naming the configuration and the guaranteed quadrature = FALSE fallback. The fuzz tier runs green.
  • frm_sample(cores > 1) on Windows falls back to sequential chains with a warning instead of failing incomprehensibly (RTMB tapes and objective closures cannot reach socket workers; upstream kaskr/tmbstan#27). pp_check() and as_draws() dispatch without attaching bayesplot or posterior (frmtmb ships its own generics, dual-registered).

Notes

  • dev/benchmarks.md records the RTMBp (parallel RTMB) measurement: no gain on Cholesky-dominated mixed models, 1.7-2x on accumulation-dominated GLMMs at 4-16 threads; not adopted.
  • dev/feature-gaps.md records the ODE roadmap (RTMBode) and the Suggests + Additional_repositories packaging plan for non-CRAN extensions.

frmtmb 0.27.0

Fixes from a full review of the v0.22-v0.25 waves.

Corrected behavior

  • cens() combined with trunc() produced a silently wrong likelihood: the censoring contribution was not restricted to the truncation window (right-censoring now contributes (F(ub) - F(y))/Z), which inflated the dispersion by ~14% on the reference problem. The corrected objective matches a hand-rolled windowed likelihood to 1e-8, collapses exactly to the old form without truncation, and the cens+trunc OSA residuals recalibrate to the analytic PIT (5e-14).
  • residuals(type = "deviance") under se() now weights each row’s unit deviance by its own variance (the glm prior-weight form) instead of treating a common dispersion as shared.
  • predict(se.fit = TRUE) on quadrature fits no longer dies with a non-conformable error: it reports mode-conditional standard errors with a warning. Singular joint precisions in the predict path now degrade like vcov() does, and the shared solver uses Matrix::solve (strictly more robust for ill-conditioned but invertible GLMM precisions).
  • frm_sample() chain inits are clamped inside any bounds passed to Stan, with a warning naming the parameters when the ML mode itself violates a bound (previously an incomprehensible rstan error).
  • vint()/vreal() variables are required in newdata; a custom family’s prediction previously returned a length-0 vector with no message when they were missing.
  • Character censoring codes like “0”/“1”/“-1” decode (the error message had advertised them).

Compatibility registry and fuzzer

  • The registry’s precedence is redesigned (lexicographic comparison of sorted side specificities, with a validator that forbids file-order ties except documented overrides). 488 of 3750 resolutions were corrected against probed reality, including 429 pairs whose covstruct conditions a family-level rule had erased, and the newly recorded caveat that spatial structures over a plain factor silently use level order as coordinates. frm_compat() accepts vectors and errors on empty input.
  • The fuzz tier’s invariants were strengthened (a previously unfailable interval check replaced by the Wald identity plus parameter-coverage tallies; per-row simulate agreement; grammar divergences asserted; convergence demotion keyed to actual convergence verdicts after fixing a truncation that discarded them). The strengthened tier reproduces only known findings.

frmtmb 0.26.0

Pooled model comparison across imputations and the diagnostics/UX backlog.

New

  • anova() on frm_multiple() fits pools nested-model tests across imputations: D1 (multivariate Wald), D2 (chi-square combining), and D3 (Meng-Rubin likelihood pooling, with the plug-in leg evaluated by re-taping each imputation’s objective at the pooled parameters - no refits). D1/D2 match mice to 1e-7 on an exactly-shared reference; D3 is validated against the Meng-Rubin formula directly, since mice::D3’s fix.coef variant is not the plug-in statistic. Includes an ARIV clamp and a Reiter-df fallback for a small-m case where mice returns NaN.
  • cbind(successes, failures) binomial responses are accepted (the glm/lme4/glmmTMB spelling), rewritten internally to successes | trials(successes + failures); bit-identical to the trials() form.
  • simulate() returns ordinal draws as ordered factors with the original levels and multinomial draws as count matrices (both families previously had no simulator), and respects na.exclude padding.
  • frmtmb_control(check_nlev_1 =, check_olre =): lme4-style warning/ignore/stop vocabulary for one-level grouping factors and gaussian observation-level random effects (the se()-based meta-analysis idiom is recognized and not flagged).
  • diagnose() adds a complete-separation heuristic, predictor-scale warnings pointing at autoscale, and an isSingular-style verdict independent of the Hessian; it also no longer errors on fits without random effects.

Corrected behavior

  • Models with zero free outer parameters fit degenerately instead of dying inside nlminb (fixing latent empty-sdreport and empty-gradient bugs found along the way).
  • A nonlinear-parameter name colliding with a data column errors instead of silently shadowing the column; nonlinear fits that fail from default zero starts name start = in the error.
  • REML anova() compares fits whose fixed-effect designs span the same column space (term reordering included) instead of refusing every REML pair; genuinely different designs and REML/ML mixes still refuse with the reason.
  • Offsets in distributional-parameter formulas were verified correct (to 1e-13) and are now regression-tested against the silent-drop failure mode reported upstream (glmmTMB#625).

frmtmb 0.25.0

Simulation-workflow ergonomics, the last deferred method-surface items, and CRAN readiness.

New

  • frm_simulate() accepts natural-scale parameter names - the same vocabulary as hypothesis()/variables(): coefficients by name, sd_<group>__<term>, cor_<group>__<t1>__<t2>, response-scale sigma - inverted to the internal parameterization through the covariance registry (us/diag/homdiag/smooth/gr structures; others refuse by block name). The internal spelling still works and is byte-identical.
  • Prior-predictive simulation, the sample_prior = "only" analog: frm_simulate(..., priors = set_prior(...)) draws a fresh parameter vector per replicate on each prior class’s documented scale (natural SDs for class sd, truncation by rejection) and attaches the drawn parameters for prior-predictive checks. Every coefficient and SD must be pinned by a prior or newparams; omissions error instead of silently becoming zero effects.
  • simulate(censored = TRUE) applies the fitted type-I censoring mechanism to the draws; the default remains the latent uncensored response, which is also brms’s posterior_predict convention (verified against brms source) and is now documented.
  • residuals(type = "deviance") across the GLM family set (gaussian, poisson, binomial/bernoulli, Gamma, exponential, inverse.gaussian, nbinom1/2, geometric, beta, tweedie), exact against stats::glm where glm offers them; families without a standard unit deviance refuse by name. deviance() itself stays -2 logLik (lme4 convention).
  • predict(type = "response", se.fit = TRUE) now works for non-identity-mean families (zi/hurdle, lognormal, trials-binomial, truncated responses) through a joint delta method across all dpar linear predictors, including cross-dpar covariance (agrees with glmmTMB’s response-scale standard errors to 1.2e-5 on a zero-inflated poisson mixed model).

Corrected behavior

  • conditional_effects(method = "predict") evaluates addition terms on the effect grid, so its bands respect trials() and trunc() (binomial bands are counts in [0, n], not Bernoulli 0/1); aterm variables must be pinned in conditions, and the error names the variable.

CRAN and infrastructure

  • Heavy reference-validation test files are skip_on_cran()-gated: the CRAN-condition suite drops from ~128s to ~72s on the development machine while CI (NOT_CRAN=true) keeps full coverage.
  • nlme added to Suggests (a test uses nlme::Soybean as reference data; CI’s --as-cran unstated-dependency check halts without the declaration).
  • Benchmark verdict recorded in dev/benchmarks.md: optimParallel is not adopted - with exact AD gradients its concurrency caps at two evaluations (measured 1.03-1.22x end to end, slower with cold clusters), RTMB tapes cannot ship to PSOCK workers, and 100% of InstEval’s optimization time is inside the taped objective and the inner sparse Cholesky. frmtmb_control(profile = TRUE) remains the measured lever for many-coefficient models (1.6x there).

frmtmb 0.24.0

The quadrature and OSA defect clusters surfaced by the fuzzer and compatibility registry.

Corrected behavior

  • quadrature = TRUE is rebuilt around one root cause: TMB’s Gauss-Kronrod marginalization calibrates each integrand’s rescaling once, at the parameter values in hand when the tape is built, and frmtmb taped at the cold start. A plain Laplace fit now runs first and the quadrature tape is built at its optimum. This fixes: conditional modes returned as NA (ranef()/fitted()/predict() were silently NA for all groups but the first, whose slot held a wrong value) - modes now come from the Laplace inner solve and match glmer(nAGQ = 25)’s ranef() to 3e-5; and bare “NA/NaN gradient evaluation” crashes for poisson/Gamma/Beta with nested or even single scalar blocks - all now fit with gradients < 3e-4.
  • quadrature combined with trunc() produced logLik = +Inf as a successful fit; the combination is refused (the CDF difference underflows at quadrature nodes; a stable fix needs log-CDF forms). mixture() with REML or profile = TRUE is refused: both Laplace-expand the mu coefficients around a single mode, and a mixture likelihood is permutation-multimodal in exactly those coefficients. mixture() with quadrature remains supported.
  • residuals(type = "osa") on censored fits was broken (raw LAPACK singularity or NaN for every censored row). Uncensored rows now get calibrated one-step residuals by conditioning on the censoring window and renormalizing the one-step CDF to it (matches the analytic conditional PIT to 7e-9); censored rows are NA (an event has no one-step CDF), and row-varying censoring points refuse. Note simulate() draws the latent uncensored response under cens(), so DHARMa is not a substitute there (documented).
  • residuals(type = "osa") on ordinal fits crashed inside the OSA machinery; the ordinal log-densities now handle oneStepPredict’s taped-observation objects (exact Lagrange-basis category indicator), and all four ordinal families produce calibrated residuals (cumulative matches the analytic randomized-quantile residual to 4e-14).
  • A singular joint precision under REML or profile = TRUE no longer throws a raw LAPACK error from vcov()/summary()/confint()/ hypothesis(): it degrades to NaN standard errors with one warning pointing at diagnose(), matching the ML branch.
  • The brms-migration vignette documents that binomial() without trials() means Bernoulli here (glm convention) where brms requires trials() or bernoulli().

The fuzz tier’s open findings drop from 28 to 16 on the identical plan; most of the remainder are now informative refusals on thin-data edge cases rather than defects.

frmtmb 0.23.0

Defect wave driven by an open-issue sweep of brms/lme4/glmmTMB, plus a feature-compatibility registry.

Corrected behavior

  • Truncation now reaches the whole post-fit surface, not just the likelihood: fitted(), predict(type = "response"), and residuals() report the truncated mean E[Y | lb <= Y <= ub] (closed forms for all six CDF families, validated to 1e-15); simulate(), posterior_predict(), and frm_simulate() rejection-sample within bounds; newdata re-evaluates variable bounds. residuals(type = "osa") on truncated (and untruncated gaussian) responses was also miscalibrated and now integrates over the truncated support (KS uniformity restored from p ~ 1e-15 to 0.76-0.97). Dpar-scale predictions stay untruncated by design. DHARMa/pp_check on truncated models are meaningful again.
  • (f || g) with a factor now yields independent per-level effects (the diag structure the syntax promises) instead of a silently fully correlated block; identical to an explicit diag(f | g). Numeric double-bars are unchanged. Existing factor-double-bar fits change, because the old ones were wrong (lme4 has the same open bug, lme4#818).
  • ar1()/hetar1() warn when the ordering factor’s integer levels have gaps: levels correlate by position (the glmmTMB reading, unchanged), so a gap counts as one step; the warning points at ou() over num_factor() for irregular spacing (glmmTMB#1278).
  • Predictions at non-estimable points of a rank-deficient design return NA with one warning naming the aliased columns, instead of silently returning the partial sum (predict.lm semantics; lme4#303). Collinear restatements of kept columns stay exact.
  • Grouping factors written as calls work: (1 | factor(x)), (1 | interaction(a, b)) (lme4#464).
  • A random-effect term crossed with * or : errors instead of being silently refit additively (lme4#196); mo()/mi() interaction multipliers must be numeric (brms#1828); anova() requires equal nobs (lme4#622).
  • frm_sample() chain initialization: chain 1 anchors at the ML mode, further chains are jittered on the unconstrained scale (init_jitter), restoring the overdispersion Rhat needs; a boundary-mode warning fires for singular fits; mixture posteriors are documented as needing init = "random".

New

  • Feature compatibility registry: frm_compat() answers what plays with what (works / conditional / refused / broken / untested) across 3750 feature pairs, and the new “Feature compatibility” article is generated from the registry at build time so it cannot drift. Known-broken pairs are listed there; the registry probing itself surfaced trunc x quadrature and cens x OSA as broken (queued for the next fix wave).
  • An audit of 559 currently open brms/lme4/glmmTMB issues is recorded in dev/test-backlog.md, including the pathologies frmtmb is structurally immune to.
  • A pairwise grammar fuzzer (env-gated: FRMTMB_FUZZ=true): a 310-spec covering array over the grammar with metamorphic invariants (predict/fitted identity, permutation invariance, simulator support membership, vcov sanity) and a brms make_standata structural oracle. Its first run surfaced the quadrature defect cluster now queued for fixing (conditional modes left NA, crashes with nested groups and Beta, +Inf logLik with trunc()); the tier reports those as failures until they are fixed.

frmtmb 0.22.0

Cross-validation against brms itself, closer brms compatibility, and fit ergonomics.

brms agreement suite

  • New test tier validating the elaborate grammar structurally against brms::make_standata()/brmsterms() without any Stan compilation (designs, RE structures, |ID| merging, ordinal thresholds, cs(), mo() codes, gp bases, smooths, addition terms, mvbf, nl, mixture naming; most exact to 1e-10 or better), plus an opt-in numeric tier (FRMTMB_BRMS_FIT_TESTS=true) showing fixed-effects estimates match the mode of brms’s own generated Stan program to 1e-4 and that our mo() parameterization is brms’s likelihood exactly (via rstan::log_prob). brms is in Suggests only.
  • Found by the suite and fixed: cs() predictor variables never reached the combined model frame, so bf(y ~ x + cs(z)) errored unless z already appeared elsewhere.

brms compatibility

  • Hilbert-space gp(x, k =) now uses brms’s input convention exactly: coordinates are rescaled by the largest pairwise distance (one shared factor across dimensions) and centered, so the boundary is L = c and the same gp() call is the same approximation in both packages (basis agreement with brms to 1e-16, tied coordinates and vector-valued c = included). This roughly doubles the effective boundary at a given c: accuracy against the exact gp improves about 25x at the same k (k = 40 now within 2e-4 logLik), and the default c = 1.25 is the right choice in 2-D as well. Reported range(gp) values stay in data units (exact log-shift back-transform); fitted lengthscales differ from pre-0.22 fits by the data-dependent scale factor.
  • cens() accepts brms’s character and factor codes ("left"/"none"/"right"/"interval", prefix-matched, case-insensitively); unknown labels and out-of-range numeric codes error informatively instead of coercing to NA.
  • gp()’s k/c/iso, rr()’s d, and se()’s sigma arguments now evaluate in the formula environment (brms behavior), so variables and expressions work; invalid values error with the offending expression named.

Fit ergonomics

  • frmtmb_control(verbose =) (and the frm(verbose =) shortcut): level 1 prints timed stage lines (parse, frame, tape, optimize, restarts, sdreport) through message() so a slow fit shows where it is slow; level 2 adds the optimizer’s own iteration trace. Bootstrap, influence, and allfit refit loops stay quiet.
  • The large-gradient warning now points to diagnose() and the new “Convergence problems” remedy ladder in the diagnostics vignette (scaling, restarts, optimizer comparison, profiles, boundary fits, and judging marginal gradients).
  • frm_allfit()’s nloptr optimizer produced an empty row: nloptr rejects callbacks that declare ... (RTMB’s obj$fn/obj$gr do), and the error was swallowed; separately, missing ftol_rel made NLopt report failure at the converged optimum. Both fixed; the agreement test now requires every optimizer to converge and match.
  • The brms-migration vignette documents how to recover the model function (the stancode() analog): the objective closure via build_objective(fit$frame), its joint-vs-marginal relationship to fit$obj$fn, and how to reproduce fit$obj.

frmtmb 0.21.0

Method-surface audit against lme4/glmmTMB/brms, plus fixes from a full code review.

Corrected behavior

  • predict(type = "response") now returns the expected response for every family (equal to fitted() in-sample), not the primary dpar’s natural scale; zi/hurdle, lognormal, and trials-binomial fits were affected. The glmmTMB aliases "conditional", "zprob", "zlink", and "disp" are accepted (validated against glmmTMB on a zero-inflated Poisson to ~8e-7). Note "disp" returns sigma on its natural scale, where glmmTMB returns the variance-scale dispersion for gaussian. se.fit for the mean of a non-identity-mean family is not yet available and errors with guidance.
  • rescor = TRUE now refuses cens(), trunc(), and se() addition terms, which the joint-gaussian likelihood silently ignored (wrong likelihood with no warning).
  • Bounds (lower/upper, set_prior lb/ub) under frmtmb_control(profile = TRUE) were positionally misaligned: a bound on a fixed coefficient could silently pin a covariance parameter instead. Bounding a profiled coefficient now errors; covariance-parameter bounds land on the right slot.
  • confint(), vcov(full = TRUE), and diagnose() labels were broken or misaligned on mi() fits: the latent imputation component was counted as an outer parameter.
  • frm_sample(priors = ) failed on fixed-effects-only fits of single-dpar families (a $ partial-match bug), and frm_sample(laplace = TRUE) was broken twice: the default init had the wrong length, and draw columns were mislabeled (a column named b[1] actually held a covariance parameter).
  • influence() tables and cooks.distance() now align with vcov() (fits with a constant dpar errored; column names are now the coefficient labels).
  • simulate() follows the stats seed contract: a "seed" attribute and a restored RNG state instead of clobbering the global stream.
  • A covariate literally named sigma is no longer shadowed by the residual sigma in hypothesis()/variables().
  • In predict(newdata = ), an NA in a variable used only in a random-effect design now propagates to the prediction instead of being silently zeroed (only genuinely new levels predict at the population value).

New methods

  • drop1() (AIC/LRT, marginality-aware; matches lme4::drop1.merMod to 1e-4), cooks.distance() directly on a fit, dfbeta()/dfbetas() on influence() results (stats sign convention; lme4 returns the negation), na.action(), and an lme4-style “Groups:” line in summary().
  • confint() accepts lme4’s "Wald" spelling and gains method = "boot" (percentile intervals via [frm_bootstrap()]).
  • vcov(full = TRUE) rows carry per-parameter names (glmmTMB convention).
  • predict() accepts the lme4/glmmTMB allow.new.levels dot spelling.
  • On draws objects: posterior_linpred() and ranef() (brms-shaped arrays).
  • emmeans::recover_data had been silently missing from NAMESPACE since v0.13 (orphaned export tag); emmeans support was broken.

Internals

  • One authoritative outer-parameter map shared by confint(), bounds resolution, vcov(full = TRUE), and sampling; graphics and grDevices added to Imports; duplicated formula/family coercion and gp position-key helpers unified; REML logLik df counts only outer parameters (documented; lme4 counts the integrated fixed effects too, so REML AICs are not comparable across packages).

frmtmb 0.20.0

  • mixture_mvn(K, D): multivariate gaussian mixture components for model-based clustering of an n x D matrix response (the mclust / clustTMB use case). Every class mean is a full linear predictor, so cluster means may depend on covariates and random effects; mixing weights are multinomial-logit dpars with their own formulas. Validated against a hand-rolled ML fit to 1.3e-10 and against faithful-data cluster recovery. Limitations (documented in ?mixture_mvn): class covariances are unstructured (us) and covariate-free - no mclust-style constrained covariance taxonomy (EII..VEV); cens()/trunc(), mvbf(), and simulate() are not supported.
  • Multi-dimensional Gaussian processes: gp(x1, x2, ...) takes up to three variables, with a separate lengthscale per dimension by default (the brms convention) or one shared lengthscale via iso = TRUE, in both the exact and the Hilbert-space (k =) form. The tensor HSGP basis is capped at k^D <= 1000 columns. confint_varcorr() reports one range row per dimension. Validated against a direct 2-D ML fit to 3.8e-11.
  • Kriging prediction for the exact gp(): predict(newdata = ) at positions not seen at fit time now returns the GP conditional mean, and se.fit = TRUE adds the conditional variance, instead of erroring. Kriging mean validated to 1.7e-15 and the standard error to the exact decomposition identity. The Hilbert-space form already interpolated through its basis.
  • frmtmb_control(sparse_x = TRUE): sparse fixed-effect design matrices (Matrix::sparse.model.matrix), the analog of glmmTMB(sparseX = ), for models where a many-level fixed factor dominates memory. Estimates are identical to the dense path (gradient agreement 3e-14; 13.8% lower tape memory in the validation model); model.matrix() on the fit returns a sparse matrix. Prediction falls back to the dense builder for newdata containing NA factor rows, which sparse.model.matrix would silently zero.
  • frmtmb_control(autoscale = TRUE): internal standardization of badly scaled continuous predictors (the lme4 >= 1.1.37 feature): a scaled pre-fit is mapped back exactly and warm-starts the reported fit, parameter magnitudes feed nlminb’s scale hook, and convergence and standard-error Hessian checks run in scaled units. Turns 12 scaling warnings into 0 on the validation problem with a 0.0 log-likelihood difference against a manually standardized reference. Reported results are always on the original scale.
  • frm_multiple() pooling extensions: $pooled_varcorr pools random-effect SDs and correlations across imputations on transformed scales (log for SDs and GP ranges, Fisher z for correlations) with Barnard-Rubin degrees of freedom, and hypothesis() on a frmtmb_multiple object pools delta-method hypothesis estimates by Rubin’s rules with t-based intervals. Fixed-effect pooling matches mice::pool() to 2.1e-6. Likelihood-ratio pooling across imputations (mice’s D3) is not implemented; anova() on pooled fits remains per-fit.

frmtmb 0.19.0

  • Latent classes now combine with continuous random effects (growth-mixture models): mixture(..., groups = ~g) accepts random effects, smooths, and gp() terms in the component formulas. The class sum is computed conditional on the latent effects, so one Laplace approximation integrates them - the sum-and-integral swap makes this exact as an integrand identity, and even crossed random effects are structurally fine. Random effects written in a component formula are class-specific by construction. Accuracy: the Laplace approximation of the class-mixture integrand carries a small documented bias (about 0.1 log-likelihood units in the validation problem, with parameter estimates matching the exact closed-form marginal to 0.01); quadrature = TRUE is numerically exact when the per-group integrand is univariate (validated to 5e-3) and approximate when class-specific intercepts couple. simulate() now draws one class per group and then simulates each observation from its group’s component; mixture_probs() gives empirical-Bayes classification conditional on the modes.
  • variables() (brms spelling): lists every parameter name usable in hypothesis() expressions (coefficients, sd_/cor_ summaries, sigma); on frm_sample() output it lists the draw columns.
  • get_prior() (brms spelling): enumerates every slot set_prior() can target (class/coef/dpar/group rows), from a formula plus data or from a fit; the default in every slot is flat.
  • README rewritten for the current scope, with related-work pointers (glmmTMB, BayesRTMB); getting-started vignette extended to the full grammar; SPEC.md status and deviations brought current.

frmtmb 0.18.0

The last three roadmap features.

  • gp(x) Gaussian-process terms: exact (a dense squared-exponential block over the unique positions, with a standard 1e-6 nugget) or the Hilbert-space approximation via gp(x, k = 30) (sine basis with spectral-density prior SDs), in any linear predictor. Exact fits match direct GP marginal ML; the approximation converges to the exact answer and predicts at arbitrary positions (the exact form predicts at observed positions only). confint_varcorr() reports sd(gp) and range(gp).
  • mo() and mi() interactions: mo(x):z, mo(x)*z, mi(x):z, mi(x)*z with numeric multipliers; mo() interactions share their variable’s simplex (brms convention). Both validated exact against direct ML (the mi() interaction stays linear in the latent value, so the closed-form marginal still applies).
  • Group-level latent-class mixtures: mixture(..., groups = ~g) sums the class assignment per group (growth-mixture / latent-class regression; the tractable nested case of brms#1905). Exact against direct ML; mixture_probs() returns posterior class probabilities per group, or per observation for ordinary mixtures. Restrictions: no random effects or smooths alongside, mixing predictors evaluated at each group’s first row, no simulate() yet. Crossed-design group mixtures remain out of the Laplace class.

frmtmb 0.17.0

Clearing the roadmap: mixtures, measurement error, and the remaining grammar gaps.

  • mixture(fam1, fam2, ...) finite-mixture families: each component keeps its own suffixed dpars (mu1, sigma1, …), mixing proportions are multinomial-logit dpars with their own linear predictors (mixture-of-experts comes free), and the likelihood is a branch-free logsumexp - so random effects and smooths work in any component formula. Exact against direct ML for gaussian and poisson mixtures. Component means initialize on spread response quantiles; the usual finite-mixture multimodality caveats apply (frm_allfit(), or order intercepts via bounds).
  • mi(sdx) measurement error (brms me()): known per-observation measurement SDs make every true value latent, with the observed values entering through a measurement model. Exact against the closed-form bivariate-normal marginal; attenuation bias is corrected and NAs (missing + mismeasured) combine.
  • cs() category-specific effects for sratio, cratio, and acat (refused for cumulative, as in brms). Exact against direct ML.
  • equalto(x + 0 | g, V) covariance structure: fully fixed known V, zero parameters (meta-analytic sampling covariances).
  • rr() fits now support se.fit: the delta method routes the factor columns through the loadings Jacobian and adds loading-parameter columns; verified parameterization-invariant against the full-rank-vs-us() equivalence.
  • Registered insight methods (find_formula, find_random, get_parameters, get_varcov, find_statistic, link accessors): insight::is_mixed_model() and the easystats accessor layer now see the random-effect structure.

frmtmb 0.16.0

The two deferred architecture features.

  • rr(x | g, d = k) reduced-rank / factor-analytic covariance (glmmTMB’s GLVM structure): per-level coefficients are loadings times iid standard-normal factors, so b holds the factors while the design matrices span the coefficient space. Matches glmmTMB exactly and reduces to us() at full rank. se.fit on rr models is deferred (the loadings enter the coefficients nonlinearly); predict, simulate, ranef, and VarCorr (the rank-k covariance) all work.
  • mi() one-step imputation for continuous predictors, reversing the original exclusion: bf(y ~ mi(x) + z) + bf(x | mi() ~ z) turns missing x values into latent parameters that Laplace integrates (exactly, in the linear-gaussian case - validated against the closed-form marginal likelihood). Imputation uncertainty propagates into the coefficient standard errors automatically. Rows with NAs in non-mi() variables still drop; imputation models must be gaussian or student; discrete predictors remain impossible (as in Stan). me() measurement error is the natural follow-on.

frmtmb 0.15.0

Tier-2 sweep of dev/feature-gaps.md plus a method surface for sampled fits.

  • mo() monotonic effects (brms syntax): scale coefficient times an estimated simplex, exact against direct ML; works in prediction, conditional_effects(), and with ordered factors. No frequentist package offers this. Standalone additive terms only for now.
  • Sequential ordinal families sratio, cratio, acat, validated against direct ML and collapsing to logistic regression at K = 2.
  • Covariance structures: hetar1, homcs, homtoep (glmmTMB parameterizations, exact where glmmTMB converges) and spatial exp(), gau(), mat() over num_factor(x, y) coordinates (Matern uses bounded internal transforms for stability where both we and glmmTMB otherwise diverge).
  • Fixed a latent parser bug: exp(x) (or any covariance-structure name) used as a plain function in a fixed formula was silently stripped by the formula splitter - since v0.1. Such calls are now protected automatically.
  • influence(fit, groups = ) and cooks.distance(): case-deletion diagnostics over warm-started refits.
  • frm_multiple(): fits across multiply-imputed datasets (list or mice::mids) pooled by Rubin’s rules with Barnard-Rubin df.
  • conditional_effects() gains method = "predict" (prediction intervals) and data-frame conditions (one condition set per row, brms style).
  • vint()/vreal() addition terms pass arbitrary data vectors to custom families (brms custom-family convention). The test suite includes a full Wiener drift-diffusion model written as a custom_family() in plain R - the workflow brms needs raw Stan code for.
  • Method surface for frm_sample() draws: summary() (with Rhat/ESS), fixef(), VarCorr() (natural scale), hypothesis() (exact posterior version), posterior_epred() / posterior_predict() (each draw runs the full prediction machinery), pp_check(), and posterior::as_draws().
  • Examples added across the reference (run by R CMD check).

frmtmb 0.14.0

Gap-closing milestone from a sweep of the lme4 / glmmTMB / brms vignettes (dev/feature-gaps.md).

  • se() addition term: known per-observation sampling SDs (meta-analysis), gaussian and student. se(x) fixes the residual SD (sigma is mapped out); se(x, sigma = TRUE) adds an estimated sigma in quadrature. With (1 | obs) this is random-effects meta-analysis, and with gr(g, cov = A) the phylogenetic version.
  • Binomial (and beta-binomial) responses may be proportions of trials(), the glm/glmer idiom, converted to counts internally.
  • New families: bernoulli, geometric, exponential, weibull (mean-parameterized, matches survreg’s likelihood), shifted_lognormal, hurdle_gamma, hurdle_lognormal, zero_inflated_binomial, zero_inflated_beta (validated against glmmTMB), and asym_laplace for quantile regression (fixed quantile dpar reproduces quantreg::rq estimates).
  • ranef(condVar = TRUE): conditional SDs of the modes (TMB/glmmTMB convention: fixed-effect uncertainty propagates), plus tidy as.data.frame() methods for ranef and VarCorr output.
  • frm_allfit(): refit under every available optimizer (nlminb, optim, bobyqa, NLopt) and compare, the lme4 allFit() analog.
  • frm_simulate(): de novo simulation from a bf() formula and supplied parameters with no fitted model (power analysis; the glmmTMB simulate_new() analog).
  • frmtmb_control(profile = TRUE) (experimental): profile the primary-coefficient vector into the inner Laplace problem, the glmmTMBControl(profile = TRUE) / glmer(nAGQ = 0) speed approximation for many-coefficient models; covariance comes from the joint precision.
  • anova() no longer fails on models sharing a primary formula (distributional vs plain fits); row labels now include dpar formulas.

frmtmb 0.13.0

Method-surface (“sugar”) milestone: the conventional S3 methods that downstream packages and user muscle memory dispatch on.

  • New accessors: sigma(), terms(), weights(), model.matrix(), deviance(), extractAIC() (enables step() / drop1()), ngrps(), prior_summary(), and a profile() method wrapping TMB::tmbprofile().
  • Breaking: coef() now follows the lme4 / glmmTMB / brms convention (per-group coefficients: fixed effects broadcast over levels plus the conditional modes). Use fixef() for the fixed effects alone. Fits without random effects still return the coefficient vector.
  • conditional_effects() with a base-graphics plot() method: grid predictions per predictor (or "x:z" pair) with Wald bands, smooths included, matrix covariates held at column means.
  • plot(fit): Pearson-residual diagnostics (residuals vs fitted, normal QQ).
  • pp_check() (registered on the bayesplot generic): predictive checks from simulate() draws through any ppc_* function.
  • hypothesis(): tests of arbitrary expressions of the parameters, brms spelling, with three methods: delta-method Wald (default), "profile" (profile-likelihood intervals via TMB::tmbroot() lincombs; linear hypotheses, ML fits), and "boot" (parametric bootstrap percentile intervals; any expression). The expression environment includes natural-scale random-effect names (sd_<group>__<term>, cor_<group>__<t1>__<t2>, sigma), so ICC inference is hypothesis(fit, "sd_g__Intercept^2 / (sd_g__Intercept^2 + sigma^2)", method = "boot"). The result is a frmtmb_hypothesis data frame carrying the bootstrap draws or profile curves in attributes, with a plot() method (bootstrap histogram, profile curve, or the implied Wald normal, per hypothesis).
  • frm_bootstrap(fit, FUN, nsim): parametric bootstrap over refit()s (the bootMer analog), with confint() percentile intervals.
  • refit(fit, newresp): refit to a new response reusing the assembled design with a warm start - one re-tape plus optimization; the engine for parametric bootstrap over simulate() draws.
  • The accessor set makes insight’s default methods work (find_response(), get_data(), get_sigma(), n_obs(), …); dedicated insight methods (random-effect formula parts, get_parameters()) are a roadmap item.

frmtmb 0.12.0

Pre-release consolidation toward CRAN. Highlights by milestone:

Model grammar (brms-compatible spelling)

  • bf() with distributional-parameter formulas carrying the full predictor grammar (random effects and smooths in any dpar), constant dpars, and addition terms weights(), trials(), cens() (left / right / interval), trunc() (with the inclusive discrete correction).
  • Nonlinear formulas (nl = TRUE) with a full linear predictor per parameter.
  • Multivariate models: mvbf() / bf() + bf() / mvbind(), a family per response, gaussian residual correlation (set_rescor()), and |ID| random-effect correlation across formulas.
  • mgcv smooths s() / t2() in any linear predictor, including matrix-covariate summation-convention terms: scalar-on-function, function-on-scalar, and function-on-function regression validated against mgcv.
  • Covariance structures: us, diag, homdiag, cs, ar1, toep, ou (positions via num_factor()), and known-structure terms gr(g, cov = A) (dense, with correlated slopes via a Kronecker product) and gr(g, prec = Q) (sparse GMRF).

Families

gaussian, poisson, binomial, Gamma, lognormal, student, negbinomial, nbinom1, beta, tweedie, compois, beta_binomial, skew_normal, inverse.gaussian, exgaussian, zero-inflated and hurdle counts, cumulative ordinal, matrix-response multinomial, and custom_family() as a plain R log-density with check_custom_family() AD verification.

Estimation and inference

  • ML and REML by Laplace approximation; quadrature = TRUE upgrades scalar random effects to adaptive Gauss-Kronrod marginalization (matches glmer(nAGQ = 25) and GLMMadaptive).
  • MAP / regularized ML via brms-style set_prior(); hard bounds (lower / upper, and per-prior lb / ub).
  • sdreport is deferred until standard errors are first needed (roughly a quarter off fit time; se = TRUE restores eager mode).
  • Pluggable optimizers (frmtmb_control(optimizer = )), including arbitrary user functions.
  • confint() (Wald / profile / likelihood-root), natural-scale confint_varcorr(), anova() likelihood-ratio tests, diagnose().

Diagnostics and ecosystem

  • Residuals: response, pearson, and one-step-ahead (type = "osa").
  • DHARMa simulation-based residuals (dharma_residuals()).
  • NUTS on the fitted objective: frm_sample() (ML-mode initialization, named draws, priors, bounds) and check_laplace(); bayesplot works on the draws.
  • emmeans and marginaleffects support, both verified against glmmTMB.

Verification

Roughly 500 tests compare fits against glmmTMB, lme4, mgcv, MASS, survival, nnet, GLMMadaptive, and hand-written RTMB references, plus an edge-case suite mined from the lme4 / glmmTMB / brms issue trackers (dev/test-backlog.md).