gllvmTMB 0.7.1 (release candidate)
This candidate is a narrow trust-release closure. It adds no new response family, likelihood, integration engine, random-slope capability, iSDM route, or broad predict(newdata = ) claim. No CRAN submission, tag, or public release accompanies this candidate.
Changed
Integrated-SDM point prediction now preserves fitted source-observation effects on prepared
newdata. The publicgllvmTMB(..., family = isdm_sources(...))route reconstructs each source formula from its fitted terms, factor levels, contrasts, and retained columns. Prediction grids no longer need a dummy response column. Unknown sources, missing source covariates, unseen factor levels on rows where that source is active, and ambiguous encoded source-column names fail with typed errors; an all-missing declared source-by-trait arm is also refused before fitting. This is a point-prediction and safety repair, not a recovery promotion: held-out spatial accuracy and calibrated map intervals remain unverified, spatial slopes remain outside the admitted prediction tier, and users continue to supply their own preparednewdatagrids.Public response-column coefficient models.
column_coef()fits IID response-column random intercepts and slopes.phylo_coef()usesK_rho = rho K + (1-rho) diag(K)with fixed numericrhoor one estimated interior value;animal_coef()uses the same covariance-scale mixture from exactly one pedigree, relationship covarianceA, or precisionAinv, with fixed numericrho;kernel_coef()uses one labelled dense kernel with fixed numericrhoor one estimated interior value, retaining its supplied marginal scale; andspatial_coef()uses one labelled response-column mesh with fixedrho = 1, jointly estimating the projected-SPDE range and the coefficient-basis covariance. All five use the ordinarygllvmTMB()entry point with long ortraits(...)wide data, andextract_Sigma(level = "column_coef")returns the ordered coefficient covariance, source, andrho. This is Gaussian point estimation only: intervals, non-Gaussian responses, and spatial IID mixtures remain unavailable. The existing*_slope()family remains current, warning-free, and non-deprecated. Exact compatibility is preserved at the no-intercept dense-vcv,rho = 1endpoint through the releasedphylo_slope()conditioningK + 1e-8 I; the matching no-intercept dense-Aanimal endpoint inheritsA + 1e-8 Ifromanimal_slope(). Other coefficient routes use the raw covariance-scale mixture. The no-interceptkernel_coef(..., rho = 1)endpoint is exactly the releasedkernel_slope()route and uses rawKwithout either ridge. The matching no-interceptspatial_coef(..., rho = 1)endpoint is exactly the releasedspatial_slope()projected-SPDE route. Intercept-bearing spatial fits add an all-ones coefficient-basis column without changing the spatial source normalization.Predictor-informed latent axes now compose across registered native response families. One complete-response ordinary unit-tier
latent(..., lv = ~ x)block may combine registered family/link rows. The loadings-onlyunique = FALSEform admits ranks through the number of logical responses; the ordinary automaticPsiform additionally requires that its free loading and diagonal parameters do not exceed the available covariance moments. The existing cross-family correlation route is unchanged; the upgraded article now shows that same fit reporting both shared correlations and rotation-invariantB_lv = Lambda alpha^Teffects. Gaussian and lognormal responses use separate within-family residual-scale slots when they coexist because one is on the raw scale and the other onlog(y). Live canaries cover every registered native family, but broad arbitrary-composition recovery and interval calibration remain partial. Missing responses or LV predictors, fixedX + X_lv, REML, extra covariance tiers, and structured-sourcelvremain unsupported.Added an evaluated guide to predictor-informed latent ecological axes. The guide teaches the ordinary native Gaussian route through the rotation-invariant trait-scale effect
B_lv = Lambda alpha^T, alongside the decomposition of each latent score into its predictor-informed mean and innovation. Existing interval evidence remains limited to the named native Gaussian cells and rank-1 multi-trial binomial logit, probit, and cloglog cells. The Julia bridge remains a complete-response, loadings-only point route with optional uncalibrated Wald plumbing; broader families, masks, tiers, REML, profile, and bootstrap inference are outside this guide.Explicitly unused optional grouping slots now warn (#1190). Supplying
unit_obsorclusteris useful only when a covariance keyword consumes that column. If no keyword does,gllvmTMB()now warns and tells the caller to omit the slot or use its column in the intended keyword. Omitted and explicit-NULLslots remain silent; the warning does not change the model, likelihood, or parameterisation.extract_Sigma_B()andextract_Sigma_W()remain soft-deprecated compatibility wrappers (#1194). They stay exported and retain their historical return names. Their help now gives the direct migration toextract_Sigma(fit, level = "unit")orextract_Sigma(fit, level = "unit_obs"); ordinary package summaries and residual helpers do not emit the wrapper warning.Variational approximation remains an opt-in experimental route (#1189). Native Laplace remains the default. This candidate makes no VA calibration, standard-error, confidence-interval, low-prevalence, or MSPL claim.
gllvmTMB 0.7.0 (development history retained for provenance)
Response-column slope helpers.
slope(),phylo_slope(),animal_slope(),kernel_slope(), andspatial_slope()support Gaussian long-format, predictor-only slopes when the RHS is the resolved response column. Wide-data syntax, non-Gaussian column slopes, and uncertainty intervals for the predictor covariance remain deferred.extract_Sigma_B()/extract_Sigma_W()now warn on call (#1194). The wrappers stay exported and still return the historicalSigma_B/R_BandSigma_W/R_Wnames. Callers of the old spelling now seelifecycle::deprecate_soft()pointing atextract_Sigma(fit, level = "unit")/level = "unit_obs". Runtime warning only; no unexport.The augmented-LHS parser guard now honours a non-default
trait =column (#1188)..assert_no_augmented_lhs()decided whether a bare covstruct keyword (latent,unique,indep,dep,scalar,spatial_dep) was written in the supported per-trait-intercept form0 + trait | gby comparing the LHS symbol against the string literal"trait", never consulting the resolvedtrait =argument. A user withtrait = "variable"writing the CORRECT long-format speclatent(0 + variable | unit, d = K)was falsely rejected as an unsupported augmented LHS, and was left with no way to write per-trait intercepts in long format under their own column name. The guard now accepts either the resolved trait column name or the literal"trait", and its abort message names the user’s own trait column. ⚠️ This blocked an external user from writing a correct model and led to a wrong published analysis (the fallback spec collapsed 29 per-item intercepts to one shared intercept). On the reporter’s direct refit, the d = 2 LV1-loading/prevalence association fell from R² = 0.742 with the shared-intercept specification to R² = 0.125 with per-item intercepts. Thanks to @iwogross for the report.gllvmTMB()’sunit_obsandclusterarguments now default toNULLinstead of the concrete strings"site_species"/"species". Both slots are optional – most callers have no reason to supply them – but a default that reads as a required column name invites callers to manufacture columns just to satisfy the signature. An external systematic map user (paper x item design, no sites, no species) wrotesite_species = paste(study_ID, variable, sep = "_")purely becauseunit_obsdefaulted to"site_species", and setcluster = "variable"; neither column had any meaning in the design or any effect on the fit. ⚠️ API/signature change; fitting behaviour preserved. The visible defaults are nowNULL, while omitted and explicit-NULLcalls still resolve internally to"site_species"/"species". Existing calls that relied on or explicitly supplied those historical names fit bit-identically. Also fixed: the “Column %s not found in data” error, which never said which argument wanted the column, now names the argument (trait,unit, orunit_obs) and the value that was looked for.mesh=with no spatial term is no longer silently ignored (#1165). Mesh validation used to run only when aspatial_*()term was present, sogllvmTMB(mesh = <raw fmesher mesh>)with an ordinary non-spatial formula fitted identically to the no-mesh call – a clean converged fit, and no signal that the spatial mechanism never ran. ⚠️ Behaviour change: a supplied mesh and no spatial term now warns, naming both possible mistakes (the formula is missing aspatial_*()term, ormeshwas left over from a term that was removed). A raw fmesher/INLA mesh is then rejected with the existingmake_mesh()error – the check that already ran when a spatial term was present. A validmake_mesh()object still fits as a non-spatial model after the warning. Fits with a spatial term, and fits withmesh = NULL, are unchanged.predict(newdata = )now also re-adds ordinary(1 | group)random intercepts (#1138). This corrects an earlier entry, which recorded there_inttier as unreachable because “its group mapping is not a top-level field on the fit”. That was wrong –fit$re_intcarriesgroups,n_groupsandoffsets, which is exactly what is needed. The tier is now reconstructed and held to the same acceptance test as the others:predict(newdata = training rows)reproducesreport$etaexactly, including with several(1 | g)terms, which is the case that exercises the end-to-end packing viare_int_offsets– an off-by-one there would give a plausible value for the first term and a silently wrong one for the second. An unseen level in a grouping factor still aborts, as it did before: for an arbitrary grouping factor there is no defined fixed-effects fallback, so refusing beats guessing.predict(newdata = )no longer requires the response column (#1154). A prediction grid has no response by construction – that is the point of predicting on one – but the newdata path built its design matrix withmodel.matrix()on the two-sided model formula, which constructs amodel.frame()first and so evaluates the left-hand side. Predicting on a grid failed withobject '<response>' not found, and the workaround (add a dummy column) was undiscoverable from the error and left the reader wondering whether the placeholder affected the prediction. The design is now built from the right-hand side only. Predictions are unchanged where the column was present.predict()andfitted()now return the family/source column on mixed-family fits, and the effort-free scale is documented (#1133). On anisdm_sources()fit theestcolumn mixes scales by design – Poisson expected counts beside cloglog detection probabilities – and the in-sample path returned no column saying which row was which. Thenewdatapath always did (it returns all ofnewdata), so the default call, andfitted()which wraps it, were the ones missing the label. The fit’sfamily_varcolumn is now carried through;estremains the last column and its values are unchanged. Fits with nofamily_varcolumn – every single-family fit – return exactly what they always did. Separately,?predict.gllvmTMB_multinow documents thattype = "response"includes the row’s offset (so it is an expected count at that effort), and that the effort-free / relative-intensity scale is obtained by zeroing the offset variable innewdata– exact, because the offset is re-evaluated againstnewdata, which is why no newtype =is needed.vignette("response-families")gains seven compact worked examples, one per family-specific dispersion trap (#1082). Gamma (phi_gammais a shape, not a dispersion), student (sigma_studentis a scale, not the response SD), Beta (phi_betais a precision; boundary 0/1 values are not Beta data), multi-trial binomial (cbind()vsweights = n_trials, and a now-fixedsimulate()gotcha for older saved draws),ordinal_probit()(cutpoints depend on which categories are actually observed, per trait), the truncated count families (phi_truncnb2is its own vector, separate fromphi_nbinom2), anddelta_gamma()(phi_gamma_deltais a CV, the inverse-square of its near-identical neighborphi_gamma) each get a minimal runnable fit and the documented route to reading its dispersion. Theresiduals(type = "randomized_quantile")diagnostics comment is also corrected: exact residuals cover 13 families now, not the 4 it previously named. These are worked examples of point estimation and fitted diagnostics, not an evidence-tier promotion.predict(newdata = )now re-adds three more random-effect tiers, andproptois no longer skipped for every row when the first row has an unseen species (#1138, #1132).diag_species,rr_Wanddiag_Ware reconstructed on thenewdatapath, each pinned by the same acceptance test the SPDE tier had to meet:predict(newdata = training rows)must reproducereport$etaexactly. That standard matters here becauseq_spis indexed(trait, species)– the transpose ofp_phy– so a swapped index would still yield a plausible non-zero contribution. Therr_W/diag_Wpair is keyed on the unit-observation column and is added only whennewdatacarries it; otherwise the existing warning names them as omitted. Also fixed: theproptore-add was gated on!is.na(sp_id[1])– row one only – so a single unseen species in the first row silently dropped the tier for the whole frame; each row is now guarded on its own. Still deliberately not re-added, and still warned about:equalto(indexed by observation, so it has no meaning for new rows), anddiag_cluster2/ the*_slopeand phylo-diagonal blocks, whose reshape conventions are not established (seegetREsd()).Boundary screening now sees two spatial/kernel Psi companions it previously missed, and
check_gllvmTMB()’s spatial psi row no longer returns zero rows (#1119). ⚠️ Behaviour change: fits that previously passed clean may now emit new boundary flags / WARN rows. Two real, non-trivial quantities were absent from.gllvmTMB_boundary_flags()’s screening list entirely:sd_spde_unique(the spatial*_unique()Psi companion) andsd_kernel_diag(its multi-kernel analogue) – a collapsed spatial or kernel Psi went unflagged. Both are now screened;sd_kernel_diagis filtered to only the tiers that actually carry a fitted diag, since the current multi-kernel grammar cannot request one yet (it is otherwise always an all-zero placeholder that would false-fire on every 2+-named-kernel fit). Separately,check_gllvmTMB()’s ownspatialpsi row used the dead literal"sd_spde"– no such name is ever REPORTed (the real name issd_spde_unique) – so it silently produced zero rows (no PASS, no WARN) for every spatial fixture; it now reports correctly. The dead literals"sd_phy"/"sd_spde"are also removed from.gllvmTMB_boundary_flags()’s own list (they never matched a REPORTed name there either;sd_phy_diagandsd_spde_bwere already screened under their real names).Unnamed mixed-family
family = list(...)lists no longer silently swap which family fits which trait (#1120)..align_mixed_family_list()left an unnamed family list unchanged, while the selector column’s levels for a character column were built viasort(unique(...))– alphabetical, not the user’s list order. A fit such asfamily = list(student(), gaussian())against a plain characterfamilycolumnrep(c("student", "gaussian"), each = n)silently fit the student data as Gaussian and the Gaussian data as Student-t: zero warnings, a converged fit, every downstream quantity wrong. This belongs to a recurring class in this package – the code chose a fallback and then reported success – alongsidefitted()’s anddeviance()’s silentNULL(#1114, #1118); the reassuring-message variants (a converged fit, a clean summary) are the dangerous form, because nothing about the output signals that a guess was made. This is a behaviour change, but not a blanket one: an unnamed list is now validated by computing BOTH the order the list was written in and the order implied by each family object’s own name (e.g. a"student"level naturally meansstudent()). When they agree, nothing was ever ambiguous and the fit proceeds exactly as before – most existing code, including every other mixed-family test in this package’s own suite, already writes list order to match level order and is unaffected. When they disagree, that disagreement IS the #1120 defect firing, and the fit now errors (classgllvmTMB_mixed_family_unnamed_ambiguous) showing both readings explicitly and telling the user to name the list (list(student = student(), gaussian = gaussian())) or supply an explicit factor. Selector columns with no name evidence at all (e.g. arbitrary labels like"count"/"binary") still use list order, but the resolved pairing is now reported once so it is auditable rather than assumed. A level whose text matches more than one family object’s name, or a list where some levels have name evidence and others don’t, is refused as ambiguous rather than partially guessed. The same defect pattern existed a second time, independently, in the multinomial mixed-family trait-identification path (expand_multinomial_response(),R/gllvmTMB.R, reachable before.align_mixed_family_list()runs) and is fixed the same way.-
predict(newdata = )no longer silently drops the spatial field, ignoresre_form, or applies the wrong arm’s inverse link (#1132). Three defects in one function, all measured in Design 126 §3:- The
newdatabranch re-added only therr_B,diag_Bandproptotiers out of ~37, so a spatial fit’s SPDE field was absent from everynewdataprediction – at training locations too – while the branch reported that random effects had been added. Measured on a converged spatial fit: dropped-piece sd 0.381 against a linear-predictor sd of 0.949, and reproduced independently on a plaingaussian()spatial fit (sd 0.516 vs 0.786). The SPDE contribution is now re-added by rebuilding the mesh projection withfmesher::fm_basis(), and any remaining active tier this path cannot reconstruct now raises a warning naming it rather than vanishing.newdata = NULLwas always correct and is unchanged. -
re_formwas read only on thenewdatapath, and there only as the literal~ 0. On the package’s default calling conventionpredict(fit, re_form = ~ 0)therefore returned the full conditional predictor, contradicting the documentation;NA– a documented form – and numeric0were ignored on both paths. All three forms are now honoured on both paths, and an unrecognised value warns instead of silently including the random effects. This changes the numbers returned by existingre_form = ~ 0calls, includingfitted(object, re_form = ~ 0), which forwards to the same path. -
type = "response"onnewdatareduced the per-row family/link ids to a per-trait modal id, which cannot represent anisdm_sources()fit (the family varies by source within trait). Detection-arm rows were returned through the count arm’s inverse link, giving “probabilities” in [0.253, 2.32] – above 1, silently. The ids are now recovered per row from the fit’sfamily_varcolumn, keeping each(family, link)pair together. Single-family and by-trait fits are unaffected. - Also fixed in the same block: with an active
lv_Bscore mean, therr_Bre-add used thez_Binnovation alone and dropped the predictor-informed meanX_lv_B alpha_lv_B; it now uses the reportedU_B_total.
- The
BEHAVIOUR CHANGE:
gllvmTMB_conditional_residual_saturatednow also warns for Gamma, Beta, and student-t fits (#1083). A diagonal random effect indexed at the observed (unit x trait) resolution (latent(0 + trait | unit, d = 1)with the defaultunique = TRUE) perfectly interpolates every observation, driving the family’s dispersion/scale parameter toward a degenerate confound:sigma_eps -> 0for gaussian/lognormal, Gamma’s shape -> Inf, Beta’s precision -> Inf, student’ssigma -> 0.residuals(fit, type = "randomized_quantile")already warned about this for gaussian and (as of the previous entry below) lognormal fits, but Gamma/Beta/student fits with the identical structure were silent. A 15-seed sweep found Gamma’sphi_gammarunning away past1e6(true 6) in 9/15 seeds and student’ssigma_studentcollapsing below 0.1 (true 0.4) in 6/15 seeds under this structure; Poisson, swept the same way, showed no such collapse (it has no continuous dispersion parameter to degenerate, so it is deliberately not included, along with binomial, NB1/NB2, tweedie, and beta-binomial – all discrete-pmf families bounded by probability 1 rather than continuous densities that can diverge to infinity). Models that previously fit silently under this structure will now warn; the fit itself is unchanged, only the residuals-time diagnostic.gllvmTMB_conditional_residual_saturatednow also warns for lognormal fits (#1083). The warning was gated tofamily_id == 0L(gaussian) even though gaussian and lognormal share one literalsigma_epsand are auto-suppressed identically at fit time (any_sigma_eps <- any(family_id_vec %in% c(0L, 3L)),R/fit-multi.R:5177) under a per-row diagonal random effect. The gate now matches that fit-time decision.deviance()no longer returns a silentNULLon agllvmTMB_multifit (#1118). With no method registered,deviance()fell through tostats:::deviance.default, which reaches forobject$devianceand returnsNULLwithout error – the same shape as thefitted()defect fixed in #1114.deviance.gllvmTMB_multi()now returns-2 * logLik(object), delegating throughlogLik()so it inherits that method’s existing MAP-point disclosure warning on ridged (aghq_ridge-penalised) fits rather than re-deriving it.Mixed-family fits with a dispersion family now have a valid Hessian and SEs (#1117). Per-trait dispersion/shape parameter vectors (
log_phi_nbinom2,log_phi_nbinom1,log_phi_gamma,log_phi_tweedie+logit_p_tweedie,log_phi_beta,log_phi_betabinom,log_sigma_student+log_df_student,log_phi_truncnb2,log_sigma_lognormal_delta,log_phi_gamma_delta) were previously gated WHOLE-VECTOR: mapped off only when the family was absent from every trait. In a mixed-family fit where the family was present on some traits but not others, the other traits’ entries were free parameters the likelihood never reads — a mechanically singular Hessian (pdHess = FALSE), no validsdreport/Wald/profile intervals, and phantom entries enumerated byprofile_targets()/confint(). Each vector is now pinned per trait to the traits that actually use its family. Single-family fits are unaffected (byte-identical).Convergence diagnostics on ridged fits now judge the objective the fit actually optimised (#1092).
aghq_ridgeapplies a loading penalty in R, outside the TMB objective, sofit_health$max_gradientpreviously reported the gradient of the unpenalised likelihood at the penalised optimum — a number that equals the missing ridge term (|Lambda|/tau^2) rather than ~0, making perfectly converged ridged fits read as unconverged. The reported gradient,stationary_by_gradient,converged,sanity_multi()and the AGHQ stop reason now evaluate the penalised objective; the newfit_health$gradient_is_penalisedfield discloses when this applies, and the raw likelihood gradient remains available viafit$tmb_obj$gr(). Unridged fits are unchanged. Relatedly, the ridge’s reach is now pinned to the ordinary loading blocktheta_rr_Bonly: an accidental merge had briefly made it also penalise spatial latent loadings (theta_rr_spde_lv), contradicting the documented exemption for spatial terms — a regression test now proves spatial loadings carry no silent penalty.Multinomial fits now warn at fit time when their contrast structure is degenerate. The screen added this release (collapsed contrast variance, rail-correlated contrasts, or a collapsed spatial range) is now surfaced automatically, under the existing
gllvmTMBcontrol(warn_runaway = )switch and with its own once-per-session slot, so it cannot suppress or be suppressed by the binomial runaway warning. Setwarn_runaway = FALSEto silence both;check_gllvmTMB()andgllvmTMB_diagnose()are unaffected. Ordinal fits deliberately emit no such warning — that family’s arms ship disarmed (see the calibration note below), so the row reports statistics without a verdict.
This development release adds an opt-in separation-screening and LA-MSPL lane for complete single-trial Bernoulli GLLVMs. Ordinary ML remains unchanged and is still the default.
Changed
-
check_gllvmTMB()’sloading_absolute_threshdefault raised from 6 to 8. Some binomial fits that previously reportedWARNon thebinomial_prevalence_loadingrow now reportPASS; nothing that reportedPASSbefore starts reportingWARN. The earlier default was calibrated on a pool whose true loading scale never reached the regime where this arm misfires. A pool built to crosssigma_lambda in c(0.7, 3.0)(928 healthy / 272 degenerate binomial-probit fits) —3.0chosen to reproduce anaghq_ridgeridge-failure regime, not argued for realism — measured a 25% false-positive rate at the old threshold, all of it attributable to this one arm; raising the threshold to 8 lowers that rate to 15.52% while sensitivity on the same pool’s degenerate fits falls only from 100.00% to 99.63% (one additional missed fit out of 272). This is an improvement, not a fix: the arm remains measurably regime-dependent — false-positive rate 3.85% at a mild true loading scale versus 49.08% at a scale whereaghq_ridge = 2is already known to struggle — so a fixed constant cannot be correct across every loading scale a fit may have. This is effect-size dependence, not the response-scale dependence #851/#855 otherwise describes: probit fixes the residual variance at 1, so there is no free response scale here to rescale against, and the class’s usual per-fit device does not obviously transfer.aghq_ridge = 2reduces the problem (46.0% -> 13.5% false positives at that larger scale) but does not remove it.
New
screen_gllvmTMB()now catches exact response-side dependencies among three or more traits (issue Ayumi-495 /urbanisation_map#23). The pairwise duplicate/complement check only sees two-way exact relations; aresponse_affine_rankrow (in$design) and a new$response_dependenciestable now also flag one-hot/simplex blocks, most commonly a review-type, geographic-scope, or temporal-scope variable recorded as several dummy columns that sum to exactly 1 on every row, via an affine-rank check on the augmented response matrix. Certificates are recovered on a best-effort basis (not exhaustive minimal-subset discovery); the newknown_groupsargument lets you declare a set of trait names as a one-hot block or a nesting/containment chain for an exact, deterministic check instead. See the new “Exact response-side dependencies” section ofvignette("pre-fit-response-screening", package = "gllvmTMB").New
ridge_path(): a loading-ridge sensitivity sweep (issue Ayumi-495 /urbanisation_map#23). Refits a model across a grid ofgllvmTMBcontrol(loading_ridge = tau)scales and reports, per trait, how the largest loading and its communality move as the penalty weakens.print()classifies each trait as “interior (stabilises as penalty weakens)” or “penalty-determined (moves toward boundary)” using a documented slope rule on the last two finite-taugrid points. This is sensitivity evidence, not an identification certificate;tau = Inf(plain ML) can legitimately fail to converge for a pathological trait, and that failure is itself part of the diagnostic.fitted()now works on the defaultgllvmTMB_multifit (#25).fittedwas only registered as an S3 method for thegllvmTMB_juliaandgllvmTMB_vaengine classes, sofitted(fit)on an ordinary fit silently returnedNULLrather than erroring or dispatching.fitted.gllvmTMB_multi()is a thin wrapper overpredict(object, newdata = NULL, type = ), returning the same long data frame (defaulttype = "response", matching thefitted()convention;predict()’s own default remains"link").Exact randomized-quantile residuals now cover nine more families.
residuals(fit, type = "randomized_quantile")andpredictive_check(fit, type = "rq_qq")compute exact family-CDF residuals for binomial (logit/probit/cloglog), lognormal, Gamma, Beta, betabinomial, Student-t, zero-truncated Poisson, zero-truncated NB2, and ordinal-probit rows, in addition to the existing Gaussian, Poisson, NB1, and NB2 support. Rows for tweedie, the delta/hurdle families (delta_lognormal,delta_gamma), and multinomial remainstatus = "unsupported_family": tweedie has no closed-form CDF without a new dependency, the delta/hurdle families need a design decision for splitting the point mass at zero, and multinomial’s categories are unordered so a randomized-quantile residual is undefined for it. See?residuals.gllvmTMB_multifor the full scope statement.-
A
check_gllvmTMB()row for multinomial K-1 contrast degeneracy.multinomial()(fid 16) fits as K-1 baseline-category contrast pseudo-traits per response;check_gllvmTMB()now reports amultinomial_contrast_degeneracyrow that screens those contrasts against each other for three failure modes generic loading diagnostics cannot see: one contrast’s loading energy collapsing to ~0 (absolutely, or relative to its sibling contrasts), two contrasts of the same response loading almost perfectly on the same axis (evaluated only where the tier’s rank is 2 or more, since rank-1 tiers reach that correlation on every healthy fit by construction), and a spatial field’s practical range collapsing relative to the coordinate domain. This is a check-row addition only – no fit-time warning, no new export, no behaviour change to any existing fit.Three screens ship armed at calibrated defaults:
multinomial_collapse_floor = 1e-10(contrast variance collapse),multinomial_rail_thresh = 0.99(two contrasts railed onto one axis), andmultinomial_range_collapse_thresh = 0.02(spatial range collapse).multinomial_collapse_rel_threshstays disarmed (Inf) – untested. Measured against pre-registered labels (128 fits, 122 converged with a PD Hessian): the variance-collapse screen detected 6/7 labeled collapses plus 7/7 on a later, entirely out-of-sample cell; the contrast-rail screen detected 8/8 labeled rails, plus four individually railed fits hidden inside a cell whose aggregate gate had passed (refitting confirms |rho| = 1.00000, against controls at 0.49 and -0.15); the spatial-range screen detected 3/3. Zero false positives on 40 informative healthy fits, and the denominator is stated honestly: fits with no loading tier emit no row at all and cannot evidence specificity, so the rule-of-three bound is about 7.5%, not a verified zero. The contrast-rail screen is deliberately silent on rank-1 tiers, where |rho| = 1 holds on every healthy fit by construction (verified 0/20 out-of-sample).The spatial-range screen was fixed before arming: it originally read the low-rank loading matrix
Lambda_spde, which the engine reports only forspatial_latent()/spatial_dep()fits, so onspatial_indep()fits – exactly the ones it was built for – it produced no row at all. It now branches on the engine route, taking the range fromlog_tau_spdeon the diagonal route: rows emitted 0/20 to 20/20, detection 0/3 to 3/3, with 0/11 false positives on the same cell’s healthy fits. -
A
check_gllvmTMB()row for ordinal-probit loading degeneracy (fixes the coverage gap reported in #897, where a degenerateordinal_probit()fit had no detector at all – 239/239 unflagged where the binomial screen caught 272/272). A mechanism probe found no empirical support for cutpoint-underflow saturation across 24 measured degenerate fits (flat-row share exactly 0 throughout) – that negative finding stands. The probe’s third measurement, originally read as positive evidence for category-level quasi-complete separation (the same mechanism the binomial row already screens for), was later shown, on a larger 315-fit calibration, to fire on 86.3% of healthy fits and so does not discriminate; category-level separation remains the residual hypothesis, not a demonstrated one. The pathology’s shape is a single trait’s loading column running away while sibling traits stay near truth.check_gllvmTMB()now reports anordinal_liability_loadingrow with two arms modeled directly on the binomial row: a trait’s largest loading relative to the typical loading among the other ordinal traits, and the largest loading on the link (liability) scale, unit tiers only – scale-free because the probit-liability residual variance is exactly 1 by the Wright/ Falconer/Hadfield threshold convention. This is a check-row addition only – no fit-time warning, no new export, no behaviour change to any existing fit.Both arms ship DISARMED at
Inf/Inf– the row still computes and reports its statistics, but neither threshold is armed by default. Calibration ran 315 fits across four pre-registered design arms, scored under the frozen pre-registration (sensitivity on thedegeneratearm’srel_frob > 10fits; false positives measured acrosshealthy+transport+mixedcombined), and found no threshold that clears the pre-registered target of 90% sensitivity with zero false alarms – and none can: the healthy pool reaches a loading magnitude of 216.9 while the degenerate arm starts at 13.5, so the classes are not separable on this statistic at all. At binomial’s own threshold of 6, the absolute arm reaches 100% sensitivity but 39.2% false positives and the relative arm 61.0% sensitivity at 28.6% false positives – worse than the 25% false-positive rate #897 reports for binomial itself, so borrowing binomial’s threshold would have shipped a bigger problem than the one the issue complains about. The false alarms concentrate in designs mixing very different per-trait loading scales: an absolute liability-scale threshold cannot transport across heterogeneous trait scales, so a future screen needs a scale-invariant statistic rather than a better constant. Honest limits: no evidence atn = 1600(that arm was dropped for run time). What the campaign establishes: link saturation is refuted as the mechanism (solid); category-level separation, the residual hypothesis, is NOT demonstrated – the evidence originally cited for it does not discriminate; and the threshold question is answered negatively with a stated path forward.Neither categorical screen changes what fitting itself does:
gllvmTMB()warns exactly as before, and both rows appear only when you callcheck_gllvmTMB()on the fit. Liability-scale phylogenetic heritability for categorical families (this corrects a real defect: on a phylogeny-only
ordinal_probit()ormultinomial()fit,extract_phylo_signal()previously reportedH2 = 1.0for every trait and every contrast. That number was the species-level-latent proportion and was arithmetically correct as such, but as a heritability it was silent nonsense – the fixed liability residual never entered the denominator, so a phylogenetic signal that should read around 0.3-0.4 read as 1. If you have reported a categoricalH2from this function, re-run it withlink_residual = "auto".)extract_phylo_signal()gains alink_residualargument. The default ("none") keeps the historical species-level-latent denominator unchanged, so no existing non-categorical result moves;link_residual = "auto"adds each trait’s fixed distribution-specific latent residual to the denominator, returning the conventional liability-scale phylogenetic heritability of Mizuno et al. (2025, J. Evol. Biol. 38:1699-1715, eq 4/18/19):ordinal_probit()reports per-traitH2 = V_a / (V_a + 1)andmultinomial()reports per-contrastH2 = V_a(k) / (V_a(k) + pi^2/3). Multinomial contrast heritabilities are baseline-referenced (the softmax link residual couples contrasts through the shared baseline category) and are reported one row per contrast, never collapsed to a scalar. An advisory now fires when a categorical fit is summarised with the default denominator, andci = TRUEwithlink_residual = "auto"refuses with a typed error rather than returning uncalibrated intervals. Verified against an MCMCglmmfamily = "ordinal"comparator on a shared phylogenetic fixture.A Species Distribution Models article collection. The pkgdown site gains a dedicated navbar menu ordering the SDM material as a curriculum — the joint species distribution model guide, a new presence-only opener, and the three integrated-source articles. The new article, Joint ecological intensity from opportunistic records (
vignettes/articles/gbif-joint-intensity.Rmd), fits a joint relative-intensity model to GBIF-style records alone through the ordinarygllvmTMB()call (Poisson counts, an effort offset, a named recording-bias covariate, and onelatent()factor). It estimates relative ecological intensity only — no abundance, absolute occurrence, or detectability claim — and states the design assumption its bias term rests on.A fifth SDM article: repeated survey visits. What do repeated survey visits add to an integrated model? (
vignettes/articles/integrated-repeated-visits.Rmd) shows visit-varying conditions entering the survey branch on the complementary-log-log scale, demonstrates that within-cell replication under varying conditions is what separates a poor visit from low intensity, and states the boundary with separate-detection occupancy models. The two-source article is renamed Integrating opportunistic records with a designed survey to keep the two subjects distinct.A sixth SDM article: rare species and separation. When a rare species breaks the JSDM (
vignettes/articles/rare-species-jsdm.Rmd) is the species-facing door to the separation workflow: a heat specialist recorded only at the warmest sites, thescreen_gllvmTMB()certificate, what ordinary ML does at that boundary, the finite opt-in experimental MSPL point with its fences, and a live demonstration that the loading ridge does not repair separation.A four-lens editorial audit of the SDM collection. Every article under the Species Distribution Models menu was audited for readability, claim discipline, statistical correctness, and figure integrity. Fixes include: the two-source and design articles’ displayed equations now carry the per-species reporting level
rho_stheir code always fitted; the stale “more than two sources: not available” claims are corrected to point atisdm_sources(); health-check outcomes are stated after everycheck_gllvmTMB()chunk;pd_hessianand Fisher-z are glossed at first use; and figures gain alt text, equal-axis identity lines, and non-overlapping labels.The LA-MSPL worked example is rewritten around its two failure modes. Rare items and runaway estimates in Paper × Items evidence synthesis (
vignettes/articles/mspl-binary-jsdm.Rmd, same URL) now grounds the example in an evidence-map-shaped corpus and demonstrates, on one dataset, that runaway loadings and fixed-design separation are different diseases with different matched remedies — the opt-in loading ridge for the first, opt-in MSPL for the second — including the negative result that the ridge does not repair separation. Claim boundaries (probit vs logit ridge regimes, AGHQ’s large-n evidence, MSPL’s refused inference surface) are stated in the article.Complete-Bernoulli GLLVMs can opt in to LA-MSPL point estimation. Set
estimator = "mspl"to fit the experimental maximum softly penalised Laplace objective for one ordinarylatent(d = 1:2)block with no free Psi, standalonespatial_indep(), or standalonespatial_latent(d = 1:2), with a common logit, probit, or complementary-log-log link.estimator = "ml"remains the unchanged default, and a separation warning never switches estimators automatically. The admitted surface also requires a full-rank resolved fixed-effect design and all-zero offsets. The implementation follows the construction of Sterzinger and Kosmidis (2023, doi:10.1007/s11222-023-10217-3) for its fixed-effect Jeffreys component and extends it with rotation-invariant radial penalties for estimated GLLVM loadings and reference-scale spatial coordinates. At this development checkpoint, the compiled objective and point-estimation API are implemented, while frozen ordinary and spatial simulation promotion remains in progress. Likelihood comparison, standard errors, confidence intervals, profiles, and hypothesis tests fail closed. Missing responses, grouped binomial data, mixed families, rank above two, extra covariance tiers, VA/AGHQ/Julia MSPL, and general inference remain planned rather than advertised.The loading MAP penalty now has the integration-neutral spelling
loading_ridge. It is an alias foraghq_ridge, not a new penalty, and preserves unpenalised Laplace fits unless named explicitly. Loading ridge and LA-MSPL are distinct estimators; supplying both is an error.screen_gllvmTMB()can opt in to fixed-design separation certificates. Usescreen_control(separation = "fixed")to classify maximal coefficient-connected Bernoulli fixed-effect blocks as overlap, complete separation, or quasi-complete separation. The detector supports the logit, probit, and complementary log-log links, finite known offsets, shared coefficients, and structural-zeroXcoef_fixedconstraints. It requires the optionaldetectseparationpackage. The default remainsseparation = "none"and does not load that package. This is a fixed-design diagnostic only: it does not certify finite latent loadings or covariance parameters, and it never selects a penalized estimator automatically.
Fixed
**
screen_gllvmTMB()‘sknown_groupsargument no longer misses a partial-order nesting declaration or a certified one-hot block in its unresolved-dependency count (reported by @Ayumi-495,urbanisation_map#23).** Two related defects in theknown_groupsfeature shipped in #1123: (1) a group of three or more traits whose containment relations form a partial order rather than a single total chain – for example a broad realm indicator (“water”) containing two mutually incomparable narrower ones (“freshwater”, “marine”) – previously fell through the chain-only check (which tested only the declared order and its reverse) and silently PASSedknown_group_checkedinstead of FAILingknown_nesting; the check now tests every pairwise containment among the declared members and names every relation it finds. (2)$response_dependencies’unresolvedaffine-dependency count never subtracted the one-hot certificates a user declared viaknown_groups, so a correctly certified dependency could still be reported as “N further exact affine dependencies … could not be automatically resolved”. The count is now the RANK of the pooled certified null-vector span (automatic certificates + declared one-hot blocks), not a subtraction of row counts, so declaring the same dependency under two names – or via two overlapping groups – cannot under-report a genuinely unresolved dependency. A nesting/containment relation is an inequality, not an exact affine relation, and still contributes nothing towardunresolved. Also fixed in the same pass: a typo’d trait name inknown_groupswas silently accepted whenever the affine/known-group check itself was infeasible for the data (no unit column, non-Bernoulli rows, duplicate unit-trait rows, too few complete units); trait names are now validated before that feasibility check, so a typo always aborts.screen_gllvmTMB()’sknown_groupsone-hot test was whole-group only and is now a bounded exhaustive subset search (#1154, reported by @Ayumi-495).known_groups = list(g = c("A", "B", "C", "D")), where{A, B, C}is a genuine one-hot block andDis an unrelated trait declared in the same group, previously reportedknown_group_checkedPASS – the same “declaring a slightly-too-large group weakens the verdict” shape as the nesting defect fixed just above, since the test wasall(abs(rowSums(Yg) - 1) < tol)over the whole declared group. A declared group ofk <= 12members now gets an exhaustive search over every subset of size 2 or more (at most2^12 - 13 = 4083row-sum checks, microseconds), and every MINIMAL one-hot subset found is reported as its ownknown_one_hot_subsetcertificate –A + B + C = 1once, never also as a redundant larger superset – and contributes to theunresolvedaffine-dependency count exactly like a whole-groupknown_one_hotcertificate.k > 12is never silently skipped: the whole-group one-hot and pairwise nesting checks still run, but an explicitknown_group_subset_not_attemptedrow records that the subset search itself was not attempted at that size, rather than folding into a silent PASS. A subset that sums to 1 only because every one of its members is constant is not certified (the same guard the whole-group check already had). Nesting was not affected: its pairwise scan already covers every pair within a declared group regardless of unrelated members, so it had no equivalent blindness.fit_health$boundary_flagsno longer flags the auto-Psi skip block’s mapped-offsd_Bplaceholders asnear_zero_sd_B(#25). The sameskip_psi_b_tpinning described in thenear_zero_psi_unitfix below (single-trial Bernoulli traits, and every multinomial contrast pseudo-trait, pinned tosd_B = 1e-6and mapped off) was also read raw by.gllvmTMB_boundary_flags(), which had nodiag_B_skipfilter of its own, so it unconditionally flagged the pinned placeholder. A default auto-skip fit and its explicitlatent(..., unique = FALSE)mirror could therefore disagree onboundary_flagsfor an otherwise identical model. Both readers now share one helper,.gllvmTMB_estimable_components(), which drops the mapped-off entries before either screen runs; a genuine collapse among the remaining, estimable traits is still caught. This is a behaviour change: fits with a mapped-off Psi trait that previously carried a spuriousnear_zero_sd_Bflag no longer do.simulate()now draws from each row’s true family instead of silently substituting a Gaussian-on-link-scale number for nine families, and multi-trialbinomial()responses are drawn at the correct trial count..draw_y_per_family()previously fell through to a single shared branch for any family it did not explicitly recognise, so tweedie, Beta, betabinomial, Student-t, truncated Poisson, truncated NB2,delta_lognormal,delta_gamma, and ordinal-probit rows were redrawn as plain Gaussian noise on the link scale — a plausible-looking but wrong number, not an error.simulate(),predictive_check()’s simulation-based plot types (stat_grouped,dens_overlay), and anything built on.gllvmTMB_predictive_draws()(bootstrap/coverage helpers included) now draw correctly for 16 of these — every family except tweedie, which has no exact draw without a new dependency and now returnsNAwith a per-call warning rather than the previous wrong-distribution substitute. Multinomial rows draw a single grouped categorical outcome per observation rather than an independent draw per contrast row. Binomial rows previously ignored trial count and drew Bernoulli regardless ofcbind(success, failure)/weights = n_trials; they now drawrbinom()at the row’s actualn_trials.check_gllvmTMB()’snear_zero_psi_unitscreen no longer flags traits the auto-Psi skip block deliberately pinned off.R/fit-multi.R’sskip_psi_b_tblock maps a trait’s between-unitPsioff (single-trial Bernoulli, and every multinomial contrast pseudo-trait) by pinningtheta_diag_Batlog(1e-6), butsrc/gllvmTMB.cppstillREPORTssd_Bfor every trait including the pinned ones, so the pinned1e-6entry always cleared both the absolute (psi_thresh = 1e-4) and relative (psi_rel_thresh) collapse thresholds. This was a structural false positive that predates the Design 123 arc above:check_gllvmTMB()WARNednear_zero_psi_uniton every fit with a single-trial-Bernoulli or multinomial trait sharing alatent()term with a free partner trait, regardless of whether the free trait’s Psi was healthy. The screen now drops pinned entries (viatmb_data$diag_B_skip) before evaluating the unit-level psi row; a genuine collapse among the remaining free traits is still caught.multinomial()structured-term admission is now fail-closed (Slice 0, Design 108/123). Several deferred keywords previously desugared (R/brms-sugar.R) onto the same internal engine flag as an admitted keyword and silently reached an untested categorical path instead of erroring:dep()at the unit tier,phylo_dep(),phylo_indep()/phylo_unique()(standalone),animal_latent(), single-namekernel_*(), andphylo_scalar()/animal_scalar(). Every one of these now aborts, as the documentation always said they should. If you fitted amultinomial()trait combined with any of the keywords above, that fit ran on an unvalidated structured-term path and should be re-checked — the currently admitted set is unchanged: fixed effects, an ordinary sharedlatent(0 + trait | unit, d = k)ordination, and intercept-onlyphylo_latent()(defaultunique = FALSE; it emits no Psi companion at all). Ami()predictor term combined with a multinomial trait, previously invisible to the admission scan due to definition order, now also aborts. A follow-up adversarial review found the fence itself was not yet load-bearing everywhere: augmented (intercept + slope)latent()/phylo_latent()random regressions,phylo_latent(unique = TRUE)(a free phylogenetic Psi, never admitted, but the fence’s first pass wrongly labelled it admitted), andmeta_V()/equalto()(no established route on a categorical-contrast pseudo-trait) are now all explicitly blocked, with a shared classed condition (gllvmTMB_multinomial_structured_not_admitted) on every path.-
The
multinomial()structured-term surface now admits a bounded set of among-category and grouping structures, each gated on a signed, pre-registered recovery campaign rather than construction alone. Every admission below is enforced by the same fail-closed classifier (R/multinomial-fence.R); anything not named below still aborts typed (gllvmTMB_multinomial_structured_not_admitted). The full per-cell table and supporting evidence are available in the package repository.Admitted: the phylogenetic/relatedness surface – intercept-only
phylo_latent()/animal_latent()/single-namekernel_latent()(loadings-only),phylo_dep()/animal_dep()/kernel_dep()(full unstructuredV, the IDENTICAL parameterisation asphylo_latent(d = K - 1)), andphylo_indep()/animal_indep()/kernel_indep()(diagonalV); the spatial (SPDE) surface –spatial_latent(),spatial_indep(), andspatial_dep()(verified identical tospatial_latent(d = n_traits)); a generic(1 | group)random intercept (baseline-vs-rest semantics,sigma_rereference-category-specific); and the non-phylogeneticcluster/cluster2diagonal tier,indep(0 + trait | g).The honest evidence, not softened: on the phylogenetic surface, the ONE-CATEGORICAL-DRAW-PER-SPECIES recovery gate FAILED for both the loadings-only route (rail rate 8/20, exceeding the 6/20 threshold, identically for
animal_latent()/kernel_latent()by proven engine identity tophylo_latent()) and the covariance-mode comparison (phylo_dep()rails 8/20;phylo_indep()’s corrected diagonal-truth rerun shows larger contrast variances recover fine, median ratio 0.78, 17/20 in band, but smaller ones collapse, median ratio 0.24, 9/20 – 7 of those 20 seeds collapse the smaller contrast variance to numerical zero (≤1e-9), each withconvergence = 0AND a PD Hessian, and no runtime detector currently flags it (R/diagnose.R’s degeneracy gate is currently family_id == 1-only) – and the planted-zero check FAILS – a full-Vphylo_latent()refit on diagonal-truth data rails to median |rho| = 1.0). A pre-registered replication rescue PASSED: five categorical draws per species (n_sp = 300,n_rep = 5) recovers V with rail rate 4/20, median rho 0.680 among the 16 NON-RAILED seeds (0.696 among all 20 conv+PD seeds – both bands, true 0.6), SD ratios 0.89/0.85 – one categorical draw per species does not identify V; five draws per species does. This rescue transfers exactly tophylo_dep()(the identical parameterisation) but has NOT been tested for the diagonal-Vmode (phylo_indep()). The spatial kappa/tau gate, by contrast, PASSED all three cells (median practical-range ratios 1.75 / 1.12 / 1.75 among the 14 conv+PD seeds per cell, band 0.33-3.0; rails 0, 3, 0 of those 14 against the frozen threshold, restated in its pre-registered form: >6/20; 6/20 seeds were non-PD per cell, excluded from the ratio band). Per-seed dispersion is wider than the median suggests: 4 of the 14 PD seeds fall outside [0.33, 3.0] forlatent()/dep()(up to a 4.56 ratio), andspatial_indep()collapses the range in 6 of all 20 seeds (ratios down to 7e-5): 3 of those 6 are PD – the rails counted under the frozen criteria, at ratios 2.3e-4 to 3.4e-4 – and the other 3 sit among the 6/20 non-PD exclusions. A collapsed field can pass the Hessian check, the same pattern as the phylogenetic surface’s zero-collapse above. The group-intercept gate ((1 | group) ONLY) also PASSED: 20/20 converged with a PD Hessian, mediansigma_reratio 0.947, range [0.60, 1.51]. This PASSED verdict does NOT extend to the cluster/cluster2 diagonal tier – the s4 campaign fitre_intexclusively;cluster/cluster2are admitted with construction-level evidence only (the fit constructs,extract_Sigma()returns a well-formed diagonal), and their recovery axis remains OPEN (a correction to an earlier draft of this entry, which wrongly implied the same PASSED verdict covered both).Refused, not merely deferred:
phylo_scalar()/animal_scalar()/kernel_scalar()/spatial_scalar()andcommon = TRUEon the cluster/ cluster2 tier – a single shared level across theK-1contrasts has no interpretable null on the(I+J)contrast geometry. Null-DGP evidence: onV_true = 0data,phylo_indep()correctly recovers near-zero variance in 5/5 seeds, butphylo_dep()’srho_hatrails toward+-1in 4/5 seeds despite a PD Hessian.Behaviour changes: a
(1 | group)or cluster/cluster2indep()term whose grouping factor covers exactly one categorical observation per level now aborts typed (gllvmTMB_multinomial_olre_not_admitted) – it is an observation-level random effect in disguise, unidentifiable because the softmax latent scale is fixed.meta_V()/equalto()(known-sampling-covariance) remains fail-closed formultinomial()traits – confirmed Gaussian-only, no established route on a categorical-contrast pseudo-trait.cluster2co-admission alongsideclusterwas a maintainer decision (2026-08-16):use_diag_speciesanduse_diag_cluster2are literally identical engine math on two different grouping columns.
gllvmTMB 0.6.0
This release focuses on multivariate stacked-trait models fitted through the R/TMB engine. Models are fitted by Laplace approximation by default; an opt-in scalar variational research route is also available. The optional Julia bridge remains experimental and is not required for the main workflow.
Changed
Integrated models now take any number of named sources, declared with
isdm_sources()(experimental). A portal stream, digitised literature records, checklists, and a structured survey can each be a named source with its own observation law, all sharing one ecological process:family = isdm_sources(gbif = poisson(), literature = poisson(), survey = binomial("cloglog")), with anisdm_sourcecolumn in the data naming each row’s source. Two laws are admitted — a Poisson count stream and a complementary-log-log detection stream — because both observe a thinning of the same shared intensity; that argument holds arm by arm, so it does not weaken as sources are added. Everything the two-source route refused stays refused: logit or probit detection, dispersion-carrying families,weights, multi-trial rows, and any trait not observed by every declared source. The existing two-source form (list(gbif = ..., survey_pa = ...)) keeps working unchanged and gives identical fits — it is now the two-source case of the same rule. A worked example, Integrating three data sources at once: portal, atlas, and survey, fits a three-source model end to end through the declared route. Everything reported remains relative intensity, and the interface remains experimental.-
Integrated two-source models can now be fitted through
gllvmTMB()itself (experimental). Opportunistic presence-only records and a structured detection/non-detection survey of the same species can enter one likelihood, sharing an ecological linear predictor while each source keeps its own observation model. There is no separate function for this: supplyfamily = list(gbif = poisson(), survey_pa = binomial("cloglog"))withattr(family, "family_var") <- "isdm_family", asourcecolumn, and a matchingisdm_familycolumn. This is the one case where the response family may vary within a trait rather than only between traits, and it is admitted because the cloglog link makes a detection observation consistent with the same underlying intensity that generates the counts. Anoffset()is admitted on that arm as a known change-of-support term. Anything short of that exact contract still gets the ordinary one-family-per-trait error, and Poisson-log mixed with binomial-logit or -probit remains refused.In: the route is reachable, documented, and contract-tested. Two articles cover it: Integrating opportunistic records with a repeated survey fits and renders through the public route, and How big does an integrated survey design need to be? reads a known-truth design curve to ask whether a design is large enough before you fit at all. Partial: everything reported is relative intensity — presence-only data cannot identify absolute abundance, occupancy, or detectability, and none are estimated. Source-specific spatial structure is only weakly identified on small designs; treat a portal-only field as a nuisance adjustment unless the design is large. Estimator accuracy at the advertised scope is not certified, and the spatial arm in particular rests on development experience rather than a cleared recovery gate. Not included in this legacy two-source spelling: declarations with more than two sources; use the experimental
isdm_sources()route described above. Calibrated intervals and weighted joint likelihoods remain unavailable on either spelling. Expect the interface to change. The exact-aa confirmation completed 44,800 production fits and returned aggregate HOLD. Gaussian
indep(), Gaussiandep(), Poisson-log rank-1latent(unique = TRUE), and Binomial(10)-logit rank-1latent(unique = TRUE)passed their prespecified small/large core point-estimation gates. Gaussian latent and NB2 latent remained characterization-only. This does not establish a package-wide dependable core: the full eligible cell set was incomplete, every campaign contained failed cells, and the observable terminal-status detector identified only 19 of 80 catastrophic truth errors. The result supports narrow tested-regime point-estimation statements only; it does not certify intervals, structured sources, slopes, mixed families, alternative integration engines, or reliable silent-failure detection. At that checkpoint the package remained version 0.6.0 while this evidence and the pre-0.7 issue backlog were reconciled.-
A fit without standard errors no longer returns a silent all-
NAanswer. When a model is fitted withgllvmTMBcontrol(se = FALSE), there is nosd_report, so a Wald interval has nothing to be built from.confint()used to return a matrix ofNAbounds with no error and no warning — a non-answer that looks like an answer, and that flows onward into tables and plots with nothing marking it.confint(method = "wald")now raises a typed error (gllvmTMB_confint_no_sdreport) naming both remedies:fit <- standard_errors(fit), ormethod = "profile", which does not need standard errors. This covers both Wald routes — the fixed-effects path and the variance-component target path (parm = "sigma_eps"and friends). The extractor-style consumersgetREsd(),getLV(se = TRUE)andpredict(se.fit = TRUE)already behaved this way.The other way to have no usable standard errors is also covered. A fit whose Hessian is not positive-definite has an
sd_report, but its standard errors come back non-finite — and those used to print as a wall of bareNaN.summary()now says so, andconfint()aborts (gllvmTMB_confint_nonfinite_se). The advice deliberately differs: this is a property of the fit, sostandard_errors()cannot help and the message points atgllvmTMB_diagnose()andmethod = "profile"instead.A single
NAis left alone. A coefficient fixed viaXcoef_fixedhas no standard error, andNAis the right answer there — only an entirely non-finite set is treated as the pathology.summary()deliberately still works. Fitting fast and reading point estimates is a legitimate workflow, sosummary()prints as before — but it now says why theStd.Errcolumn is empty instead of leaving a column of bareNAs to be read as a computed result.extract_cutpoints()reports the same way for itstau_secolumn.This is a deliberate behaviour change, made rather than staged: the package is pre-1.0 and experimental, so no released code depends on the old return, and anything that did would have been depending on a wrong answer.
New
gllvmTMBcontrol(integration = "va")now admits all 18 scalar family/link cells and defaults to seven-node Gauss-Hermite evaluation. Each cell passed an independent arithmetic, compiled, and light-fit gate before H = 7 and automatic GH routing were promoted; analytic cells retain exact fast paths, explicit JJ remains available only for binomial-logit comparisons, and multinomial or other non-scalar architectures remain excluded. The preregistered 36,000-fit confirmation is complete: only the Poisson-log q = 5 cell passed the overall point route, while 24 of 36 family-by-rank cells failed and 11 were inconclusive. Against the campaign’s cell-specific criteria, fixed-effect VA-Wald outcomes were 20 pass / 16 fail, while latent posterior-SD outcomes were 15 pass / 20 fail / 1 inconclusive. These are campaign outcomes, not public calibration certificates:vcov()andconfint()remain labelledcalibrated = FALSE,getLV(se = TRUE)remains a variational posterior SD rather than a frequentist standard error, and Laplace remains the package default. Both campaign stages ran on Totoro, so this is not cross-platform confirmation.-
vcov()andcoef()now work on multi-trait fits.coef(fit)returns the named fixed-effect estimates andvcov(fit)their covariance, taken from the fit’ssdreport(). Both were previously registered only for the variationalgllvmTMB_vaclass — wherecoef()deliberately refuses andvcov()is now restricted to uncalibrated fixed-effect VA-Wald inference — so calling either on an ordinary fit raised “no applicable method”, despite the documentation saying otherwise.coef()works on any fit, including one made withse = FALSE: point estimates do not depend onsdreport().vcov()does, and raises the same typed conditionsconfint()does when it is missing or non-finite, so a caller that handles one handles the other. Rows and columns for coefficients held fixed viaXcoef_fixedareNA— a parameter that was not estimated has no sampling covariance. -
standard_errors()computes standard errors after fitting. Fitting withgllvmTMBcontrol(se = FALSE)skips the TMBsdreport()and is meaningfully faster, but it used to be a one-way door: the only route to standard errors afterwards was to fit the model again.fit <- standard_errors(fit)now computes them on demand from the fitted object.The result is the fit-time calculation deferred, not a different one — the same single
sdreport()call on the same converged parameter vector, verified bit-exact (tolerance = 0) against a fit made withse = TRUE. Every existing consumer —summary(),getLV(),getREsd(),confint(method = "wald")— then works on the returned object.Note the R semantics: the fit is returned, not modified in place. Assign the result, or the standard errors are discarded.
Same-session only. A TMB ADFun holds external pointers that do not survive
saveRDS()or a new R session, so this cannot revive a saved fit. That limitation is shared by every part of the package that reuses the fitted TMB object; what is new is that this function says so with a clear, typed error instead of failing obscurely. This adds no new inference and changes no likelihood, parameterisation, or honesty caveat — Wald standard errors carry exactly the caveats they carried before. Spatial mesh, CRS, and range-plot helpers were substantially rewritten within gllvmTMB. The retained implementation descends from earlier GPL-3
sdmTMBhelpers and keeps that attribution ininst/COPYRIGHTS.make_mesh()returns agllvmTMBmeshbuilt through fmesher’s public mesh, finite-element, and basis APIs, while valid legacysdmTMBmeshobjects receive a temporary lifecycle warning and conversion.plot_anisotropy()andplot_anisotropy2()now show the fitted isotropic practical range,sqrt(8) / kappa, for native gllvmTMB spatial fits. Equal axes are explicitly the model assumptionH = I; gllvmTMB does not estimate directional anisotropy. Delta and spatiotemporal states remain unsupported. This changes no TMB likelihood or spatial covariance parameterisation. The mesh/plotting helpers themselves are covered by focused tests; broader spatial-family evidence remains partial; directional anisotropy, delta/spatiotemporal fields, barriers, and new spatial likelihoods are rejected here.-
offset()now works, for count responses.offset(log(trap_nights))in a Poisson or negative-binomial model is the standard way to model a rate rather than a raw count, and until now gllvmTMB rejected it. Both closest comparators offer offsets, so this was a gap rather than a rough edge — and for counts there is no workaround, since an effort adjustment cannot be folded into the response the way a Gaussian one can be centred by hand.It is deliberately restricted to count families —
poisson(),nbinom1(),nbinom2(),truncated_poisson(),truncated_nbinom2(). An offset is a multiplicative rate adjustment on the log link; undergaussian()it would be an unexplained mean shift and underbinomial()a fixed shift in log-odds, neither of which is what the term is for. In a stacked model the check is per trait, so the refusal says which one:offsets are supported for count families (poisson, nbinom) only; trait `t2` uses `gaussian`.This is the advantage of a mixed-family design rather than a cost of one. A single-family package can only recycle one offset across every response.
A zero offset is allowed everywhere and does nothing, because zero on the log scale is a multiplier of one. That is how a mixed-family model gives an offset to its count traits and not the rest — set the offset column to
0on the other rows. In wide format,offset(w)applies one unit-level column to every trait, whileoffset(e1, e2)gives one column per trait intraits()order.The offset also reaches
simulate(),bootstrap_Sigma(),coverage_study(), andpredict(newdata = )— all of which rebuild the linear predictor themselves and would otherwise have used a model you did not fit.predict(newdata = )needs the offset variable present innewdataand says so if it is missing.engine = "julia"has no offset in its predictor and refuses the term rather than dropping it.Offsets inside an
lv = ~ ...sub-formula, or inside animpute/mi()covariate formula, are still not supported. -
A loading penalty is available for fits that run away, via
gllvmTMBcontrol(aghq_ridge = tau). Binomial fits at small sample sizes can drive one trait’s loading to an absurd value while reporting every conventional sign of health —convergence = 0and a positive-definite Hessian — because quasi-complete separation makes that solution the genuine maximum of the likelihood. A Gaussian ridge on the loadings adds curvature where the likelihood is flat and removes the runaway: measured at 47% of fits down to 0% at n = 100, and on one reproduction fit it takes the largest implied loading norm from 979.1 to 3.35.On the default Laplace route the penalty is opt-in and never applied unless you name it, so no existing Laplace fit changes. AGHQ is itself opt-in; once AGHQ is enabled, its default is the penalised
tau = 2MAP route unlessaghq_ridge = Infrequests an unpenalised fit.tauis the prior standard deviation on each free loading, and its scale is set by the model rather than tuned: the latent scores are standard normal by identification, so a loading is the trait’s latent standard deviation in link units, makingtau = 2weakly informative. Its influence also vanishes as the sample grows, because a fixed penalty contributes a constant against a log-likelihood growing with n. The penalty is rotation-invariant.Two costs, stated plainly. A penalised fit is a maximum-a-posteriori point, not a maximum-likelihood estimate, so
logLik(),AIC()andBIC()no longer describe it — setaghq_ridge = Infand refit every model being compared if you need likelihood-based comparison. And the penalty currently covers the unit-tier loadings only.check_gllvmTMB()now names this remedy when it reports a runaway loading.aghq_ridge = "auto"adds a narrower experimental, opt-in scale-aware route for pure single-trial Bernoulli models with one ordinary unit-tierlatent()block. It first runs an unpenalised 9-node multi-start AGHQ pilot, then usestau = min(6, max(1, ||Lambda_pilot||_F / sqrt(p q))). A plain Laplace fit is never used as the scale yardstick. If that pilot or the scale-aware final fit is unusable, the function returns an independently startedtau = 2fit and records the reason infit$aghq$ridge_auto; it does not silently claim that auto-selection succeeded. Both pilot and returned AGHQ fits use the fixed 9-node multi-start rule evaluated for failure/runaway avoidance in this Bernoulli grid; this is not interval calibration or general estimator certification. Conflictingaghqoraghq_multistartcontrols are replaced with a warning.This scope is supported as a runaway/failure-avoidance capability, not as a general accuracy improvement. In the 600-replicate evaluation, the auto fit was usable in 135 replicates. A transparent auto-when-usable, otherwise-
tau = 2policy did not increase per-cell failure or runaway rates and stayed within the preregistered +0.02 loading-error non-inferiority margin, but its six-cell macro-mean loading-error difference was +0.00282 (slightly worse). Caps 5, 6, and 8 were indistinguishable for fresh valid pilots; cap 6 is the smallest candidate supported by the stored wider-range rescore. Other families, covariance tiers, and likelihood-based model comparison remain outside this claim. The measured grid used logit,p = 6,q = 2, andn = 100,400, or1600; other links and dimensions are explicit extrapolations, not covered accuracy claims. The package default staystau = 2. -
Adaptive quadrature (
gllvmTMBcontrol(aghq = k)) now tries two starting points and keeps the better fit. It previously ran from a single start — the Laplace optimum — on the grounds that without a penalty there is nothing to choose between two starts. That holds for two starting points; it does not hold for two converged fits, which can simply be compared. It matters because the Laplace optimum is sometimes itself a runaway, and quadrature then inherits it: measured over 40 binomial fits at n = 100, sixteen ran away catastrophically, and on all sixteen the second start reached a strictly better objective — by 1.1 to 12.9 in negative log-likelihood — and a far more plausible loading matrix. Catastrophic fits fell from 16 in 40 to 1 in 40.The second start is data-driven and uses no knowledge of the truth; the cost is one extra adaptation run.
aghq_multistart = FALSErestores the previous single-start behaviour exactly. Note that this switch was documented but did not previously take effect, because it was not among the argumentsgllvmTMBcontrol()accepted. -
Quadrature fits can now report convergence at realistic sample sizes, and every stop says why. The convergence test compared the gradient against a fixed threshold. Because a likelihood’s gradient grows with the amount of data, that threshold became unreachable as the sample grew: no fit at n = 400 or n = 1600 could be certified as converged, in any family tried — including cases where the quadrature had landed on precisely the point the Laplace fit reported as converged. The test now also accepts a relative gradient (
aghq_grad_tol_rel). This changes the verdict, not the estimate: fits are identical either way.Every stopping condition now reports its gradient, including the one that previously reported none, so a genuine stall can be distinguished from a near miss. Read
fit$aghq$convergedfor the verdict — notfit$opt$convergence, which on this path records the optimiser’s per-pass iteration cap and therefore reports a limit even on a healthy fit.fit$aghqalso now carriesgrad_max,grad_rel, and how many starts were run. getLV()gains anse = TRUEargument that returns the standard error of every unit-level (or within-unit) latent score, alongside the scores, aslist(scores, se). The defaultse = FALSEis unchanged (a bare matrix). SEs come from the fitted model’s TMBsdreport()random-effect block and were cross-checked against an independent joint-precision-inversion route to machine precision. Requiresrotate = "none"; predictor-informedlatent(..., lv = ~ x)fits andengine = "julia"bridge fits are not yet supported and raise an informative error.gllvmTMB()now accepts both canonical long data and wide data through atraits(...)left-hand side. The two forms use the same fitting engine.Random slopes accept the
||uncorrelated coupling:mode(1 + x || g)fits the intercept and slope with no intercept-slope covariance (equivalentlymode(1 | g) + mode(0 + x | g)), alongside the correlated single-barmode(1 + x | g). Available forphylo_indep/animal_indep/kernel_indep(per-trait diagonal),phylo_dep/animal_dep/kernel_dep(blockSigma_int (+) Sigma_slope), and the source-tierphylo_latent/animal_latent/spatial_latent(which were already the uncorrelated form).Dense-kernel random slopes:
kernel_indep(1 + x | g, K = K)andkernel_dep(1 + x | g, K = K)(and their||forms) fit a random regression on a supplied dense kernelK, byte-equivalent to the phylogenetic path withvcv = K.Random-slope routes are runtime-permitted for lognormal and Student-t responses. This is fit admission only: direct route-specific recovery and inference evidence are not yet covered, so this release makes no scientific-validation claim for those combinations.
New
multinomial()response family for an unordered categorical response with three or more categories (baseline-category logit / softmax). It recovers the per-category intercepts and slopes as contrasts against a reference category, andpredict(type = "response")returns per-category probabilities. Usemultinomial(baseline = ...)to choose the reference category. The validation boundary is explicit: fixed-effect point recovery is validated — no detectable bias (|bias| ≤ 0.02 per coefficient across a 500-seed calibration, with recovery asserted on a 20-seed aggregate atK = 3andK = 4rather than any single fit) — and that is a statement about the estimates, not about their intervals. The two covariance routes — a singlephylo_latent()term, and the narrow ordinary shared-latent()cross-family route — are only partially validated, meaning they fit and report but their recovery has not been certified. The latter reports the nominal trait as itsK - 1baseline-contrast block rather than inventing one scalar categorical correlation; it permits one multinomial trait per fit and rejects unsupported tiers before TMB construction. Multiple multinomial traits, augmented slopes, explicit multinomialunique()/indep(), and unlisted source tiers remain blocked. A two-category response isbinomial(link = "logit"). For ordered categories useordinal_probit(). See the Unordered categories withmultinomial()article for a worked diet-guild example.phylo_latent()on amultinomial()trait (partially validated) reports the(K-1) x (K-1)among-category phylogenetic covariance V (how the category liabilities coevolve) viaextract_Sigma(fit, level = "phy", part = "shared", link_residual = "none"). The default total/"auto"extraction instead reports V plus the fixed softmax residual(pi^2/6)(I + J). Two honest caveats: recovery of V is data-hungry (it needs per-species replication or large N; a single categorical draw per species is weakly informative, so one-per-species point estimates are high-variance and can reach the +/-1 boundary), and V is on the baseline-contrast scale, so a diagonal V is not independence – the null contrast covariance is(I + J)-structured (equal diagonal, equal off-diagonal; the observation-scale link residual is applied as(pi^2/6)(I + J)– the softmax analog of binomial’spi^2/3). Treat this phylogenetic V route as recovery-oriented and data-hungry, not universally validated.For the admitted cross-family nominal route (partially validated), ordinary
latent()keeps its default diagonal companion but the current engine maps off multinomial-contrastPsi. That variance is not identified with one categorical draw per unit; replication can identify it in principle, but the current conservative implementation still suppresses it. Explicitly addingunique()orindep()for those contrasts remains fail-closed. Point extraction and target-specific Wald/bootstrap interval plumbing exist, but their repeated-sampling calibration is not covered. Nonlinear profile intervals are withdrawn.extract_cross_correlations()now restrictslevelto the ordinary unit tier for every method. Previously onlymethod = "profile"enforced this, solevel = "unit_obs","phy", or"spatial"combined withmethod = "point","wald", or"bootstrap"were reachable. Those combinations now raise a typed error. This is a deliberate reduction rather than a regression: the estimand for a source-tier cross-family correlation was never validated on those paths, and returning an uncalibrated number was worse than refusing. Useextract_Sigma()for source-tier covariance.-
Known limitation — random-slope covariance is not calibrated when each cluster carries little information. This is a limitation of the data regime, not of one keyword: it applies to any random-slope covariance fitted on single-trial binary responses with few observations per grouping level, and it affects both the current
phylo_indep()/animal_indep()/spatial_indep()slope forms and the soft-deprecated*_unique()forms. Measured on a phylogenetic slope fit with a logit link (60 species, 4 replicates, 3 traits — 12 single-Bernoulli observations per species), the whole 2x2 slope covariance is over-estimated, not just its slope entry:target true relative error intercept variance 0.40 0.82 slope variance 0.30 0.78 intercept-slope correlation 0.50 0.367 (absolute) The bias does not shrink with more clusters — it persists across 60, 120 and 240 species — on fits that are otherwise healthy (converged, positive-definite Hessian, valid
sdreport). The cause is too little information per cluster: with a handful of single-trial binary observations per species, the sampling variance of each species’ estimated slope is comparable to the true between-species variance itself, so roughly half the spread across species is sampling noise. The identical design recovers cleanly under a Gaussian response, which is what rules out an engine problem.Do not read a random-slope variance or correlation from sparse binary data as calibrated. The remedy is more information per grouping level — more replicates per species, or multi-trial
cbind(successes, failures)data instead of single 0/1 draws — rather than more species. Note also that the binomial slope routes are covered by a structural contract only: those tests check that the model fits and reports the right shapes, and deliberately do not certify variance recovery or interval calibration. The corresponding recovery test is deliberately skipped rather than passed by retuning its data-generating truth. The reader-facing covariance grammar crosses five correlation sources (
none,animal,phylo,spatial, andkernel) with three taught modes: independent, dependent, and latent. The one-shared-variance (“scalar”) case is the parsimony modifiercommon = TRUEon anyindepterm.The retained pkgdown guides now focus on runnable, numerically inspected workflows: morphometrics, Gaussian latent-rank selection, binary joint species distribution modelling, behavioural covariance, reaction norms, phylogenetic covariance, missing responses, response screening, fit diagnostics, profile routes, and formula/reference concepts.
predictive_check(), diagnostic residuals,diagnostic_table(),check_gllvmTMB(), andgllvmTMB_diagnose()provide complementary fitted-model checks. They diagnose a fitted response distribution and numerical health; they do not prove latent rank or interval calibration.extract_Sigma_table(),plot_Sigma_table(), andplot_correlations()provide report-oriented covariance and correlation displays. Correlation extraction is point-only by default; interval routes are explicit and carry an uncalibrated status rather than a coverage certificate.
Changed
-
The default number of bootstrap replicates is now 999, raised from 200.
bootstrap_Sigma()is the only exported function whose signature changes. The same raise was applied to the internal bootstrap paths behindextract_lv_effects(),extract_communality(),extract_repeatability(), the loading intervals, and the phylogenetic-signal intervals, so every bootstrap interval in the package now uses one replicate count — those extractors take longer and return slightly different bounds without any change to their own arguments. Calls that relied on the old default return slightly different interval bounds and take roughly five times longer. Passn_boot = 200tobootstrap_Sigma()to restore the previous behaviour; for exploratory work that remains a reasonable time-for-precision trade.Two distinct things bound this argument, and only the lower one is a correctness constraint. A percentile interval built from
Bdraws is bounded by its widest possible realisation, whose coverage cannot exceed(B - 1) / (B + 1)whatever the data are — so below roughly2 / (1 - conf)the interval cannot reach the requested level. Above that threshold what remains is Monte Carlo error in the endpoints, which is a precision question. The old default of 200 already cleared the ceiling comfortably (0.990 atconf = 0.95), so this change buys endpoint precision for intervals that get reported — it does not fix a correctness bug. 999 rather than 1000 so that(1 - conf) / 2 * (B + 1)is a whole number atconf = 0.95, letting the bounds land on order statistics instead of being interpolated between them.bootstrap_Sigma()refuses ann_bootbelow the arithmetic floor, warns below the default, and returns$coverage_ceilingso a simulation campaign can assertcoverage_ceiling >= confon its own configuration before trusting its own numbers. The guard and its tests are covered; bootstrap interval calibration for non-Gaussian and mixed-family fits remains uncertified. Ordinary
latent()now representsSigma = Lambda Lambda^T + Psiby default. Uselatent(..., unique = FALSE)for the earlier loadings-only subset. Source-specific and kernel latent terms remain loadings-only by default; passunique = TRUEwhen their intended covariance includes the diagonal companion.The one-shared-variance (“scalar”) covariance is now the parsimony modifier
common = TRUEon anyindepterm:indep(..., common = TRUE),phylo_indep(..., common = TRUE),animal_indep(..., common = TRUE),spatial_indep(..., common = TRUE), andkernel_indep(..., common = TRUE)fit one variance shared across all traits (intercept-only). The covariance grid is taught as three modes – independent, dependent, latent – withcommon =as the scalar sub-case, rather than a separate fourth mode.Current
phylo_indep(),animal_indep(), andspatial_indep()intercept-and-slope terms fit one independent 2 x 2 (intercept, slope) block per trait: within-trait correlation is estimated for|, fixed to zero for||, and cross-trait covariance is zero. Current*_dep()routes instead use a full 2T x 2T augmented covariance. The soft-deprecatedphylo_unique(),animal_unique(), andspatial_unique()slope forms retain their legacy shared 2 x 2 channels; they are not aliases for the current*_indep()shape. Admission is decided separately for each response family and each random-effect route, so a combination that fits is not thereby validated: some routes are admitted with recovery evidence, others are permitted at fit time only. This release does not publish a per-route coverage table, so the documentation will not tell you which of the two a given combination is. Treat a successful fit as evidence that the model is admissible, not that its variance components or intervals have been validated. Where a route is known to be weak this changelog says so explicitly — see the random-slope limitation above.fit$fit_healthseparates optimiser success, raw and objective-scaled gradients, Hessian health, andsdreport()availability. Itsconvergedfield is conservative: optimiser success, a finite objective, and a small raw maximum gradient are all required. Hessian health remains a separate inference check.extract_correlations()now returns point estimates by default. Fisher-z/Wald bounds are heuristic sensitivity summaries; bootstrap routes are target-specific and are not labelled as universally calibrated. The nonlinear penalty-profile prototype is no longer a public route.Reader-facing pages no longer expose internal validation identifiers, development phases, agent roles, or capability bookkeeping.
Fixed
-
Standardized loading inference now uses the model-implied total variance,
rho[t,k] = Lambda[t,k] / sqrt(Sigma_total[t,t]), rather than an entrywise loading-plus-scalar approximation.loading_ci(method = "wald_asym"),suggest_lambda_constraint(method = "wald_retention"), andsuggest_lambda_constraint(method = "varimax_threshold")now account for every latent axis in the trait denominator; the Wald routes propagate the full joint fixed-parameter covariance, including fitted variance components and parameter-dependent link residuals. Loading CI, flagging, plotting, bootstrap, andconfint(..., parm = "Lambda")outputs now label theirloading_scale; raw symmetric Wald inference remains the default.Deterministic algebra and routing are covered, but standardized interval coverage is not yet calibrated. Wald retention treats the fitted varimax rotation as fixed, and standardized profile/bootstrap intervals are not currently available. Per-axis loading intervals therefore remain rotation-conditional sensitivity summaries rather than the primary rotation-invariant evidence.
-
Ordination no longer collapses when the response is on a large scale. Starting values for the latent structure were built as though the response had a standard deviation of about 1 — a hardcoded loading start, a matching variance start, and latent scores beginning at exactly zero. Standardising the latent scores is precisely what pushes the response scale into the loadings, so where that assumption did not hold the fit could collapse: loadings, the covariance, the fixed effects, and the correlations and communality that most users actually report all came back wrong, with every convergence signal green. Nothing warned you.
All three starts now follow the data. The loadings and the variance term are placed on the scale of the working residuals, and the latent scores are seeded from the data instead of from zero — scaled to unit variance, since the scores are standardised by definition and it is the starting direction that was missing, not the magnitude.
For an ordinary single-tier
latent()model on 4 traits, multiplying the response by 100 now reproduces every expected transformation to within about one part in 100,000. At a factor of 5000 it holds to about 1% in the worst of eight simulated datasets — looser, but well inside the 2% we accept, and the quantities most people report (the correlations and the communality) hold there too. Two other implementations of the same model do not hold that law on the same data: their worst cases are roughly 100% and 200% out, meaning a loading that should have doubled did not move. If you have worked around this by rescaling your response by hand, you no longer need to.This covers Gaussian responses, and the ordinary latent structure on your unit grouping —
latent()and the variance term it carries. Everything else deliberately keeps its previous starting values: any model with a non-Gaussian response, the phylogenetic, spatial, kernel and random-slope latent terms, and the second grouping of a nested two-tier fit (the within-unit level, e.g. species-within-site). Starting values for the latent scores are also left alone when you givelatent()a predictor withlv = ~ ..., because there the scores have a fitted mean rather than a free one.The reason for drawing the line there rather than wider: “multiply the response by 100” is only a meaningful thing to do to an unbounded continuous response, and that is the only case the collapse was measured on. A count or a presence/absence on an awkward scale may well have the same problem — but moving a starting value that has not been measured trades a known problem for an unmeasured one, which is the whole reason the old default was dangerous. If you fit any of the excluded forms on a response far from unit scale, rescaling it by hand is still worth doing.
One case is not fully resolved. In a nested two-tier fit the residual scale error is around 2%, and that remainder is a property of the likelihood surface rather than of the starting values — a fit can report convergence while sitting some distance from the optimum, because a wide region of parameter space is nearly flat there. That is tracked separately and no starting value will address it.
-
check_gllvmTMB()no longer passes a binomial fit whose loading has run away. The loading row could only fire when the trait’s marginal prevalence was also extreme (at or beyond 0.9). But quasi-complete separation is a property of the fitted linear predictor, not of the marginal rate, so it runs a loading away while prevalence stays entirely ordinary — and the row was keyed on a quantity the pathology does not move. Across 3,944 simulated binomial fits the worst-affected trait’s prevalence never left 0.20 to 0.807, and its distance from 0.5 was essentially uncorrelated with the size of the blow-up, while loadings reached 24,000 times the typical trait’s. On one Bernoulli fit the row reportedPASSwith a loading 6,980 times typical and every fitted probability saturated, while the implied covariance was wrong by a factor of 156,645. A loading at or beyondloading_runaway_thresh(new argument, default 25) now reports on its own, naming the improper solution (Heywood case) and pointing at the loading penalty ingllvmTMBcontrol(aghq_ridge = ). The existingloading_relative_threshof 8 keeps its prevalence conjunct, because healthy fits with a sparse loading structure reach that level routinely — so nothing that was flagged before stops being flagged, including a genuinely near-constant trait, which the row still reports as it always did.A second, complementary criterion is added alongside it. A relative criterion cannot see a loading matrix that is inflated as a whole, because scaling every loading leaves every ratio unchanged — so
loading_absolute_thresh(new argument, default 6) reports a loading that is simply too large on the link scale. That threshold is meaningful because the latent scores are standard normal by identification, making a binomial loading the trait’s latent standard deviation in link units; a value of 6 already implies a fitted probability indistinguishable from 0 or 1 across an ordinary swing of the axis. Measured over the same 3,944 fits: no healthy fit exceeded 3.99, none was flagged, and it reported 97.3% of degenerate fits — catching 14 that the relative criterion missed. Being a link-scale quantity it does not transport to families whose response scale is arbitrary, which is why this row remains binomial-only.Two limits on the calibration are stated plainly. It was measured on single-family binomial fits at the true latent rank, where it reports 96.3% of fits whose implied covariance is wrong by a factor of five or more, with no healthy fit reaching the threshold (the largest was 12.1). When the fitted rank is larger than the truth, several traits can inflate together, which lifts the very yardstick the ratio is measured against; in one such run the row missed 3 of 8 degenerate fits, and they were the three worst. A scale diagnostic, not a ratio, is the right instrument for that case and none is wired yet.
-
check_gllvmTMB()now reports a unique variance that has collapsed only relative to its siblings. A Heywood case in a Gaussian or Poisson fit usually appears as a per-trait unique variance driven to the boundary, not as a runaway loading — and becausepsiis estimated on the log scale, the boundary is an interior point of the transformed space, sopdHessstays positive definite and nothing else objects. Across 360 fits with a deliberately over-specified latent rank, 58% drove a unique standard deviation below a tenth of its true value while reportingconvergence = 0andpdHess = TRUE— one reached 6e-50. The covariance itself was still recovered to within 7%, so this is a failure of theLambda Lambda' + Psidecomposition rather than of the fitted covariance, and no recovery-based check can see it.psi_rel_threshis raised from 0.001 to 0.01, which reports 96.2% of those fits rather than 73.7%. The measured false-positive rate is zero both on 151 healthy fits and on 359 healthy fits whose true unique variances differ by up to a factor of 1000 — the case that decides whether the number transports, since a small ratio is then correct rather than pathological. Looser values do not transport: 0.1 reaches full sensitivity but flags 19% of those healthy fits. Some fits that previously passed will now warn; on this evidence they are fits with a genuinely boundary-pinned component. The typical loading size is now taken over the traits being screened. Previously it pooled every trait in the fit regardless of family, so in a mixed-family model a trait on a large response scale could set the yardstick for a binomial one — masking a genuine runaway, or manufacturing a spurious one when the other family’s loadings were small.
Profile confidence intervals no longer lose their bounds at higher confidence levels. The profile search used a fixed deviance budget that did not depend on
level, and that budget was smaller than the threshold a profile must cross for any level above roughly 0.955. Above it the search could stop short and the bound was reported as infinite. On a four-trait Gaussian fit,level = 0.99returned an infinite bound for four of ten targets wherelevel = 0.95returned all ten finite. The budget is now sized from the requestedlevel, with headroom — merely reaching the threshold is not enough, because the bound is located by interpolating across it. The defaultlevel = 0.95also gains margin it did not previously have.An unbounded interval is no longer reported where the bound is simply unknown. A profile that stops without crossing its threshold can mean two different things, and only one was reported. If the profile has flattened out, no finite bound exists and an infinite bound is the honest answer. If the profile was still climbing when the search stopped, the bound is unknown — calling it infinite asserts an unbounded parameter on the strength of having stopped looking. The two are now distinguished from the shape of the profile itself, and the second returns
NA.Interval bounds are more accurate. Bounds are now interpolated on the signed-square-root (
zeta) scale rather than the deviance scale. For a quadratic log-likelihoodzetais exactly linear in the parameter, so the interpolation is exact there; on the deviance scale it carried curvature error that grew with the spacing of the profile grid. Reported bounds may shift slightly, and are closer to what a finer grid would give.The internal coverage-study helper no longer counts an interval it could not compute as a success. A missing bound was already treated as non-coverage, but an infinite bound fell through that guard, was scored as covering, and stayed in the denominator. A method returning an unbounded interval on every replicate would have reported perfect coverage. Missing and infinite bounds are now both excluded and reported as excluded. The helper is internal and unexported; the correction is recorded because any coverage figure produced with an earlier version of it is affected.
Near-zero variance components are now detected relative to their siblings, not only against an absolute threshold. A boundary-pinned (Heywood) component — one trait’s unique variance collapsing to zero — could previously pass every check the package had:
check_gllvmTMB()reportednear_zero_psi_unit … PASSandfit_health$boundary_flagsstayed empty for a component whose variance was six orders of magnitude below the others. The absolute threshold is expressed on the standard-deviation scale, so the old1e-4demanded a variance below1e-8before flagging anything.pdHesscould not catch it either:psiis estimated on the log scale, so a collapsed component is an interior point of the transformed parameter space and the Hessian stays positive definite there.check_gllvmTMB()gainspsi_rel_thresh(default0.001) and reports the offending ratio in the check message;.gllvmTMB_boundary_flags()applies the same relative test to every variance block it already covered. Fits that were flagged before are still flagged.A diagonal covariance term is no longer duplicated when the
unitandclustercolumns are the same grouping factor. This removes a flat variance split and restores coherent covariance extraction and Wald infrastructure.extract_phylo_signal()now uses the declared species-level denominator in crossed site-by-species designs instead of silently returning one for every trait when non-phylogenetic species variance was stored at the cluster tier.phylo_dep()is treated as a full covariance parameterisation, not as a set of exchangeable latent axes, in rotation and weak-axis diagnostics.Missing or undefined link-scale residual variances now propagate as
NAinstead of being replaced by zero or another finite fallback. In particular, Student-t variance is undefined when its degrees of freedom are at most two.Several optional Julia-bridge shape, dispatch, missing-cell, and confidence- interval error paths now fail explicitly instead of silently returning malformed output. The bridge remains experimental.
Deprecated compatibility syntax
The
sigma_d2argument toloading_ci(),suggest_lambda_constraint(), andsuggest_lambda_constraints()is deprecated and ignored. Standardized loading denominators are now derived from each fitted model’s total trait variance.-
gllvmTMBcontrol(start_method = list(method = "res"))is soft-deprecated and warns once per session. It still fits; prefer the default starts.It was retired on measurement. Across 89 simulated fits — Gaussian, Poisson and negative-binomial,
d = 1to3, three and five traits — the residual start was never materially better than the default start (its three best margins were 0.07, 0.29 and 0.66 log-likelihood units, the scale of landing on a slightly different point of the same optimum), was materially worse eight times, once by 14.6 units, and was exactly neutral atd >= 2(objectives agreed to seven decimals in 34 of 34 fits). Every failure reportedconvergence == 0and a positive-definite Hessian on both sides, so the worse fit was silent, and restarts were not a guard: in one such fit all fiven_init = 5restarts returned the same worse optimum.The damage concentrates at
d = 1with three traits, the exactly identified corner where a per-trait variance can collapse to zero. Seeding from the residual covariance commits the optimiser to that matrix’s leading direction, which there is not the best-likelihood factor. This is not a residual-noise problem — a start built from noise-free random-effect estimates lands in the same wrong place — so it is not fixable by a better residual. The formula parser continues to accept
unique()as compatibility syntax; source-specific*_unique()functions remain exported soft-deprecated aliases. Useindep()/*_indep()in new standalone diagonal formulas.The scalar family –
scalar(),phylo_scalar(),animal_scalar(),spatial_scalar(),kernel_scalar()– is soft-deprecated compatibility syntax that emits a one-time warning and keeps working. Useindep(..., common = TRUE)/*_indep(..., common = TRUE), which fits the same model.gllvmTMB_wide()remains available for migration, but new wide examples usegllvmTMB(traits(...) ~ ...).meta_known_V()remains a deprecated alias ofmeta_V().
Known limitations
-
Variance components: what is and is not claimed. Point estimates are the package’s primary inferential output, but their evidence is route- and regime-specific. Broad package-wide interval coverage is not certified. Three exact native pinned unrotated ordinary-Gaussian standardized-loading Wald cells have target-specific certificates:
(n_units=150,d=2),(n_units=400,d=1), and(n_units=400,d=2), restricted to structurally free strict-lower targets. This is one frozen DGP: trait intercepts(-0.20, 0.10, 0.25), unique standard deviations(0.70, 0.80, 0.90), and loading vector(0.80, 0.45, -0.35)ford=1, with second column(0, 0.70, 0.40)ford=2. Coverage is conditional on eligible fits (optimizer convergence, converged fit health, availablesdreport(), and a positive-definite Hessian); availability was 98.82%, 93.38%, and 96.18% in the three certified cells. Then_units=150,d=1cell failed. No other truth-parameter regime inherits the result. Pinned diagnostics, Fisher-z Wald, arbitrary constraints, rotated or neighbouring regimes, and the global loading route remain uncalibrated.profile_ci_total_variance()now labels every computed penalty-profile approximationroute-only; its formercertified-0.94labels are withdrawn because retained endpoints do not prove exact constrained-refit convergence or target attainment. Intervals are route output, not a coverage guarantee. - Interval support is target-specific. A route that returns bounds is not, by itself, evidence of nominal repeated-sampling coverage.
- The previous public
check_identifiability()andcoverage_study()prototypes have been withdrawn from the exported surface. Their fitted-model simulation designs did not establish unknown generating rank or retain every attempted replicate in the coverage denominator. They remain internal until redesigned around predeclared known-data-generating targets and complete failure accounting. - Nonlinear penalty-profile prototypes for communality, correlation, variance proportions, and predictor-informed latent effects have been withdrawn from the exported surface. The approximation could accept loose constraints or unusable constrained optimisations without a complete status ledger. Direct TMB parameter profiles and simple linear-contrast profiles remain available; nonlinear routes will return only after an exact constraint and failure- diagnostic contract is verified.
- For single-trial Bernoulli cells, a default per-trait diagonal random effect can be unidentifiable and is mapped off. The fixed link-scale residual defines the liability convention; multi-trial or genuinely repeated designs have different information.
-
meta_V()remains an important development target, but a dedicated public article will wait until its supported estimands, diagnostics, and validation evidence form a complete reader path. - More advanced or weakly evidenced draft articles have been retired rather than published as capability claims. Their topics can return when the underlying model, extractor, diagnostics, comparison, and uncertainty path are ready.
