Package {tulpa}


Title: Template Unified Latent Process Architecture for Bayesian Hierarchical Models
Version: 0.2.0
Description: A general-purpose engine for fitting Bayesian hierarchical models with spatial fields, temporal effects, spatially varying coefficients, and multiple inference backends. Scalable spatial structure includes Hilbert space approximate Gaussian processes (HSGP; Riutort-Mayol et al. 2023 <doi:10.1007/s11222-022-10167-2>), nearest-neighbor Gaussian processes (NNGP; Datta et al. 2016 <doi:10.1080/01621459.2015.1044091>), intrinsic conditional autoregressive models (ICAR; Besag, York, and Mollie 1991 <doi:10.1007/BF00116466>), the reparameterized Besag-York-Mollie model (BYM2; Riebler et al. 2016 <doi:10.1177/0962280216660421>), and stochastic partial differential equation fields (SPDE; Lindgren, Rue, and Lindstrom 2011 <doi:10.1111/j.1467-9868.2011.00777.x>). Temporal structure covers random walks, autoregressive processes, and Gaussian processes. Inference is tiered by correctness guarantee: exact Hamiltonian Monte Carlo with the No-U-Turn sampler, Laplace and nested Laplace approximations with hyperparameter integration (Rue, Martino, and Chopin 2009 <doi:10.1111/j.1467-9868.2008.00700.x>), and variational inference. Model-specific packages plug observation likelihoods into the engine through a templated C++ callback interface.
License: MIT + file LICENSE
Encoding: UTF-8
Language: en-US
SystemRequirements: C++17
Depends: R (≥ 4.1.0)
Imports: Rcpp (≥ 1.0.12), Matrix, tulpaMesh (≥ 0.1.3), generics, lifecycle, stats, tools, utils, methods, graphics, grDevices
Suggests: testthat (≥ 3.0.0), bayesplot, ggplot2, sf, terra, knitr, rmarkdown, posterior (≥ 1.5.0), loo (≥ 2.7.0), rstantools, lme4, nlme, lmtest, numDeriv, spdep, MASS, betareg, glmmTMB, pscl, tweedie, fmesher, patchwork, stars, statmod, withr
LinkingTo: Rcpp, RcppEigen, Matrix
Config/testthat/edition: 3
VignetteBuilder: knitr
URL: https://github.com/gcol33/tulpa, https://gillescolling.com/tulpa/
BugReports: https://github.com/gcol33/tulpa/issues
Config/roxygen2/version: 8.0.0
NeedsCompilation: yes
Packaged: 2026-08-27 23:30:17 UTC; Gilles Colling
Author: Gilles Colling ORCID iD [aut, cre, cph], Frances Y. Kuo [ctb, cph] (Sobol direction numbers in src/sobol_direction_numbers.h, BSD-3-clause), Stephen Joe [ctb, cph] (Sobol direction numbers in src/sobol_direction_numbers.h, BSD-3-clause)
Maintainer: Gilles Colling <gilles.colling051@gmail.com>
Repository: CRAN
Date/Publication: 2026-09-09 15:30:02 UTC

tulpa: Template Unified Latent Process Architecture for Bayesian Hierarchical Models

Description

A general-purpose engine for fitting Bayesian hierarchical models with spatial fields, temporal effects, spatially varying coefficients, and multiple inference backends. Scalable spatial structure includes Hilbert space approximate Gaussian processes (HSGP; Riutort-Mayol et al. 2023 doi:10.1007/s11222-022-10167-2), nearest-neighbor Gaussian processes (NNGP; Datta et al. 2016 doi:10.1080/01621459.2015.1044091), intrinsic conditional autoregressive models (ICAR; Besag, York, and Mollie 1991 doi:10.1007/BF00116466), the reparameterized Besag-York-Mollie model (BYM2; Riebler et al. 2016 doi:10.1177/0962280216660421), and stochastic partial differential equation fields (SPDE; Lindgren, Rue, and Lindstrom 2011 doi:10.1111/j.1467-9868.2011.00777.x). Temporal structure covers random walks, autoregressive processes, and Gaussian processes. Inference is tiered by correctness guarantee: exact Hamiltonian Monte Carlo with the No-U-Turn sampler, Laplace and nested Laplace approximations with hyperparameter integration (Rue, Martino, and Chopin 2009 doi:10.1111/j.1467-9868.2008.00700.x), and variational inference. Model-specific packages plug observation likelihoods into the engine through a templated C++ callback interface.

Author(s)

Maintainer: Gilles Colling gilles.colling051@gmail.com (ORCID) [copyright holder]

Authors:

Other contributors:

See Also

Useful links:


Reject non-finite fitting inputs.

Description

The design is built with na.action = na.pass, so a missing predictor or response survives into X / y. tulpa() does not drop incomplete cases, so an NA/NaN/Inf would propagate into the C++ kernels as a NaN estimate. Fail loudly with the offending row instead.

Usage

.assert_finite_model_inputs(X, y)

Resolve a supplied fixed-effect prior to its Gaussian ⁠(mean, sd)⁠ fields

Description

The one place that decides whether a beta_prior argument can be expressed as a Gaussian prior on the fixed effects at all. .normalize_beta_prior() (per-coefficient) and .beta_prior_ridge_sd() (scalar ridge) both resolve through it and differ only in the shape they recycle the fields to, so a prior one accepts is a prior the other accepts.

Usage

.beta_prior_fields(beta_prior)

Arguments

beta_prior

A list (or tulpa_prior object) carrying sd and optionally mean.

Details

A prior with no sd is an input the fitters cannot express: substituting the default for it would replace the user's modelling statement with a different one and leave no trace on the posterior.

Value

list(mean, sd), unrecycled.


Resolve a beta_prior = list(mean, sd) to a single ridge SD.

Description

For the fitters whose fixed-effect prior is a mean-zero Gaussian ridge with a scalar SD (EP, multinomial, ordinal): validates the unified beta_prior object, enforces a mean of 0, and returns the positive scalar SD. Keeps the shared prior interface (beta_prior = list(mean, sd)) while rejecting the options those fitters do not implement (a non-zero mean, a per-coefficient SD). Validation of the supplied object itself is .beta_prior_fields(), shared with .normalize_beta_prior().

Usage

.beta_prior_ridge_sd(beta_prior, default_sd = .tulpa_prior_sd("ridge"))

Arguments

beta_prior

list(mean, sd); mean must be 0, sd a positive scalar. NULL takes the engine default.

default_sd

SD used when beta_prior is NULL. A supplied prior is resolved through .beta_prior_fields() and never falls back to this.


Assemble NUTS initial positions and an inverse-mass diagonal

Description

Assemble NUTS initial positions and an inverse-mass diagonal

Usage

.build_warm_start(
  fit,
  layout,
  re_terms,
  n_chains,
  sigma_re = NULL,
  jitter = 1,
  metric = FALSE
)

Arguments

fit

Source fit (mode = "eb" or "laplace").

layout

The target sampler layout, from cpp_tulpa_glmm_layout().

re_terms

Random-effect terms of the model being sampled.

n_chains

Number of chains to initialise.

sigma_re

Random-effect SDs, when the source fit conditioned on them.

jitter

Scale of the between-chain dispersion, as a multiple of each parameter's warm-start SD. 0 stacks every chain on the mode.

metric

Hand the assembled inverse-mass diagonal to the kernel as well as the position. FALSE (default) because it can only be filled for some blocks today and a partial metric measurably slows the sampler; see the note at the end of this function.

Value

⁠list(init = <n_chains x D matrix>, inv_metric_diag = <D vector or NULL>)⁠.


Resolve a family spelling to its canonical registry name.

Description

A no-op for a canonical name, for a ⁠<family>_<link>⁠ code, and for anything unrecognized, which reaches .family_or_stop() and errors there against the canonical list.

Usage

.canonical_family(family)

Arguments

family

Family identifier as supplied by the caller.


Proper-CAR precision Q = tau * (D - rho W) at fixed (tau, rho).

Description

D is the diagonal degree matrix (neighbour counts) and W the adjacency, so Q has the same nonzero structure as the ICAR precision with rho scaling the off-diagonals. Matches tulpa::add_car_proper_prior / the nested CAR_proper precision builder.

Usage

.car_proper_precision_Q(spatial, tau, rho)

Families whose dispersion can be estimated

Description

Families whose dispersion can be estimated

Usage

.dispersion_families()

Dispersion derivatives for a family, or NULL

Description

Dispersion derivatives for a family, or NULL

Usage

.family_dphi(family)

Arguments

family

Family name, as registered in .FAMILY_OPS.

Value

A list with dloglik, dscore and dweight, or NULL when the family has no free dispersion or none has been derived. NULL is a refusal to estimate, never a signal to fall back to a fixed value silently.


Second-order dispersion derivatives for a family, or NULL

Description

Second-order dispersion derivatives for a family, or NULL

Usage

.family_dphi2(family)

Arguments

family

Family name, as registered in .FAMILY_OPS.

Value

A list with dloglik2, dscore2, dweight2 and dweight_deta, or NULL when the family's phi Hessian has not been derived. NULL is a refusal, not a fixed-value fallback: it hands the phi Hessian to the differencing stencil rather than reporting an inexact closed form.


Whether a family's phi Hessian additionally needs d(W_obs)/dphi

Description

TRUE whenever the Newton working weight is not the observed curvature, which is exactly when the border's Hinv and the mode motion's Hinv_mode are different matrices. For a family whose weight is also free of eta the two inverses multiply a zero channel and the answer would come out the same either way, but the entry is required regardless rather than resting on that second coincidence.

Usage

.family_dphi2_needs_obs(family)

Observed curvature (-d^2 log-lik / d eta^2 at the realized y), elementwise.

Description

Falls back to the y-free weight for families whose curvature carries no y – those where the response enters the log-likelihood linearly in eta (Poisson, binomial, and the zero-truncated Poisson), for which the observed and expected curvature coincide identically rather than only in expectation.

Usage

.family_obs_weight(eta, y, family, n_trials = NULL, phi = 1, phi2 = NULL)

Look up a family's operation set, with a clear error for unknown families.

Description

Look up a family's operation set, with a clear error for unknown families.

Usage

.family_ops(family)

Build a length-n_obs -> length-n_units indicator design matrix Z.

Description

The latent field lives on units – unique coordinates for an NNGP field, adjacency units for an areal one – and each observation belongs to exactly one, so Z[i, j] = 1 iff observation i sits on unit j.

Usage

.field_design_Z(obs_to_unit, n_units, n_obs)

Arguments

obs_to_unit

Per-observation unit index, or empty for one observation per unit.

Details

The map is passed in rather than read off the spec, because the two specs that reach here name it differently: a GP spec carries obs_to_loc, an areal spec carries spatial_idx. Reading one name served only one caller; for the other the field was absent, the map silently became seq_len(n_obs), and the column index ran past n_units as soon as a location carried more than one observation.


Attach the standard tulpa_fit contract to a fitter's result.

Description

The dispatch layer and the fitters that return generic-accessor-facing objects route their return value through this helper (some special-purpose fitters – the logpost samplers, tulpa_ep, the categorical drivers – still stamp their class by hand), so a directly-called fitter and a tulpa()-dispatched one yield the same enriched object: the tulpa_fit class (so the generic S3 methods – coef / summary / vcov / confint / tidy / glance / ranef – dispatch), the fixed-effect layout (n_fixed / fixed_names / param_names, which the .fit_fixed_table summary path reads), and an explicit posterior-provenance tag (draws_kind) the chain-vs-iid diagnostic gate (.tulpa_draws_kind()) reads to decide whether Rhat/ESS apply. Each field is filled only when the fitter did not already set it, so a fitter that knows better wins and the helper is idempotent under tulpa_dispatch().

Usage

.finalize_fit(
  fit,
  backend = NULL,
  draws_kind = NULL,
  n_fixed = NULL,
  fixed_names = NULL,
  param_names = NULL,
  extra_class = NULL
)

Arguments

fit

The fitter result (a list); returned unchanged if not a list.

backend

Backend key (sets ⁠$backend⁠; supplies the default draws_kind via the registry emits property when it is a registry key).

draws_kind

Explicit "chain" / "iid" / "point" tag; used when the backend is absent from BACKEND_REGISTRY or to override the registry.

n_fixed, fixed_names, param_names

Fixed-effect layout, each filled only when the fitter left it unset.

extra_class

Subclass(es) to prepend before tulpa_fit.

Details

draws_kind precedence is: a value already on the fit, then the explicit draws_kind argument, then the registry emits property for backend. Backends that are not registry keys (the ⁠tgmrf_*⁠ fitters) must pass draws_kind explicitly, since their emits cannot be looked up.

Value

The enriched fit, classed c(extra_class, ..., "tulpa_fit").


Connected components of an adjacency graph, as a list of node-index vectors.

Description

Iterative depth-first search, so it carries no recursion-depth risk on a large map. Returns what graph_partition (inst/include/tulpa/graph_components.h) returns – the actual component MEMBERSHIP, not just a count – so a genuine disconnected map (a mainland plus islands) pins each component's constant over that component's real nodes rather than an equal-size contiguous split. Takes the dense adjacency rather than a sparse one because symmetric sparse storage (dsCMatrix) keeps a single triangle, which would put an edge's two endpoints in different components.

Usage

.graph_components(adjacency)

Number of connected components of an adjacency graph.

Description

The count of .graph_components, single-sourcing the DFS.

Usage

.graph_n_components(adjacency)

Intrinsic ICAR field precision, augmented to full rank.

Description

L = D - W (degree minus adjacency) plus the sum-to-zero augmentation ⁠sum_c 1_c 1_c' / J_c⁠ that identifies each component's constant null direction (inst/include/tulpa/sum_to_zero.h). The components are the actual connected components of the graph (.graph_components), each pinned over its own node set of size J_c – matching for_each_icar_component (src/icar_kernel.h), so an unequal / non-contiguous disconnected map reconstructs the same precision the kernel penalizes with.

Usage

.icar_precision_Q(spatial)

Details

The conditional Laplace kernel passes tau_spatial = 1 on both the ICAR/CAR path (R/fit_laplace.R) and the BYM2 structured block (which carries sigma and rho in its d_fac instead), and the field enters the linear predictor with d_fac = 1, so this is exactly the field block of that fit's joint Hessian. test-marginal-se-areal.R pins it against the kernel's own log_prior_icar.


Marginal H_beta for a BYM2 field. The latent is the two-block ⁠[phi (structured), theta (unstructured)]⁠ convolution; each enters the linear predictor through the field indicator scaled by its d_fac (sigma * sqrt(rho) * scale_factor for phi, sigma * sqrt(1 - rho) for theta), with phi carrying the augmented ICAR precision .icar_precision_Q and theta ~ N(0, I). The conditional Laplace kernel hardcodes sigma = 1, rho = 0.5.

Description

Marginal H_beta for a BYM2 field. The latent is the two-block ⁠[phi (structured), theta (unstructured)]⁠ convolution; each enters the linear predictor through the field indicator scaled by its d_fac (sigma * sqrt(rho) * scale_factor for phi, sigma * sqrt(1 - rho) for theta), with phi carrying the augmented ICAR precision .icar_precision_Q and theta ~ N(0, I). The conditional Laplace kernel hardcodes sigma = 1, rho = 0.5.

Usage

.marginal_H_beta_bym2(
  mode,
  X,
  spatial,
  family,
  phi,
  n_trials,
  weights = NULL,
  offset = NULL,
  sigma_spatial = 1,
  rho = 0.5,
  re_idx = NULL,
  n_re_groups = 0L,
  sigma_re = 1
)

Negate an additive term (numeric literal -> its negative; anything else -> a unary-minus call), for flattening binary - in a bar LHS.

Description

Negate an additive term (numeric literal -> its negative; anything else -> a unary-minus call), for flattening binary - in a bar LHS.

Usage

.negate_term(t)

Build NNGP precision Lambda = (I - A)' D^-1 (I - A) at given hyperparameters.

Description

Mirrors the algorithm in src/gpu_nngp_laplace.h::batch_nngp_scatter plus apply_nngp_full_prior_dense – for each Vecchia row i (in NNGP order):

Usage

.nngp_precision_Q(spatial, sigma2_gp, phi_gp)

Details

Identifiers follow the C++ side: nn_idx[i, k] is a 1-based NNGP-order index, nn_order[j] maps NNGP-order j (1-based) to obs idx (1-based).


Normalize an optional fixed-effect Gaussian prior

Description

Validates beta_prior and recycles scalar mean / sd to length p. Returns NULL (use the built-in weak prior) or list(mean, sd) with both vectors of length p. Shared by tulpa_laplace() and the EM driver so the validation rules live in one place.

Usage

.normalize_beta_prior(beta_prior, p)

Arguments

beta_prior

NULL, or a list with sd (required) and optional mean.

p

Number of fixed effects (ncol(X)).


Normalize a zero-inflation prior to a scalar SD

Description

The compiled zero-inflated kernels carry one mean-zero Gaussian prior over the whole beta_zi block (ModelData::zi_prior_sd on the sampler paths, the appended BetaPrior tail on the Laplace path), so the prior is a single SD rather than the per-coefficient list(mean, sd) that beta_prior takes. The list form is kept so the two priors read alike at the front door and so a per-coefficient or non-zero-mean ZI prior can be added without a signature change.

Usage

.normalize_zi_prior(zi_prior)

Arguments

zi_prior

NULL, or a list with a scalar sd.

Value

A scalar prior SD; the engine default when zi_prior is NULL.


Interpret an RE term's covariance specification

Description

Maps one re_list element to the two representations the Laplace path needs: pack, the value the C++ kernel consumes (a length-n_coefs marginal-SD vector for a diagonal / uncorrelated term, or a packed lower-triangular Cholesky of length n_coefs (n_coefs + 1) / 2 for a correlated one), and Q, the ⁠n_coefs x n_coefs⁠ RE precision Sigma^{-1} used to build the marginal fixed-effect SE. A correlated term is signalled by r$L (a lower- triangular Cholesky factor, ⁠Sigma = L L'⁠) or r$cov (the covariance matrix); when present these take precedence over r$sigma.

Usage

.re_cov_spec(r)

Arguments

r

One element of a re_list (see tulpa_laplace()).

Value

list(pack, Q, diagonal).


n_obs x n_re_groups indicator design for an iid RE block.

Description

re_idx is the 1-based per-observation group index. Observations whose group falls outside 1:n_re_groups contribute no RE column, matching the g >= 0 && g < n_re_groups guard in the C++ kernels.

Usage

.re_design(re_idx, n_re_groups, n_obs)

Resolve a warm_start request into sampler init / inverse-mass vectors

Description

Accepts the front door's warm_start: "eb" or "laplace" to run that fit first, or an already-fitted object from either. Returns NULL when no warm start was asked for, so the caller passes nothing and the kernel keeps its own defaults.

Usage

.resolve_warm_start(warm_start, args, re_terms, sigma_re, beta_prior, n_chains)

Arguments

warm_start

NULL, "eb", "laplace", or a fitted tulpa_fit.

args

The assembled sampler arguments (the same values the kernel will receive), used both to run the source fit and to probe the target layout.

re_terms

Random-effect terms in tulpa_eb() / tulpa_laplace() form.

sigma_re

Random-effect SDs to condition on, for the Laplace source.

beta_prior

Optional fixed-effect prior, threaded into the source fit.

n_chains

Number of chains the sampler will run.


Schur complement of the latent block out of the joint Hessian.

Description

Given the fixed-effect design X, the combined latent design D (the iid RE indicator block stacked with the spatial-field design), the latent prior precision Q_latent, and the GLM weights W, returns the marginal fixed- effect precision ⁠H_beta = X'WX - (X'WD) (D'WD + Q_latent)^{-1} (X'WD)'⁠. Shared by the SPDE and NNGP marginal-SE paths.

Usage

.schur_H_beta(X, D, Q_latent, W)

Redirect a backend selection: swap the backend and restamp mode / tier / tier name from the registry, recording the reason shown on the fit.

Description

A redirect off an EXPLICIT request (anything but mode = "auto") is an override, not a resolution: the fit does what the model structure requires instead of what the caller asked for. That is recorded on the selection – sel$overridden for callers, and a clause appended to the reason the fit reports – so an overridden fit is distinguishable from one that was never asked for a mode at all. Only the FIRST override is recorded: with several redirects in a chain, the request the user actually made is the one worth naming, not the intermediate backend a previous redirect chose.

Usage

.sel_redirect(sel, backend, reason, notify = TRUE)

Details

notify says whether tulpa() should additionally WARN. It is for the case where the requested backend could have fitted the model and the redirect takes a capability away – a smoother sending an eb / agq request to the nested kernels loses the random-effect SD those two would have estimated. It is off where the requested mode is not expressible for the structure at all and the redirect is the documented resolution rather than a loss: a random slope has no scalar sigma_re for mode = "laplace" to condition on, and an SPDE field redirected from nested_laplace to spde is the same mode and tier reaching its own integrator. Those are recorded, not warned about, so a documented route does not warn on every fit.


Assemble the fractional rSPDE precision and obs map at a given (range, sigma)

Description

Shared by every fractional-nu fit path (Laplace single-point, nested, NUTS, marginal SEs). Maps ⁠(range, sigma)⁠ to the SPDE hyperparameters ⁠kappa = sqrt(8 nu) / range⁠, builds the latent precision ⁠Q = Pl' C^{-1} Pl⁠ and field shift Pr (field ⁠u = Pr x⁠) from the validated R oracle .spde_rational_assemble(), then normalizes the field to marginal variance sigma^2. The rational construction is correct in spectral shape but its overall scale carries kappa-dependent constants (the l_max normalization, the per-factor conditioning rescalings); without the variance normalization the implied field variance is not sigma^2 and varies with the range, which biases the nested ⁠(range, sigma)⁠ integration. The normalization rescales Pr (hence ⁠A_eff = A Pr⁠) by ⁠sigma / sqrt(mean marginal variance)⁠; Q is untouched, so logdet_Q remains the correct prior normalizer for the variance-normalized model.

Usage

.spde_assemble_at(spatial, range, sigma, order = 2L)

Arguments

spatial

A validated spatial_spde spec with fractional nu.

range, sigma

Spatial range and marginal SD.

order

Rational approximation order. Default 2 (the rSPDE convention), which keeps the rational precision's condition number tractable.

Value

A list with Q, Pr, A_eff, Pl (all CSC, sized to the non-orphan submesh), keep (1-based indices of the retained mesh nodes), n_mesh_full (the full mesh size), kappa, var_scale, l_max, and logdet_Q.


Build the sparse FEM stiffness / projector from a spatial_spde spec

Description

Prefers the stored Matrix objects (spatial$G, spatial$A); falls back to rebuilding them from the pre-extracted CSC slots so a spec carrying only the slots still assembles.

Usage

.spde_fem_matrices(spatial)

Fractional rSPDE single-point Laplace fit at a fixed (range, sigma)

Description

The fractional counterpart of the integer cpp_laplace_fit_spde branch in laplace_spde_at(). Assembles ⁠(Q, A_eff)⁠ via .spde_assemble_at() and runs the precomputed C++ solve, whose latent is the auxiliary weights x; the returned mode mesh block is mapped back to the field ⁠u = Pr x⁠ so every downstream consumer reads field-space mesh effects exactly as on the integer path. The log-marginal is the well-conditioned B / determinant-lemma marginal (cpp_spde_fractional_logmarginal()) for the RE-free case (comparable across the ⁠(range, sigma)⁠ grid); a fit with an iid RE block keeps the precomputed precision-space marginal.

Usage

.spde_laplace_fractional_at(
  y,
  n_trials,
  X,
  spatial,
  family,
  phi,
  range,
  sigma,
  re_idx,
  n_re_groups,
  sigma_re,
  max_iter,
  tol,
  n_threads,
  offset,
  weights = NULL,
  order = 2L
)

Mean marginal variance of the rSPDE field u = Pr x, x ~ N(0, Q^-1)

Description

Estimates ⁠mean_i [Pr Q^{-1} Pr']_ii = tr(Pr Q^{-1} Pr') / n⁠ by Hutchinson probing: for z ~ N(0, I), ⁠a = Pr' z⁠, ⁠Q v = a⁠, then ⁠E[a' v] = tr(...)⁠. The solve is against Q through the SAME sparse Cholesky the precomputed C++ fit uses, so the normalization is consistent with the fit even when the wide rational spectrum makes Q ill-conditioned: a shared solver makes the implied field covariance identical between the normalization and the likelihood, which is what the nested ⁠(range, sigma)⁠ integration needs (an inconsistent solver breaks the cross-grid marginal). The probe matrix is fixed across calls for a deterministic, grid-smooth normalization.

Usage

.spde_mean_marginal_var(Q, Pr, C0, n_probe = .SPDE_VARNORM_NPROBE)

Numerically stable Laplace log-marginal for a fractional rSPDE at (range, sigma)

Description

Delegates the well-conditioned B / matrix-determinant-lemma marginal to C++ (cpp_spde_fractional_logmarginal()). The precision-space ⁠0.5(log|Q| - log|H|)⁠ is corrupted in a range-dependent way by the rational precision's wide spectrum (cond(Q) ~ 1e13+), so the marginal is formed through the obs-space ⁠B = (A_eff Pl^{-1}) C (A_eff Pl^{-1})' + X X'/tau_beta⁠, built through the operator factor Pl (cond = sqrt cond(Q)), never an explicit Q inverse. Gaussian is the exact conjugate marginal; non-gaussian uses the det-lemma at the precomputed Laplace mode. phi is the Gaussian residual SD (variance phi^2), consistent with the integer path and the engine family convention.

Usage

.spde_nested_logmarginal_at(
  spatial,
  range,
  sigma,
  y,
  X,
  family,
  phi,
  n_trials,
  order,
  max_iter,
  tol,
  n_threads,
  offset,
  tau_beta = 1e-04
)

Value

A list with log_marginal, n_iter, converged, and the assembly.


Is an SPDE smoothness nu fractional (rational path) or integer (exact path)?

Description

Is an SPDE smoothness nu fractional (rational path) or integer (exact path)?

Usage

.spde_nu_is_fractional(nu)

Arguments

nu

Matern smoothness; alpha = nu + 1 in 2D.

Value

TRUE when alpha is non-integer (the rational SPDE path).


Backend names belonging to a tier (derived from the registry).

Description

Backend names belonging to a tier (derived from the registry).

Usage

.tier_backends(tier_key)

Reject families the compiled kernels do not implement.

Description

Reject families the compiled kernels do not implement.

Usage

.validate_family_compiled(family)

Validate the response y for a count family.

Description

Errors when family is a count family and y carries negative or non-integer values, which the integer-casting kernels would silently floor into a biased likelihood. A no-op for continuous families.

Usage

.validate_family_counts(family, y, zi = FALSE)

Arguments

zi

Whether a zero-inflation component is being fitted alongside the family. With a zero-truncated family this is the hurdle model, where the zeros belong to the zero component rather than to the truncated count density, so the y >= 1 requirement does not apply to them.


Validate the dispersion parameter phi for a family.

Description

Errors when family carries a dispersion / precision parameter and phi is not a positive finite scalar. A no-op for families without dispersion (binomial, poisson) and for unknown / model-package families. Shared by the tulpa() front door and tulpa_laplace() so the rule lives in one place .

Usage

.validate_family_phi(family, phi)

Validate the response against a family's support.

Description

The one support rule set, shared by the tulpa() front door and by the engine fitters (tulpa_laplace(), tulpa_laplace_beta(), tulpa_nuts_beta()). Holding it in a single function is what keeps a boundary response rejected with the same message wherever it enters: a per-fitter copy is only as good as the set of fitters that carry it, and tulpa() reaches beta-with-RE through a sampler that carried none.

Usage

.validate_family_support(family, y, n_trials = NULL, zi = FALSE)

Arguments

family

Family identifier, canonical or aliased.

y

Response vector, or NULL (a model with no response passes).

n_trials

Binomial denominators, when known. Supplied, the binomial rule is ⁠0 <= y <= n_trials⁠; absent, the response is the 0/1 form.

zi

Whether a zero-inflation component is fitted alongside the family, passed through to the count rule for the hurdle case.

Details

Counts delegate to .validate_family_counts(); the continuous families and the binomial denominator add their rules here.


Reject a ZI request against a family with no atom at zero.

Description

Reject a ZI request against a family with no atom at zero.

Usage

.validate_family_zi(family)

Reject a ZI request the compiled kernels cannot fit.

Description

Reject a ZI request the compiled kernels cannot fit.

Usage

.validate_family_zi_compiled(family)

Validate a GLM design bundle (y, X, n_trials) at a fitter's front door.

Description

Shared by the flagship drivers (tulpa_nested_laplace(), tulpa_gibbs(), the re_cov fitters) so nrow(X) == length(y) and length(n_trials) == length(y) are enforced in one place – otherwise a mismatched n_trials (as.integer(NULL) -> integer(0)) reaches the C++ kernel silently.

Usage

.validate_glm_design(y, X, n_trials, where)

Arguments

y, X

Response vector and design matrix.

n_trials

Binomial denominators, or NULL (defaults to 1 per row).

where

Caller name for the error message.

Value

List list(N, n_trials) with n_trials coerced to a length-N integer vector.


Validate a 0-based/1-based random-effect index against its group count.

Description

re_idx addresses one grouping factor: 0 marks "no random effect" and ⁠1..n_re_groups⁠ a group. An out-of-range id would index out of bounds in the C++ kernel, so bound it in R.

Usage

.validate_re_idx(re_idx, n_re_groups, N, where)

Arguments

re_idx

Per-observation group index (length N).

n_re_groups

Number of groups.

N

Expected length.

where

Caller name for the error message.

Value

re_idx coerced to an integer vector.


Validate the SPDE smoothness parameter

Description

nu must be a single finite positive number. Integer nu (1, 2, 3, ...) gives an exact FEM construction; fractional nu uses the rational SPDE approximation (see rational_spde_coefficients()).

Usage

.validate_spde_nu(nu)

Arguments

nu

Candidate smoothness parameter.

Value

Invisibly TRUE; raises an error on an invalid nu.


Split a Laplace / EB mode vector into its semantic blocks

Description

Returns the count-side fixed effects, the zero-inflation coefficients, and the per-term random-effect deviations, so the caller places each into the sampler's corresponding block rather than relying on the two flat vectors agreeing end to end (they do not, under zero inflation).

Usage

.warm_start_blocks(fit, re_terms)

Arguments

fit

A fitted tulpa_fit from mode = "eb" or mode = "laplace".

re_terms

The random-effect terms of the model being sampled, in tulpa_laplace() form. Taken from the caller rather than from the fit: re_layout is attached by the front door, so a source fit built directly by tulpa_eb() does not carry one, and the model being sampled is the authority on the block shapes in any case.


Per-term random-effect SDs from a warm-start source fit

Description

An EB fit estimated them, so they are read off map. A Laplace fit conditioned on values the caller supplied, so those are passed in.

Usage

.warm_start_sigmas(fit, sigma_re, n_terms)

Arguments

fit

A fitted tulpa_fit.

sigma_re

Fallback SDs, used when the fit did not estimate them.

n_terms

Number of random-effect terms.


Build the zero-inflation design matrix from ziformula.

Description

A one-sided formula giving the fixed effects of the structural-zero logit. ~ 1 is the constant-probability model. Random effects in the ZI predictor are not supported: the compiled kernels share the random-effect block into the count process only, so a bar here would be silently dropped.

Usage

.zi_design(ziformula, data, n_obs)

All registered backends across every tier (derived from the registry).

Description

All registered backends across every tier (derived from the registry).

Usage

ALL_BACKENDS

Backend -> supported families, derived from the registry.

Description

Backend -> supported families, derived from the registry.

Usage

BACKEND_FAMILY_SUPPORT

Backend registry – single source of truth for the inference backends.

Description

One entry per backend. Adding a backend is a single entry here; tier membership, family support, the input contract, and R-level reachability all derive from this list – the single source of truth for per-tier backends and family support.

Fields:

Family identity (for families) is checked against family$name, family$distribution, and family$numerator$distribution (tulpaRatio-style ratio families nest a per-process distribution).

Usage

BACKEND_REGISTRY

Inference tiers with their properties (derived from TIER_META + BACKEND_REGISTRY). Preserves the ⁠INFERENCE_TIERS$<tier>$backends⁠ interface relied on by downstream code and tests.

Description

Inference tiers with their properties (derived from TIER_META + BACKEND_REGISTRY). Preserves the ⁠INFERENCE_TIERS$<tier>$backends⁠ interface relied on by downstream code and tests.

Usage

INFERENCE_TIERS

Tier metadata: the epistemic guarantee attached to each tier.

Description

Tier membership (which backends live in a tier) is not stored here – it is derived from BACKEND_REGISTRY. This table holds only the per-tier description/guarantee so the two never drift.

Usage

TIER_META

Random-effect variances and correlations

Description

Summarize the random-effect covariance of a fit: the standard deviation of each random-effect coefficient, the correlations between them within a term, and the covariance matrix itself.

Whether the covariance was estimated, sampled, or merely conditioned on is reported alongside it, because the three are different claims. A fit from mode = "laplace" conditions on sigma_re, so its values are the ones supplied; mode = "eb" estimates them; a sampler tier integrates them.

Usage

VarCorr(x, sigma = 1, ...)

## S3 method for class 'tulpa_fit'
VarCorr(x, sigma = 1, ...)

Arguments

x

A tulpa_fit.

sigma

Ignored, for compatibility with the VarCorr generic.

...

Ignored.

Value

A data frame with one row per random-effect coefficient: term, coef, sd, and source (one of "estimated", "sampled", "conditioned"). Correlated terms additionally carry the covariance matrices in the "cov" attribute, one per term, each with a "correlation" attribute. Returns an empty data frame when the fit has no random effects.

See Also

ranef() for the per-level deviations, tulpa_eb() to estimate the covariance rather than condition on it.

Examples


set.seed(1)
G <- 30L; per <- 10L; n <- G * per
grp <- rep(seq_len(G), each = per); x <- rnorm(n)
b <- rnorm(G, 0, 0.8)
d <- data.frame(y = rpois(n, exp(0.3 + 0.5 * x + b[grp])), x = x,
                g = factor(grp))
fit <- tulpa(y ~ x + (1 | g), data = d, family = "poisson", mode = "eb")
VarCorr(fit)


Construct a spatial adjacency graph for areal models

Description

Build the symmetric adjacency matrix that spatial() and spatial_car() consume, from a common spatial layout, instead of hand-coding it. The model still receives an explicit graph (spatial(graph = g$adjacency, ...)), so the graph stays inspectable before fitting – the engine never guesses connectivity from coordinates silently.

adjacency() is a single front-door verb that dispatches on the kind of input:

a data.frame

cell centroids on a regular grid – queen (edge or corner) or rook (edge only) contiguity over the lattice. This covers both plain coordinate grids and rasterised cells passed as a table of centres.

an sf object

polygon contiguity from shared boundaries (queen = share a point or edge; rook = share an edge). Requires sf.

a SpatRaster (terra) or stars object

raster cells – the lattice of (non-NA) cell centres, queen or rook. Requires terra or stars respectively.

The result is a tulpa_adjacency object holding the matrix plus the cell identifier for each node, so a model can pass g$adjacency while the observation data is remapped to 1-based node indices with node_index().

Usage

adjacency(x, ...)

## Default S3 method:
adjacency(x, ...)

## S3 method for class 'data.frame'
adjacency(
  x,
  type = c("queen", "rook"),
  x_coord = "x",
  y_coord = "y",
  id = NULL,
  order = 1L,
  tolerance = 1.5,
  offsets = NULL,
  ...
)

## S3 method for class 'sf'
adjacency(x, type = c("queen", "rook", "touches"), id = NULL, ...)

## S3 method for class 'SpatRaster'
adjacency(
  x,
  type = c("queen", "rook"),
  order = 1L,
  tolerance = 1.5,
  offsets = NULL,
  na_rm = TRUE,
  ...
)

## S3 method for class 'stars'
adjacency(
  x,
  type = c("queen", "rook"),
  order = 1L,
  tolerance = 1.5,
  offsets = NULL,
  na_rm = TRUE,
  ...
)

Arguments

x

The spatial layout: a data.frame of centroids, an sf polygon layer, or a raster (SpatRaster / stars).

...

Passed to methods.

type

Contiguity rule: "queen" (default; neighbours share an edge or a corner) or "rook" (neighbours share an edge only). For polygons, "touches" is an alias of "queen".

x_coord, y_coord

For the data.frame method, the names of the coordinate columns holding the cell centres (default "x" and "y").

id

For the data.frame and sf methods, an optional column naming each cell's identifier. Node i of the graph corresponds to id's value in row i; pass this column to node_index() to translate the observation data's cell column into node indices. Default NULL uses the row position (1:n) as the identifier.

order

For grid / raster layouts, the neighbourhood order (ring count): order = 1 (default) is first-order contiguity (queen = 8 neighbours, rook = 4); order = k extends the stencil to the k-th ring, so queen keeps every cell within Chebyshev distance k (⁠(2k + 1)^2 - 1⁠ neighbours: 8, 24, 48, ... for ⁠k = 1, 2, 3⁠) and rook every cell within Manhattan distance k (⁠2k(k + 1)⁠ neighbours: 4, 12, 24, ...). Any positive integer is allowed, so the neighbourhood is fully settable. Ignored by the sf (polygon) method, which is first-order contiguity only.

tolerance

For grid / raster layouts, the per-offset neighbour distance cut-off as a multiple of the inferred cell size: a candidate at a lattice offset is kept when its true centre distance is at most tolerance times that offset's expected distance. The default 1.5 admits a snapped neighbour while rejecting one much farther than its lattice slot implies; raise it only for irregularly spaced centroids. Neighbourhood extent is set by order, not by tolerance.

offsets

Advanced, grid / raster layouts only: a custom neighbour stencil as a two-column integer matrix or a list of length-2 c(dx, dy) lattice offsets (cell-step units, origin excluded). Supplying it overrides type and order and builds exactly that stencil, so any neighbourhood is expressible (anisotropic, ring-only, off-axis). An ICAR / CAR field is undirected, so the graph must be symmetric: an asymmetric stencil (e.g. list(c(0, 1), c(-1, 0), c(1, 0)) = up / left / right but not down) is symmetrized to an undirected graph, with a message, since a directed neighbourhood cannot be represented by an undirected field. Default NULL uses the type / order stencil.

na_rm

For raster layouts, drop cells whose value is NA before building the graph (default TRUE), so the nodes are the cells that carry data.

Value

A tulpa_adjacency object: a list with

adjacency

the ⁠[n x n]⁠ symmetric sparse adjacency matrix (dgCMatrix, 0/1, zero diagonal) to pass as graph / adjacency.

ids

the cell identifier for each node, in node order (length n).

n

the number of nodes.

cellsize

the inferred cell size c(x, y) for grid / raster layouts, or NA for polygons.

type

the contiguity rule used.

See Also

node_index() to map cell identifiers to node indices, check_adjacency() to validate a hand-built matrix, spatial() and spatial_car() which consume the graph.

Examples

# A 3 x 3 regular grid of cell centres
grid <- expand.grid(x = 1:3, y = 1:3)
grid$cell <- paste0("c", seq_len(nrow(grid)))

g <- adjacency(grid, x_coord = "x", y_coord = "y", id = "cell",
               type = "queen")
g
g$adjacency

# Second-order (24-neighbour) queen contiguity: any order is settable
g2 <- adjacency(grid, id = "cell", order = 2)

# Advanced: a custom stencil (symmetrized for the undirected field)
g3 <- adjacency(grid, id = "cell", offsets = list(c(1, 0), c(0, 1)))

# Use it in a model: graph stays explicit and inspectable
spatial(graph = g$adjacency, formula = ~ 1 || cell_idx)

# Remap observation data (original cell ids -> 1:n node indices) by key
obs <- data.frame(cell = c("c5", "c1", "c5", "c9"))
obs$cell_idx <- node_index(g, obs$cell)
obs


Convert adjacency matrix to CSR for tulpa C++

Description

Convert adjacency matrix to CSR for tulpa C++

Usage

adjacency_to_csr_tulpa(adj)

Adaptive Gauss-Hermite quadrature for one-RE GLMMs

Description

Marginal-likelihood approximation for a generalised linear mixed model with one cluster-level intercept random effect. Adaptive Gauss-Hermite quadrature (AGQ) generalises Laplace by replacing the single-point Gaussian integral around the cluster's posterior mode with n_quad-point quadrature. AGQ at n_quad = 1 recovers the Laplace approximation; higher n_quad reduces approximation error, especially for clusters with few observations or non-Gaussian likelihoods.

This is an engine block, not a front door: its tuning knobs (max_iter, tol, n_quad) sit in the signature rather than in a control list.

Scoped to the lme4::glmer(..., nAGQ = N) use case: one intercept- only RE term, families binomial, poisson, or gaussian. For multi-RE or random-slope models, use tulpa_laplace() (Laplace at the joint mode) or HMC.

Usage

agq_fit(
  y,
  X,
  group,
  n_groups = max(group),
  family = c("binomial", "poisson", "gaussian"),
  n_trials = NULL,
  sigma_eps = 1,
  n_quad = 7L,
  beta_init = NULL,
  sigma_init = 1,
  max_iter = 200L,
  tol = 1e-06,
  verbose = FALSE
)

Arguments

y

Response vector.

X

Fixed-effects design matrix (⁠n_obs x p⁠).

group

Integer cluster labels (1-based, length n_obs).

n_groups

Number of clusters (default max(group)).

family

One of "binomial", "poisson", "gaussian".

n_trials

Trial sizes (binomial only; default rep(1, n_obs)).

sigma_eps

Residual SD (gaussian only; default 1). Held fixed at this value – it is not profiled or estimated, so for a gaussian response set it to a known / pre-estimated residual SD rather than the default.

n_quad

Number of Gauss-Hermite quadrature nodes per cluster. 1 recovers Laplace; common choices are 5 or 7. Default 7.

beta_init

Initial fixed-effects (default zeros).

sigma_init

Initial RE SD (default 1).

max_iter

Optimiser iteration cap (default 200).

tol

Convergence tolerance (default 1e-6).

verbose

Print optimiser summary (default FALSE).

Value

A list with class tulpa_fit carrying:

An optimum at which the AGHQ objective is undefined (some group's solve fails there, so its value is the failure sentinel rather than a marginal likelihood) is an error naming the groups, not a returned fit.

Tier

Tier 2 (Structured). AGQ is exact in the limit n_quad -> infinity but at finite n_quad it is a controlled approximation – same epistemic class as Laplace.

References

Pinheiro, J. C., & Bates, D. M. (1995). Approximations to the log-likelihood function in the nonlinear mixed-effects model. Journal of Computational and Graphical Statistics, 4(1), 12-35.

See Also

tulpa_laplace() for the joint-mode Laplace path (handles multi-RE and spatial structure).

Examples

set.seed(1)
n_g <- 20; n_per <- 5; n <- n_g * n_per
group <- rep(seq_len(n_g), each = n_per)
x <- rnorm(n)
X <- cbind(1, x)
u <- rnorm(n_g, 0, 0.8)
eta <- 0.3 + 0.7 * x + u[group]
y <- rbinom(n, 1, plogis(eta))

# Compare Laplace (n_quad = 1) and AGQ-7.
fit_lap <- agq_fit(y, X, group, family = "binomial", n_quad = 1)
fit_agq <- agq_fit(y, X, group, family = "binomial", n_quad = 7)
c(fit_lap$log_marginal, fit_agq$log_marginal)


Apply RSR projection to spatial effect

Description

Project spatial effect into the space orthogonal to covariates. Called during posterior computation.

Usage

apply_rsr_projection(w, P_perp)

Arguments

w

Spatial effect vector (length n)

P_perp

Projection matrix from compute_rsr_projection

Value

Projected spatial effect (length n)


Posterior draws in the posterior package's format

Description

Convert a fit's posterior draws to a posterior draws object. as_draws() returns the draws_array shape; as_draws_array(), as_draws_matrix(), as_draws_df() and as_draws_rvars() return theirs. When the posterior package is installed these are also registered against its generics, so posterior::as_draws(fit) works.

Usage

as_draws(x, ...)

## S3 method for class 'tulpa_fit'
as_draws(x, n_draws = NULL, seed = NULL, ...)

as_draws_array(x, ...)

## S3 method for class 'tulpa_fit'
as_draws_array(x, n_draws = NULL, seed = NULL, ...)

as_draws_matrix(x, ...)

## S3 method for class 'tulpa_fit'
as_draws_matrix(x, n_draws = NULL, seed = NULL, ...)

as_draws_df(x, ...)

## S3 method for class 'tulpa_fit'
as_draws_df(x, n_draws = NULL, seed = NULL, ...)

as_draws_rvars(x, ...)

## S3 method for class 'tulpa_fit'
as_draws_rvars(x, n_draws = NULL, seed = NULL, ...)

Arguments

x

A tulpa_fit object.

...

Passed to the corresponding posterior converter.

n_draws

Number of draws to synthesize from the Gaussian approximation for a fit that carries none. NULL (default) errors on such a fit rather than silently approximating. Ignored, with a warning, when the fit already carries draws.

seed

Optional integer seed for the synthesis. The RNG state is restored afterwards.

Details

Fits differ in whether they carry draws at all. Sampler and nested-Laplace fits do, and convert directly. A Gaussian-approximation fit (mode = "laplace", mode = "eb") carries a mode and a precision instead, and converting it means drawing from the approximation – which is a modelling decision, not a format change, because every downstream posterior summary would then treat a normal approximation as a posterior sample. So it is opt-in: pass n_draws to synthesize that many draws from N(coef(object), vcov(object)), or get an error naming the alternative. Synthesized draws form a single chain and cover the fixed effects only.

Value

A posterior draws object of the requested shape.

See Also

tulpa_draws_array() for the base R array without the dependency, posterior_sample() for the raw matrix.

Examples


set.seed(1)
df <- data.frame(x = rnorm(80))
df$y <- rpois(80, exp(0.5 + 0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson")
if (requireNamespace("posterior", quietly = TRUE)) {
  posterior::summarise_draws(as_draws(fit))
}


Error if a selected backend has no R-level fitter.

Description

Enforces the registry honesty contract: a backend may ship a C++ kernel reachable from model packages (via LinkingTo: tulpa) yet have no R entry point. Such a backend must never be silently selectable from R – selecting it errors with a precise message naming the C-ABI symbol, rather than pretending to dispatch.

Usage

assert_backend_reachable(backend)

Mark an outer-grid setting as a default rather than a pin

Description

Declares that a setting shaping the outer hyperparameter grid carries a default the caller computed, not a choice the user made. The auto-recenter pass (outer_grid_placement) leaves a user-pinned setting exactly as given, and re-centres (or, for a prior, engages its own regularizer over) a marked one when the fit rails against its ceiling.

Three kinds of setting take the mark:

Wrapper packages are the intended caller: one that builds a default of its own – because it derives a second axis from it, hands the same vector to several blocks, or exposes its own argument with a default – would otherwise be indistinguishable from a user who pinned that setting deliberately. Mark it and the rescue stays live. A setting whose value is exactly the engine's own default is recognised without a mark; anything else needs one.

The mark is an attribute, so it is dropped by sort(), [, c() and as.numeric(): build the value first, mark it last.

Usage

auto_grid(x)

Arguments

x

Numeric vector or matrix of grid nodes, a numeric scalar knob, or a prior-specification list.

Value

x carrying the marker attribute. Numeric input is coerced to double IN PLACE, so everything else it carries – dim() and dimnames() above all – survives the mark; a list is returned unchanged apart from the attribute.

See Also

is_auto_grid(), tulpa_nested_laplace_joint(), fit_st_nested()

Examples

prior <- list(type = "icar", sigma_grid = auto_grid(c(0.1, 0.5, 1, 2, 3)))
is_auto_grid(prior$sigma_grid)
is_auto_grid(auto_grid(list("pc.prec", c(U = 3, alpha = 0.01))))

Auto-select mode (Tier 1 or Tier 2 only)

Description

Implements the "auto" mode selection. Chooses the most reliable method that is expected to finish for the given model.

Critical rule: Auto never selects Tier 3 (Optimized).

Usage

auto_select_mode(
  family,
  n_obs,
  has_spatial,
  has_temporal,
  has_latent,
  temporal = NULL,
  spatial_type = NULL,
  has_re = FALSE
)

Is a backend reachable from R (does it have an R-level fitter)?

Description

Is a backend reachable from R (does it have an R-level fitter)?

Usage

backend_is_reachable(backend)

Test whether a backend supports the given family.

Description

Test whether a backend supports the given family.

Usage

backend_supports_family(backend, family)

Bayesian R-squared

Description

Per posterior draw s, R2_s = Var_i(mu_si) / (Var_i(mu_si) + Var_res_s), where mu_si are the response-scale fitted means and Var_res_s is the family's residual variance averaged over observations (Gelman et al. 2019). The linear predictor is rebuilt per draw exactly as in posterior_predict() (fixed effects + random effects + offset at the training data).

Usage

bayes_R2(object, ...)

## S3 method for class 'tulpa_fit'
bayes_R2(
  object,
  ndraws = NULL,
  summary = TRUE,
  probs = c(0.025, 0.975),
  seed = NULL,
  ...
)

Arguments

object

A tulpa_fit object from tulpa().

...

Passed to methods.

ndraws

Number of posterior draws to use. Defaults to all stored draws, or 400 on the draw-free Laplace tier.

summary

Summarize the per-draw values (default TRUE).

probs

Quantiles reported by the summary (default 2.5% / 97.5%).

seed

Optional integer seed (RNG state is restored on exit), used by the Gaussian fixed-effect sampling on draw-free fits.

Value

With summary = TRUE (default) a one-row data frame with estimate (posterior median), std.error, and the probs quantiles; with summary = FALSE the vector of per-draw R^2 values.

References

Gelman, Goodrich, Gabry & Vehtari (2019). R-squared for Bayesian regression models. The American Statistician 73(3):307-309.

Examples


set.seed(1)
d <- data.frame(x = rnorm(200))
d$y <- rnorm(200, 2 * d$x, 1)
fit <- tulpa(y ~ x, data = d, family = "gaussian", mode = "laplace", phi = 1)
bayes_R2(fit)


Bridge sampling for marginal likelihood

Description

Estimate the log marginal likelihood \log Z = \log p(y) from posterior draws via the Meng-Wong / Gronau bridge-sampling identity. Useful for Bayes factors, model comparison, and as a sanity check on Laplace-approximated marginal likelihoods.

The classical iterative scheme (Meng & Wong 1996; Gronau et al. 2017) is used with a multivariate-normal proposal fit to half of the posterior draws – the other half is used for the bridge ratio so the proposal is independent of the samples that score it. All computations are done in log-space via logsumexp for numerical stability.

Usage

bridge_sampling(
  draws,
  log_posterior,
  n_proposal = nrow(draws),
  max_iter = 1000L,
  tol = 1e-10,
  split = TRUE,
  verbose = FALSE
)

Arguments

draws

Numeric matrix of posterior draws, one parameter per column. Typically fit$draws from a tulpa Tier-1 fit.

log_posterior

Function ⁠function(theta) -> numeric⁠ returning the unnormalized log posterior density ⁠log p(theta, y)⁠ at a single parameter vector. Must accept a numeric vector of length ncol(draws) and return a finite scalar.

n_proposal

Number of proposal draws. Default nrow(draws).

max_iter

Maximum bridge iterations (default 1000).

tol

Convergence tolerance on ⁠|log r_{t+1} - log r_t|⁠ (default 1e-10).

split

Logical: split the posterior draws in half so the proposal is fit on one half and scored on the other (default TRUE, recommended). Set FALSE only for diagnostic comparison.

verbose

Print iteration history (default FALSE).

Value

A list with:

Tier

Bridge sampling is a post-hoc marginal-likelihood estimator, not a sampling backend, so it does not appear in the INFERENCE_TIERS registry. It operates on draws from any Tier-1 (Exact) backend.

References

Meng, X.-L., & Wong, W. H. (1996). Simulating ratios of normalizing constants via a simple identity: a theoretical exploration. Statistica Sinica, 6, 831-860.

Gronau, Q. F., Sarafoglou, A., Matzke, D., Ly, A., Boehm, U., Marsman, M., ... & Steingroever, H. (2017). A tutorial on bridge sampling. Journal of Mathematical Psychology, 81, 80-97.

Examples

# Toy: marginal likelihood of N(theta | 0, 1) under Y = theta + eps,
# eps ~ N(0, 1), prior theta ~ N(0, 10). Closed-form available.
y <- 1.5
log_post <- function(theta) {
  dnorm(y, theta, 1, log = TRUE) + dnorm(theta, 0, 10, log = TRUE)
}
draws <- matrix(rnorm(2000, mean = y * 100 / 101, sd = sqrt(100 / 101)),
                ncol = 1)
bs <- bridge_sampling(draws, log_post)
bs$log_marginal  # should match dnorm(y, 0, sqrt(101), log = TRUE)


Build a GLMM log-posterior (and gradient) from a model-data bundle.

Description

Build a GLMM log-posterior (and gradient) from a model-data bundle.

Usage

build_glmm_logpost(
  bundle,
  family,
  sigma_re = NULL,
  n_trials = NULL,
  phi = 1,
  beta_prior = .tulpa_default_beta_prior("glmm_logpost"),
  weights = NULL,
  phi2 = NULL
)

Arguments

bundle

Output of tulpa_build_model_data() (needs y, X, offset, re_terms, n_obs, n_fixed).

family

Character family name (see family_names()).

sigma_re

Numeric vector of random-effect SDs, one per RE term. Length must equal length(bundle$re_terms). Ignored when there are no RE terms.

n_trials

Binomial denominators (or NULL).

phi

Dispersion/precision passed to the family.

beta_prior

list(mean, sd) Gaussian prior on the fixed effects (scalars, recycled). Defaults to the engine default, prior_normal(0, 2.5).

weights

Optional per-observation likelihood weights (length n_obs): each row's log-likelihood and score contribution is scaled by its weight.

phi2

Optional second dispersion (Student-t degrees of freedom).

Value

A list with:


Check if data is gridded (can use stars)

Description

Check if data is gridded (can use stars)

Usage

can_use_stars(plot_data)

Central Composite Design (CCD) grid for nested-Laplace integration

Description

Produces a structured set of standardised hyperparameter points z \in \mathbb{R}^k for use as integration nodes in a nested Laplace approximation when k \ge 3. CCD scales much better than the full tensor expand.grid() used by the 1D and 2D backends: a CCD has 1 + 2k + 2^{k - q} points (1 centre, 2k axial, 2^{k - q} factorial), versus m^k for an m-per-axis tensor product.

Point layout (with centre+axial+factorial scaling f_0):

For k \le 6 the factorial portion is the full 2^k design. For k \ge 7 a half-fraction (q = 1) using the defining word x_1 \cdots x_k keeps the point count reasonable while preserving Resolution V.

Used by tulpa_nested_laplace() for higher-dimensional hyperparameter blocks. The standardised z-coordinates are mapped to physical hyperparameters \theta via ccd_to_theta().

Usage

ccd_grid(k, f_0 = sqrt(k))

Arguments

k

Number of hyperparameters (length of theta). Must be >= 1.

f_0

Radius of the design sphere (default \sqrt{k}). Larger f_0 spreads points further out; smaller concentrates near the centre. INLA's default scales like \sqrt{k}.

Value

A list with components:

See Also

ccd_to_theta() to map z-coordinates to physical theta.


Map standardised CCD coordinates to physical hyperparameters

Description

Converts standardised CCD z-coordinates (in \mathbb{R}^k) to physical hyperparameters \theta via the affine map \theta = \hat\theta + L \cdot z with optional log-scale transform per component. L is typically a Cholesky factor of the negative Hessian inverse evaluated at the (working) mode \hat\theta – i.e. it scales z to one posterior standard deviation per axis.

Usage

ccd_to_theta(z, theta_hat, L, log_scale = FALSE)

Arguments

z

Matrix ⁠[n_points x k]⁠ of standardised coordinates from ccd_grid().

theta_hat

Numeric vector of length k: centre of the design, in either physical or log-space (per log_scale).

L

Numeric ⁠[k x k]⁠ matrix: scale/rotation applied to z. Pass diag(sd) for a diagonal axis-aligned grid where sd is the per-axis posterior SD; pass a Cholesky factor to capture correlations between hyperparameters.

log_scale

Logical (or logical vector of length k). If TRUE for component j, theta_hat[j] and column j of the affine transform live on log-scale and are exponentiated afterward (useful for positive parameters like tau). Default FALSE everywhere.

Value

Numeric matrix ⁠[n_points x k]⁠ of physical theta-values.


Corrected R-INLA CCD integration weights

Description

Per-point design weights for a ccd_grid() used as nested-Laplace integration nodes. Implements the corrected R-INLA central-composite-design weights: the formula in Rue, Martino and Chopin (2009) has a typo, corrected by the R-INLA team to give a positive central weight. With m hyperparameters, np design points and standardized scaling f0, every non-centre point gets

w = 1 / ((np - 1)(1 + e^{-m f0^2 / 2}(f0^2 - 1)))

and the centre point gets w_0 = 1 - (np - 1) w. Used as \Delta_k: the integration weight of node k is \Delta_k \exp(\log\,\mathrm{marginal}_k), renormalized. On a standardized (whitened) hyperparameter posterior these reproduce the Gaussian moments exactly.

Usage

ccd_weights(ccd)

Arguments

ccd

A ccd_grid() result. The standardized scaling is recovered as f0 = ccd$f_0 / sqrt(m): a ccd_grid(m, f_0 = sqrt(m) * f0) design places the factorial corners at ⁠+/- f0⁠, matching the INLA convention (f0 = 1.1 is the INLA default).

Value

Numeric vector of length ccd$n_points: the design weight for each point (the centre weight w0 for the central point, w otherwise).

See Also

ccd_grid(), ccd_to_theta().


Validate a spatial adjacency matrix

Description

Check that a hand-built adjacency matrix is a well-formed spatial graph before passing it to spatial() / spatial_car(): square, symmetric, zero on the diagonal, 0/1 valued, and free of isolated nodes. adjacency() runs the same checks on the graphs it constructs.

Usage

check_adjacency(adjacency, ids = NULL)

Arguments

adjacency

A matrix (dense or sparse Matrix).

ids

Optional cell identifiers; if supplied, their length must match nrow(adjacency) and they must be unique.

Value

Invisibly, a tulpa_adjacency_check list with the per-check results (square, symmetric, zero_diag, binary, isolated-node indices, edge count) and an overall ok flag. Issues are reported via warning() and printed; the function does not stop, so every problem surfaces in one pass.

See Also

adjacency() to construct a graph, node_index().

Examples

adj <- matrix(0, 4, 4)
for (i in 1:3) adj[i, i + 1] <- adj[i + 1, i] <- 1
check_adjacency(adj)


Quick convergence check

Description

Runs the core convergence diagnostics on a fit and reports whether Rhat, bulk-ESS, and divergence thresholds are all met. A terse companion to diagnostic_summary() for use in scripts and tests.

Usage

check_diagnostics(
  fit,
  rhat_threshold = 1.01,
  ess_threshold = 400,
  quiet = FALSE
)

Arguments

fit

A tulpa_fit object.

rhat_threshold

Maximum acceptable Rhat (default 1.01).

ess_threshold

Minimum acceptable bulk-ESS (default 400).

quiet

Logical; if TRUE, suppress messages (default FALSE).

Value

Invisibly, TRUE if all checks pass, FALSE if any fail, or NA for a non-chain (approximation) fit where Rhat/ESS do not apply.

See Also

diagnostic_summary(), diagnostics(), n_divergent()

Examples


set.seed(123)
df <- data.frame(x = rnorm(60))
df$y <- rpois(60, exp(0.5 + 0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc",
             control = list(n_iter = 500L, warmup = 250L, n_chains = 2L,
                            seed = 1L))
check_diagnostics(fit)



Diagnostic panel plot

Description

Produces a 2x2 or 1x3 panel of diagnostic plots:

  1. QQ plot of PIT residuals vs Uniform (with KS p-value)

  2. Residuals vs fitted (with lowess smoother)

  3. Dispersion histogram (observed variance vs simulated)

  4. Spatial correlogram via Moran's I (if coords provided)

Usage

check_model(object, ...)

## Default S3 method:
check_model(object, coords = NULL, nsim = 250L, seed = 123L, ...)

Arguments

object

A fitted model with simulate(), fitted(), residuals()

...

Passed to methods.

coords

Optional N x 2 coordinate matrix for spatial panel

nsim

Number of simulations (default 250)

seed

Random seed (default 123)

Value

Invisible list with ks_p, disp_ratio, moran (if spatial)


Fixed-effect coefficients

Description

Fixed-effect coefficients

Usage

## S3 method for class 'tulpa_fit'
coef(object, ...)

Arguments

object

A tulpa_fit object.

...

Ignored.

Value

Named numeric vector of fixed-effect posterior means (the Laplace mode for the Laplace tier). Random effects come from ranef().


Collect additive terms from a + chain

Description

Recursively flattens a + b + c into list(a, b, c).

Usage

collect_additive_terms(expr)

Arguments

expr

A language object

Value

A list of language objects (individual terms)


Compare models by information criteria

Description

Rank fitted models best-first by an information criterion. "waic" and "loo" use the native pointwise log-likelihood layer (tulpa_criteria() / tulpa_psis()) – WAIC and PSIS-LOO respectively, computed from each fit's ⁠[n_draws x n_obs]⁠ pointwise log-likelihood (fit$draws$log_lik), with no loo package dependency. "loglik" returns the (integrated) joint log-likelihood with the parameter count. A fit carrying no pointwise log-likelihood (a deterministic / point approximation) yields NA criterion columns rather than an error, so the table always has one row per model.

Usage

compare_models(..., criterion = c("waic", "loo", "loglik"))

Arguments

...

Named tulpa_fit objects.

criterion

"waic" (default), "loo", or "loglik".

Value

A data frame. For "loglik": model, n_params, logLik. For "waic" / "loo" (ranked best-first): model, elpd, se_elpd, p_eff, ic (-2 * elpd), delta (elpd gap to the best model), se_diff (SE of that pointwise elpd difference), and weight (the Akaike-style weight on the criterion).

See Also

model_average() for model-averaged predictions, tulpa_criteria() and tulpa_psis() for the native criteria layer.

Examples


set.seed(1)
df <- data.frame(x = rnorm(120))
df$y <- rpois(120, exp(0.4 + 0.5 * df$x))
f1 <- tulpa(y ~ x, data = df, family = "poisson")
f2 <- tulpa(y ~ 1, data = df, family = "poisson")
compare_models(full = f1, null = f2, criterion = "waic")


Compute BYM2 scaling factor

Description

Compute the scaling factor for BYM2 following Riebler et al. (2016). This makes the spatial fraction parameter interpretable.

Usage

compute_bym2_scale(adjacency)

Arguments

adjacency

Adjacency matrix

Value

Scaling factor (scalar)


Compute valid bounds for rho in proper CAR

Description

For proper CAR, rho must be in the range (1/lambda_min, 1/lambda_max) where lambda are the eigenvalues of D^(-1)W. In practice, we typically restrict to (0, 1) for interpretability (positive spatial autocorrelation).

Usage

compute_car_rho_bounds(adjacency)

Arguments

adjacency

Adjacency matrix

Value

Named vector with lower and upper bounds for rho


Compute nearest neighbors for NNGP

Description

Compute the k nearest neighbors for each observation using Euclidean distance. Returns in a format suitable for the NNGP likelihood.

Usage

compute_nngp_neighbors(coords, k)

Arguments

coords

N x d matrix of coordinates

k

Number of nearest neighbors

Value

List with:


Compute RSR projection matrix

Description

Compute the orthogonal projection matrix P_perp = I - P_X that projects the spatial effect into the space orthogonal to the covariates.

Usage

compute_rsr_projection(X)

Arguments

X

Design matrix of covariates to orthogonalize against

Value

Projection matrix (n x n)


Credible intervals for the fixed effects

Description

Credible intervals for the fixed effects

Usage

## S3 method for class 'tulpa_fit'
confint(object, parm = NULL, level = 0.95, ...)

Arguments

object

A tulpa_fit object.

parm

Parameter names or indices (default: all fixed effects).

level

Interval level (default 0.95).

...

Ignored.

Value

Matrix with lower and upper columns. A nested-Laplace fit carries interval_source / interval_declined (which read produced the bounds – by default the grid's Gaussian-mixture CDF), retained_mass (the share of the grid weight the bounds are conditional on), and skew_applied, one logical per reported coefficient saying whether its bounds are the inner-Laplace skew-corrected quantiles or not. See summary.tulpa_fit().


Create the ggplot2 map

Description

Create the ggplot2 map

Usage

create_map(
  plot_data,
  obs_coords,
  title,
  palette,
  point_color,
  point_size,
  na_color,
  crs,
  legend_title,
  ...
)

Create point-based map

Description

Create point-based map

Usage

create_point_map(plot_data, palette, na_color, legend_title)

Create raster-style map using stars

Description

Create raster-style map using stars

Usage

create_raster_map(plot_data, crs, palette, na_color, legend_title)

DIC and CPO

Description

Generic front doors onto the two criteria tulpa_criteria() computes that the loo package owns no generic for. WAIC and PSIS-LOO have theirs (loo::waic(), loo::loo()), so a model package registers methods on those rather than on new names that would mask them.

Usage

dic(object, ...)

## Default S3 method:
dic(object, loglik_at_mean = NULL, ...)

cpo(object, ...)

## Default S3 method:
cpo(object, ...)

Arguments

object

A pointwise log-likelihood matrix (draws x observations), or a fitted model object a method is registered for.

...

Passed to tulpa_criteria() (e.g. group, chunk_size).

loglik_at_mean

Length-n_obs vector of pointwise log-likelihoods at the posterior mean of the parameters. Required for DIC's plug-in deviance; without it the DIC fields are NA.

Details

The default methods take a draws x observations pointwise log-likelihood matrix, the same input tulpa_criteria() takes. A model package registers a method taking its own fit object, builds the matrix from the posterior, and delegates here.

Value

A tulpa_criteria object.

See Also

tulpa_criteria() for every criterion at once and for what the LOO unit means.

Examples

set.seed(1)
y  <- rnorm(40)
mu <- matrix(rnorm(200 * 40, sd = 0.2), 200, 40)
ll <- dnorm(matrix(y, 200, 40, byrow = TRUE), mean = mu, log = TRUE)
cpo(ll)

Decompose the LHS of a bar term into intercept flag + slope language objects

Description

Walks the additive chain of the LHS and separates numeric intercept indicators (0, 1, -1) from slope term expressions. Returns language objects, not deparsed strings.

Usage

decompose_bar_lhs(lhs)

Arguments

lhs

A language object (LHS of a bar term)

Value

A list with has_intercept (logical) and slope_terms (list of language objects)


Comprehensive Diagnostic Summary

Description

Provides a comprehensive diagnostic report for a tulpa model, combining convergence metrics, divergence information, and actionable recommendations.

Usage

diagnostic_summary(fit, quiet = FALSE)

Arguments

fit

A tulpa_fit object.

quiet

Logical; if TRUE, suppress printed output (default: FALSE).

Value

A list with class tulpa_diagnostic_summary containing:

status

Overall status: "PASS", "WARN", or "FAIL"

n_divergent

Number of divergent transitions

divergent_pct

Percentage of divergent transitions

worst_rhat

Data frame of parameters with worst Rhat

worst_ess

Data frame of parameters with worst ESS

e_bfmi

E-BFMI value (HMC only)

pareto_k, quad_ess

approximation fits only: the outer PSIS k-hat, or the grid quadrature ESS when no k-hat was produced

pareto_k_declined

approximation fits only, and only when there is no k-hat: WHY – "not_requested" and "unguessable_axis: <axis>" are benign or permanent, "degenerate_proposal" and "grid_too_small" are signals about the fit, and "internal_inconsistency" is an engine bug and raises the status to "WARN"

inner_skew_max, inner_skew_declined

approximation fits only: the largest scored inner-Laplace ⁠|gamma_3|⁠, or why nothing was scored ("coupled_arm" marks arms the inner layer could score neither per observation nor through the cell tensor)

axis_fields_dropped

data frame of grid axes the fit's own resolved path could not read and dropped as engine defaults: one row per field, with the block, family, path and the axis that path integrated instead. Absent whenever every supplied axis was used

recommendations

Character vector of recommendations

See Also

check_diagnostics(), diagnostics(), plot_diagnostics()

Examples


set.seed(123)
df <- data.frame(x = rnorm(60))
df$y <- rpois(60, exp(0.5 + 0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc",
             control = list(n_iter = 500L, warmup = 250L, n_chains = 2L,
                            seed = 1L))
ds <- diagnostic_summary(fit)
print(ds)



Posterior diagnostics for a fitted model

Description

The diagnostic front door for any tulpa fit. What a fit's posterior draws can be asked depends on how they were produced, so this reads the draws provenance and returns the diagnostic that applies:

MCMC chain draws

improved Rhat (the maximum of rank-normalized split-Rhat and folded split-Rhat), bulk / tail / mean / sd / quantile effective sample size, and Monte Carlo standard errors, following Vehtari et al. (2021). Split-Rhat is defined for a single chain, so a result is produced for any number of chains.

i.i.d. approximation draws

the approximation-reliability table – the PSIS tail-shape pareto_k scored against the exact inner-Laplace marginal and the outer-grid quadrature effective sample size (the OUTER hyperparameter-grid integration layer), the inner-Laplace skewness diagnostic gamma_3 when computed (the INNER Gaussian approximation to the latent field, a separate layer pareto_k does not cover), a combined whole-fit verdict naming which layer degrades when one does, and a per-parameter posterior summary. See laplace_diagnostics() for the full description of this table and its attributes.

point summaries

no sample to diagnose; returns NULL with a message naming the backend.

Provenance is read from ⁠$draws_kind⁠ (stamped by tulpa_dispatch), falling back to the backend registry's emits property and then to an inner ⁠$joint_fit⁠. A fit that predates the tag is treated as a chain, so an older fit is never silently refused.

Usage

diagnostics(fit, ...)

## Default S3 method:
diagnostics(
  fit,
  pars = NULL,
  measures = c("rhat", "ess_bulk", "ess_tail"),
  probs = c(0.05, 0.95),
  sbc = NULL,
  ...
)

## S3 method for class 'sbc'
diagnostics(fit, ...)

Arguments

fit

A tulpa_fit (or subclass) carrying posterior ⁠$draws⁠. Multiple chains are recognised from a 3D ⁠[iter, chain, param]⁠ draws array, a ⁠$chain_id⁠ row map, or an ⁠$n_chains⁠ count over chain-major rows.

...

Passed to the method.

pars

Optional character vector of parameter names to restrict to.

measures

Character vector selecting which diagnostics to compute, in output-column order. Available: "rhat", "rhat_bulk", "rhat_fold", "ess_bulk", "ess_tail", "ess_mean", "ess_sd", "mcse_mean", "mcse_sd", "ess_quantile", "mcse_quantile". Defaults to the core set c("rhat", "ess_bulk", "ess_tail"). Applies to chain fits; the approximation-reliability table has a fixed set of columns.

probs

Numeric probabilities for the quantile-based measures ("ess_quantile", "mcse_quantile"); each expands to one column named e.g. ess_q5, ess_q95. Default c(0.05, 0.95). Chain fits only.

sbc

Optional sbc() result for the same model, whose calibration verdict is attached to the returned table.

Value

For a chain fit, a data frame with a parameter column followed by one column per requested measure; entries are NA for parameters that are constant or have too few draws. For an i.i.d. approximation fit, the laplace_diagnostics table (see that function for its attributes). For a point fit, NULL.

Extending

diagnostics() is an S3 generic so a model package can answer for its own fit class (diagnostics.ratiod_fit(), say) rather than shadowing this export with a same-named function. The default method does the provenance routing described above and is what a method should delegate to once it has assembled a draws array.

Calibration alongside reliability

The tables above score ONE fit's own internal reliability. Whether the backend's posterior is CALIBRATED is a different question, answered over many simulated data sets by sbc(), and the two disagree in both directions – a fit whose reliability band is clean on both layers can still fail calibration, and one whose outer k-hat is well past the escalation threshold can pass. So the band is a screen, not a verdict. Pass an sbc result as ⁠sbc =⁠ and the calibration verdict is attached to the table and printed underneath it; diagnostics() called on the sbc result itself returns its report table.

References

Vehtari, Gelman, Simpson, Carpenter & Burkner (2021). Rank-normalization, folding, and localization: an improved Rhat for assessing convergence of MCMC. Bayesian Analysis 16(2):667-718.

Vehtari, Simpson, Gelman, Yao & Gabry (2024). Pareto smoothed importance sampling. JMLR 25(72):1-58.

See Also

sbc() for calibration, laplace_diagnostics() for the approximation-reliability table in full, tulpa_draws_array(), plot_rhat(), plot_ess(), diagnostic_summary(), check_diagnostics()

Examples


set.seed(1)
df <- data.frame(x = rnorm(60))
df$y <- rpois(60, exp(0.5 + 0.3 * df$x))

# chain fit -> Rhat / ESS
hmc <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc",
             control = list(n_iter = 500L, warmup = 250L, n_chains = 2L,
                            seed = 1L))
diagnostics(hmc)

# deterministic fit -> PSIS approximation reliability
smc <- tulpa(y ~ x, data = df, family = "poisson", mode = "smc")
diagnostics(smc)


Dispatch a spatial Polya-Gamma Gibbs fit to the correct sampler

Description

The Gibbs analogue of dispatch_laplace_spatial(): routes on spatial$type to the matching ⁠cpp_pg_<family>_gibbs_<structure>⁠ sampler, building the neighbour-list / coordinate inputs each one needs. The binomial Polya-Gamma augmentation backs the full areal (icar/bym2/rsr) + continuous (gp/nngp/ multiscale_gp) family; neg_binomial_2 is backed by the single areal ICAR negbin sampler (cpp_pg_negbin_gibbs_spatial), the only negbin spatial kernel.

Usage

dispatch_gibbs_spatial(
  y,
  n_trials,
  X,
  re_group,
  n_re_groups,
  spatial,
  family,
  iter,
  warmup,
  thin = 1L,
  prior_beta_sd = .tulpa_prior_sd("gibbs"),
  prior_sigma_re_scale = 2.5,
  verbose = FALSE,
  n_threads = 1L
)

Dispatch a temporal Polya-Gamma Gibbs fit

Description

The temporal analogue of dispatch_gibbs_spatial(): maps a validated temporal_multiscale() spec onto the multiscale temporal Polya-Gamma sampler (cpp_pg_binomial_gibbs_temporal), which composes an additive RW1 trend + cyclic-RW1 seasonal + AR1/IID short-term decomposition. Binomial only. The C++ kernel implements an RW1 trend (rw2 is rejected here rather than silently downgraded).

Usage

dispatch_gibbs_temporal(
  y,
  n_trials,
  X,
  re_group,
  n_re_groups,
  temporal,
  family,
  iter,
  warmup,
  thin = 1L,
  prior_beta_sd = .tulpa_prior_sd("gibbs"),
  prior_sigma_re_scale = 2.5,
  verbose = FALSE,
  n_threads = 1L
)

Dispatch spatial Laplace to the correct C++ backend

Description

weights is the per-observation likelihood weight. It reaches the same BuiltinFamilyResponse::weights channel the non-spatial route uses, which scales each row's log-density, score and Fisher curvature by the same w_i, so the mode these kernels return and the marginal precision ⁠.marginal_H_beta_*()⁠ builds at it describe one model.

Usage

dispatch_laplace_spatial(
  y,
  n_trials,
  X,
  re_idx,
  n_re_groups,
  sigma_re,
  spatial,
  family,
  phi,
  max_iter,
  tol,
  n_threads,
  offset = NULL,
  weights = NULL
)

Durbin-Watson test for temporal autocorrelation

Description

Tests first-order autocorrelation in temporally-ordered residuals.

Usage

durbin_watson(object, alternative = c("two.sided", "greater", "less"))

Arguments

object

A numeric vector of temporally-ordered residuals

alternative

"two.sided", "greater" (positive autocorr), or "less"

Value

An htest object with DW statistic, lag-1 r, and p-value


Extract coordinates from various formats

Description

Extract coordinates from various formats

Usage

extract_coords(x)

Extract coordinates from tulpa_fit object

Description

Extract coordinates from tulpa_fit object

Usage

extract_coords_from_fit(fit)

Elementwise log-likelihood for a family.

Description

Elementwise log-likelihood for a family.

Usage

family_loglik(eta, y, family, n_trials = NULL, phi = 1, phi2 = NULL)

Inverse-link mean for a family. phi reaches entries whose response mean depends on the dispersion (lognormal); the others ignore it via ....

Description

Inverse-link mean for a family. phi reaches entries whose response mean depends on the dispersion (lognormal); the others ignore it via ....

Usage

family_mean(eta, family, phi = 1)

Supported R-level family names.

Description

Supported R-level family names.

Usage

family_names()

Response-scale mean of y given eta for a family (trial-scaled where relevant).

Description

Response-scale mean of y given eta for a family (trial-scaled where relevant).

Usage

family_response_mean(eta, family, n_trials = NULL, phi = 1)

One response draw per element of eta (posterior predictive), elementwise.

Description

One response draw per element of eta (posterior predictive), elementwise.

Usage

family_sample(eta, family, n_trials = NULL, phi = 1, phi2 = NULL)

Score (d log-likelihood / d eta), elementwise.

Description

Score (d log-likelihood / d eta), elementwise.

Usage

family_score_eta(eta, y, family, n_trials = NULL, phi = 1, phi2 = NULL)

Response variance Var(y | eta) for a family, elementwise.

Description

Response variance Var(y | eta) for a family, elementwise.

Usage

family_variance(eta, family, n_trials = NULL, phi = 1, phi2 = NULL)

Laplace/IRLS working weight (-d^2 log-lik / d eta^2), elementwise.

Description

Laplace/IRLS working weight (-d^2 log-lik / d eta^2), elementwise.

Usage

family_weight(eta, family, n_trials = NULL, phi = 1, phi2 = NULL)

Find all latent(...) calls in a formula's parse tree

Description

Recursively walks the formula AST and collects all latent(...) calls. The matched calls are returned unevaluated; tulpa_parse_formula() resolves them in the formula's environment.

Usage

find_latent_terms(term)

Arguments

term

A language object (formula term)

Value

A list of language objects, each a latent(...) call


Find all bar terms in a formula's parse tree

Description

Recursively walks the formula AST and collects all | and || nodes found inside parentheses. These are the random effect specifications.

Usage

findbars(term)

Arguments

term

A language object (formula term)

Value

A list of language objects, each a | or || call


Fit a Spatial Model using SPDE Laplace Approximation

Description

Fits a GLM with a Matern spatial field via the SPDE approach. Uses CHOLMOD sparse solver with optional nested Laplace for hyperparameter integration.

Usage

fit_spde(
  y,
  X,
  spatial,
  family = "binomial",
  n_trials = NULL,
  range = NULL,
  sigma = NULL,
  nested_laplace = is.null(range) || is.null(sigma),
  phi = 1,
  offset = NULL,
  re_idx = NULL,
  n_re_groups = 0L,
  sigma_re = 1,
  mode = c("laplace", "nuts"),
  control = list()
)

Arguments

y

Integer response vector.

X

Design matrix.

spatial

A tulpa_spatial object from spatial_spde() or spatial_spde_custom().

family

Distribution family: "binomial", "poisson", "neg_binomial_2", or "gaussian" (continuous-field geostatistics, with phi the observation-noise standard deviation).

n_trials

Integer vector of trial sizes (binomial only).

range

Spatial range parameter. If NULL, uses nested Laplace to integrate over range and sigma.

sigma

Marginal standard deviation. If NULL, uses nested Laplace.

nested_laplace

Logical. If TRUE (default when range/sigma are NULL), use nested Laplace approximation over hyperparameters.

phi

Dispersion parameter (negbin only).

offset

Optional fixed additive term on the linear predictor (⁠eta = offset + X beta + A w⁠), length length(y); NULL -> no offset.

re_idx, n_re_groups, sigma_re

Optional single iid random-intercept (1 | g) term alongside the Matern field: re_idx is a length-length(y) 1-based group index, n_re_groups the number of groups, and sigma_re the (conditioned) random-effect SD. The field and the RE block are Laplace- marginalised jointly. n_re_groups = 0 (default) is no RE term. Not supported for a fractional-nu field.

mode

Inference method (the method is an argument, not a parallel verb): "laplace" (default) is the nested-Laplace integration over ⁠(range, sigma)⁠ documented here; "nuts" delegates to tulpa_nuts_spde() for exact HMC over the field (and, unless both range and sigma are fixed, the Matern hyperparameters). mode = "nuts" does not support an offset or a random-effect term, and its sampler knobs pass via control (see tulpa_nuts_spde()); it returns that sampler's draws object.

control

A named list of numerical / tuning knobs (statistical arguments stay in the signature above). Recognized entries:

  • method: hyperparameter integration backend when nested Laplace is active. "ccd" (default) uses a central composite design centered on the joint posterior mode of ⁠(range, sigma)⁠, oriented by the local Hessian (9 design points instead of n_grid^2), folding the PC priors from spatial$prior_range / spatial$prior_sigma into the integrated marginal and falling back to "grid" if the surface is too flat for a Hessian-based design. "grid" uses a rectangular grid in ⁠log(range) x log(sigma)⁠ around the prior modes.

  • n_grid: grid points per hyperparameter dimension for method = "grid" (ignored under "ccd"). Default 5.

  • diagnose_k: if TRUE (default), compute the outer Pareto-\hat{k} accuracy diagnostic (⁠$pareto_k⁠) by importance sampling the joint ⁠(range, sigma)⁠ posterior on the log scale against the Gaussian proposal that orients the integration. See tulpa_psis().

  • k_samples: importance draws for diagnose_k. Default 200, each one extra batched SPDE marginal evaluation.

  • mode_find: tuning for the outer ⁠(range, sigma)⁠ mode-find under method = "ccd", as list(factr =, ndeps =, maxit =); supply any subset. ndeps is the central-difference step for optim()'s numerical gradient on the log scale (default 1e-2): it must clear the inner solver's convergence tolerance, and a step wide enough that its truncation error exceeds the reduction the line search chases near a flat optimum leaves L-BFGS-B aborting at the mode it just reached, in which case the CCD design declines to the rectangular grid. factr is the relative-reduction stop in units of .Machine$double.eps (default 1e5); maxit the iteration cap (default 300).

  • max_iter: maximum Newton iterations. Default 100.

  • tol: Newton convergence tolerance. Default 1e-6.

  • n_threads: OpenMP threads. Default 1.

  • checkpoint: grid-cell checkpoint/resume spec list(path =, resume =). Each solved ⁠(range, sigma)⁠ cell is appended to path; a resume = TRUE run loads the finished cells and re-solves only the rest, so a killed or rebooted fit resumes instead of restarting. resume = FALSE starts a fresh file. Default NULL (off).

Value

A list with:

References

Lindgren, Rue & Lindstrom (2011). An explicit link between Gaussian fields and Gaussian Markov random fields: the stochastic partial differential equation approach. JRSS-B 73(4):423-498. Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392.

Examples


if (requireNamespace("fmesher", quietly = TRUE)) {
  set.seed(1)
  n <- 200L
  coords <- cbind(runif(n), runif(n))
  mesh <- fmesher::fm_mesh_2d(loc = coords, max.edge = c(0.15, 0.4), cutoff = 0.05)
  fem  <- fmesher::fm_fem(mesh)
  A    <- as(fmesher::fm_basis(mesh, loc = coords), "CsparseMatrix")
  spec <- spatial_spde_custom(C = fem$c0, G = fem$g1, A = A, nu = 1,
                              prior_range = c(0.3, 0.5), prior_sigma = c(0.6, 0.05))
  w <- as.numeric(rnorm(spec$n_mesh, 0, 0.6)); w <- w - mean(w)
  x <- rnorm(n)
  y <- rpois(n, exp(2.0 + 0.5 * x + as.numeric(spec$A %*% w)))
  fit <- fit_spde(y = y, X = cbind(1, x), spatial = spec, family = "poisson")
  fit$nested$range_mean
}


Fit an additive spatiotemporal GLM by nested Laplace

Description

Fits ⁠y ~ X beta + u_spatial[s] + v_temporal[t]⁠ with an areal spatial field (icar / bym2 / car_proper) and a temporal field (rw1 / rw2 / ar1), integrating the spatial precision, temporal precision, and (for ar1) the temporal autocorrelation over a hyperparameter grid via the ⁠cpp_nested_laplace_st_*⁠ kernels. The fixed-effect posterior is the grid-marginalised mixture; the spatial and temporal field posterior means are the grid-weighted latent modes.

Usage

fit_st_nested(
  y,
  X,
  spatial_idx,
  adjacency,
  temporal_idx,
  n_times,
  spatial_type = c("icar", "bym2", "car_proper"),
  temporal_type = c("ar1", "rw1", "rw2"),
  family = "binomial",
  n_trials = NULL,
  phi = 1,
  cyclic = FALSE,
  re_idx = NULL,
  n_re_groups = 0L,
  sigma_re = 1,
  control = list()
)

Arguments

y

Response vector.

X

Fixed-effects design matrix (nrow(X) == length(y)).

spatial_idx

Integer per-observation spatial-unit index (1-based).

adjacency

Spatial adjacency (a symmetric 0/1 matrix or sparseMatrix).

temporal_idx

Integer per-observation time index (1-based).

n_times

Number of distinct time points.

spatial_type

"icar" (default), "bym2", or "car_proper".

temporal_type

"ar1" (default), "rw1", or "rw2".

family

Response family (see family_names()).

n_trials

Binomial denominators, or NULL (= 1).

phi

Dispersion passed to the family.

cyclic

Logical; wrap the temporal field (seasonal). Default FALSE.

re_idx, n_re_groups, sigma_re

Optional single iid random-intercept term alongside the fields (conditioned on sigma_re); n_re_groups = 0 (default) is no RE term.

control

A list of numerical / grid knobs: n_grid_spatial, n_grid_temporal (default 4 each), n_grid_rho (ar1 only, default 3), tau_lower / tau_upper (precision grid bounds, default 0.25 / 16), rho_lower / rho_upper (ar1 grid, default 0.1 / 0.9), max_iter, tol, n_threads, auto_recenter (default TRUE; FALSE holds the grid exactly as specified – the per-axis policy names tulpa_nested_laplace() takes are refused here with an error, since this driver recentres on the grid's collapsed-edge regime rather than on a per-axis rail).

The ⁠(tau_lower, tau_upper)⁠ span (and, for ar1, ⁠(rho_lower, rho_upper)⁠) is a starting axis, not a hard ceiling: when the fitted precision (or, for ar1, autocorrelation) posterior mode rails a boundary node (pareto_k_regime = "collapsed_edge", see below), the driver fits a mode-Hessian via a derivative-free optim() over the collapsed grid and refits a grid re-centred on it (one attempt).

A grid knob PINS the axes it shapes, and a pin always wins – but pinning is decided by value, not by presence: a knob set to the engine's own default, or marked with auto_grid(), expresses no preference and leaves its axes free. That is what lets a wrapper package thread its own n_grid-style argument through control without silently disabling the recenter for every fit it makes. Pinning is also per axis: tau_lower / tau_upper hold the two precision axes, n_grid_spatial / n_grid_temporal one each, and n_grid_rho / rho_lower / rho_upper the ar1 autocorrelation axis, so pinning one axis leaves the others free to be recentred. A pinned axis keeps its nodes exactly and is named in outer_grid_pinned_axes; with EVERY axis pinned the recenter declines outright and outer_grid_recenter_declined records which reason applied.

Value

A tulpa_fit (subclass tulpa_nested_laplace) carrying the fixed-effect posterior (draws via the grid mixture), spatial_effects, temporal_effects, log_marginal, weights, and theta_grid over ⁠(tau_spatial, tau_temporal, rho)⁠. Also carries pareto_k_regime ("spread" / "collapsed_interior" / "collapsed_edge", see tulpa_nested_laplace_joint()'s return docs for the definition) and outer_grid_placement ("fixed" or "auto_recentered") plus, on a "fixed" placement, outer_grid_recenter_declined ("grid_knobs_overridden" / "grid_not_collapsed" / "no_usable_curvature" / "refit_failed" / "sd_ceiling_unresolved" / "sd_floor_unresolved"). A recentred fit also carries outer_grid_pinned_axes, the axes whose knobs were pinned and whose nodes were therefore kept, and outer_grid_recenter_sd_clamp / ⁠_sd_raw⁠ / ⁠_sd_used⁠ – per moved axis, which mode-SD bound the placement hit, the SD the stencil measured, and the SD the axis was laid from. A bound-decline is PER AXIS here: the axes the mode-find did resolve are still re-placed, and outer_grid_recenter_sd_declined names the ones that kept their incoming nodes and on which bound, so a partially re-placed grid is not read as a fully re-placed one. With every free axis declined the pass reports the grid as the fixed one it still is.

See Also

tulpa() (front door), tulpa_nested_laplace() (single field).

Examples


set.seed(1)
n_s <- 16L; n_t <- 8L; N <- 400L
adj <- matrix(0, n_s, n_s)
for (i in 1:(n_s - 1)) adj[i, i + 1] <- adj[i + 1, i] <- 1
s <- sample(n_s, N, TRUE); tt <- sample(n_t, N, TRUE)
us <- as.numeric(scale(cumsum(rnorm(n_s)))); vt <- as.numeric(scale(cumsum(rnorm(n_t))))
x <- rnorm(N)
y <- rbinom(N, 1, plogis(0.2 + 0.5 * x + 0.7 * us[s] + 0.6 * vt[tt]))
fit <- fit_st_nested(y, cbind(1, x), s, adj, tt, n_t, family = "binomial")


Fitted values (population level)

Description

In-sample mean response from the fixed effects and the observation offset (⁠E[y] = g^{-1}(X beta + offset)⁠, trial-scaled for binomial). Random effects are held at their prior mean of zero; group-level effects are in ranef(). y - fitted(object) equals residuals(object, type = "response").

Usage

## S3 method for class 'tulpa_fit'
fitted(object, ...)

Arguments

object

A tulpa_fit object (must carry ⁠$model_matrix⁠).

...

Ignored.

Value

Numeric vector of fitted mean responses, length nobs.


Fixed-effect coefficients (lme4-compatible)

Description

The fixed-effect point estimates, equivalent to coef() on a tulpa_fit. Provided under the fixef name for code written against the lme4 / nlme interface: lme4::fixef(fit) and nlme::fixef(fit) dispatch here too when either package is installed, so a tulpa_fit can be dropped into an lme4-shaped workflow.

Usage

fixef(object, ...)

## S3 method for class 'tulpa_fit'
fixef(object, ...)

Arguments

object

A tulpa_fit object.

...

Ignored.

Details

Note the deliberate difference from lme4::coef.merMod, which returns per-group sums of the fixed and random effects. On a tulpa_fit, coef() returns the fixed effects alone and fixef() is its synonym; the random effects are ranef().

Value

Named numeric vector of fixed-effect estimates.

See Also

coef.tulpa_fit(), ranef()

Examples


set.seed(1)
df <- data.frame(x = rnorm(80))
df$y <- rpois(80, exp(0.5 + 0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson")
fixef(fit)


Format mode selection message

Description

Format mode selection message

Usage

format_mode_selection(selection)

Arguments

selection

List from select_inference_mode()

Value

Character string for display


Render a slope spec as a display string

Description

Used by the print method. Derives the textual form on demand from the stored language objects so we never store deparsed slope text.

Usage

format_re_lhs(re)

Format tier information for display

Description

Format tier information for display

Usage

format_tier_info(tier_info, verbose = FALSE)

Arguments

tier_info

List from get_backend_tier()

verbose

Include full description

Value

Character string


Gauss-Hermite quadrature (probabilist's, exp(-z^2/2) weight)

Description

Computes nodes and weights for ⁠\int f(z) (2\pi)^{-1/2} exp(-z^2/2) dz⁠ via Golub-Welsch on the Hermite Jacobi matrix. Sums of ⁠w_k f(z_k)⁠ approximate ⁠E_{Z ~ N(0,1)}[f(Z)]⁠.

Usage

gauss_hermite_prob(n)

Arguments

n

Number of quadrature nodes.

Value

List with nodes and weights (each length n).


Get tier for a backend

Description

Get tier for a backend

Usage

get_backend_tier(backend)

Arguments

backend

Character string naming the backend

Value

List with tier information


Map mode to valid backends

Description

Map mode to valid backends

Usage

get_mode_backends(mode)

Arguments

mode

Character: "auto", "exact", "structured", or "optimized"

Value

Character vector of valid backends for this mode


Get appropriate color scale

Description

Get appropriate color scale

Usage

get_palette_scale(palette, na_color, legend_title, geom = "fill")

Geweke Convergence Test

Description

Performs Geweke's convergence diagnostic, comparing the mean of the first portion of a chain to the last portion. Useful for single-chain diagnostics.

Usage

geweke_test(fit, frac1 = 0.1, frac2 = 0.5, pars = NULL)

Arguments

fit

A tulpa_fit object.

frac1

Fraction of chain for first window (default: 0.1).

frac2

Fraction of chain for second window (default: 0.5).

pars

Character vector of parameter names (default: all main params).

Details

The Geweke test computes a z-score comparing the means of early and late portions of a chain. Large z-scores (|z| > 2) indicate the chain has not converged.

Value

A data frame with columns: parameter, z_score, p_value.

See Also

diagnostics(), check_diagnostics()

Examples


set.seed(123)
df <- data.frame(x = rnorm(60))
df$y <- rpois(60, exp(0.5 + 0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc",
             control = list(n_iter = 500L, warmup = 250L, n_chains = 2L,
                            seed = 1L))
geweke_test(fit)



Model-level summary statistics (broom-compatible)

Description

Model-level summary statistics (broom-compatible)

Usage

## S3 method for class 'tulpa_fit'
glance(x, ...)

Arguments

x

A tulpa_fit object.

...

Ignored.

Value

Single-row data frame.

Examples


set.seed(1)
df <- data.frame(x = rnorm(100))
df$y <- rpois(100, exp(0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson")
glance(fit)


Compute GLM working weights for Laplace Hessian

Description

Thin wrapper over the family-ops registry (family_weight()) so the weight formulas live in exactly one place (R/family_loglik.R).

Usage

glmm_weights(eta, family, n_trials = NULL, phi = 1, phi2 = NULL)

Map a spatial_gp covariance spec to the Laplace cov_type integer

Description

The Laplace NNGP kernel (laplace_core.cpp) supports three covariance functions: 0 = exponential, 1 = Matern(nu=1.5), 2 = Matern(nu=2.5). Anything else is rejected with a clear error rather than silently falling back to a different covariance.

Usage

gp_cov_type_for_laplace(spatial)

Describe one outer-grid hyperparameter axis

Description

Builds a single axis spec for tulpa_hyper_grid(). Carries the candidate values, the optional log-prior, and the metadata (log-scale, bounds, refinable flag) that the generic refinement / consistency passes need.

Usage

hyper_axis_spec(
  name,
  grid,
  log_prior = NULL,
  log_scale = FALSE,
  bounds = NULL,
  refinable = FALSE
)

Arguments

name

Character. Axis label, used as the column name of the grid matrix and in posterior summaries.

grid

Numeric vector of length >= 1. The per-axis candidate values (the outer integration nodes on this axis). The full outer grid is the Cartesian product across axes.

log_prior

Optional ⁠function(x)⁠ returning the scalar log prior density at axis value x. NULL (default) is a flat / improper prior (zero log-prior contribution).

log_scale

Logical. Does the axis live naturally on a log scale (sigma, tau, lengthscale, ...)? Drives geometric vs arithmetic spacing in refinement and log-axis quantile fits. Default FALSE.

bounds

Numeric vector of length 2 giving the natural support ⁠(lower, upper)⁠ of the axis, e.g. c(0, Inf) for sigma, c(0, 1) for a BYM2 mixing coefficient. NULL (default) is unbounded.

refinable

Logical. When TRUE, the axis participates in the adaptive-grid and var-of-means consistency passes (when those are enabled at the driver level). Spatial prior amplitudes (sigma) are typically left at the user-specified grid (refinable = FALSE); the copy coefficient alpha and per-arm dispersion phi typically opt in. Default FALSE.

Value

An object of class tulpa_hyper_axis_spec (a validated list with the six fields above).

See Also

tulpa_hyper_grid().


Independence Metropolis-Hastings with Laplace proposal

Description

Sample from a posterior using independence Metropolis-Hastings with a multivariate-normal proposal centred at the Laplace mode and scaled by the inverse Hessian. For posteriors that are well- approximated by a Gaussian near the mode this is dramatically cheaper than HMC: each iteration is one log-posterior evaluation plus one accept/reject step. Embarrassingly parallel across chains.

Use cases:

Usage

imh_laplace(
  log_posterior,
  mode,
  hessian,
  n_iter = 2000L,
  warmup = n_iter%/%2L,
  scale = 1,
  init = NULL,
  thin = 1L,
  verbose = FALSE
)

Arguments

log_posterior

Function ⁠function(theta) -> numeric⁠ returning the unnormalized log posterior at a single parameter vector.

mode

Numeric vector of length d: the Laplace mode (i.e., tulpa_laplace(...)$mode[seq_len(d)] for the fixed-effects block, or any other mode you trust).

hessian

Symmetric positive-definite ⁠d x d⁠ matrix: the negative Hessian of log_posterior at mode (or H_beta from tulpa_laplace).

n_iter

Total iterations including warmup (default 2000).

warmup

Warmup iterations to discard (default n_iter / 2).

scale

Optional inflation factor on the proposal covariance (default 1.0). Values slightly > 1 (e.g., 1.5) give heavier-tailed proposals that improve mixing when Laplace underestimates posterior spread.

init

Optional starting parameter vector (default = mode).

thin

Keep every thin-th post-warmup sample (default 1).

verbose

Print acceptance rate at end (default FALSE).

Value

A list with class tulpa_fit carrying:

Tier

Tier 1 (Exact). The MH accept/reject step makes the chain asymptotically correct under the standard MH conditions. Tier status does not depend on Laplace's quality – only its quality affects efficiency.

See Also

tulpa_laplace() for the mode + Hessian, bridge_sampling() for marginal-likelihood estimation on the resulting draws.

Examples

# Toy: Bernoulli logistic with one covariate.
set.seed(1)
n <- 100
x <- rnorm(n)
eta <- 0.3 + 1.2 * x
y <- rbinom(n, 1, plogis(eta))
X <- cbind(1, x)

lap <- tulpa_laplace(y, n_trials = rep(1L, n), X = X,
                     family = "binomial")

log_post <- function(beta) {
  eta <- as.numeric(X %*% beta)
  sum(y * eta - log1p(exp(eta))) +
    sum(dnorm(beta, 0, 10, log = TRUE))
}

fit <- imh_laplace(log_post, mode = lap$mode[1:2],
                   hessian = lap$H_beta, n_iter = 1000)
fit$mean_accept
colMeans(fit$draws)


Print inference mode information

Description

Displays information about the inference modes available in tulpa, including their tiers, guarantees, and appropriate use cases.

Usage

inference_mode_info()

Value

NULL, invisibly. Called for the side effect of printing the available inference modes, their tiers, guarantees, and use cases to the console.


Inference Mode System

Description

tulpa uses an explicit tier system for inference that encodes epistemic guarantees, not just runtime characteristics.

This design makes the difference between inference methods first-class and unavoidable, rather than hiding them as implementation details.

The Three Tiers

Tier 1 - Exact: Asymptotically correct posterior inference (up to Monte Carlo error). Credible intervals are interpretable as posterior uncertainty. This is the reference standard.

Tier 2 - Structured: Accurate inference conditional on explicit structural assumptions. Typically requires latent Gaussian structure, conditional independence, smooth posteriors. Very fast for the right model class, but can be wrong outside that class. Failure modes are predictable and explainable.

Tier 3 - Optimized: No general correctness guarantee beyond empirical usefulness. Point estimates usually good, but uncertainty often underestimated. Tails and correlations unreliable. Failure is usually silent. This is optimization, not sampling.

Auto Mode

mode = "auto" chooses between Tier 1 and Tier 2 only. It will never silently choose Tier 3 (Optimized).

The contract: "Use the most reliable method that is expected to finish for this model."

Auto decisions are deterministic, explainable, and overrideable.

Implementation Rules

  1. Modes change semantics, not just runtime. Intervals from Optimized do not mean the same as Exact.

  2. The mode must always be visible in output.

  3. No silent upgrading or downgrading. If Exact fails, we error - we do not switch to Structured.

  4. Backends slot into tiers. Adding a backend never introduces a new epistemic promise.


Initialize latent factor parameters

Description

Initialize latent factor parameters

Usage

initialize_latent_params(latent_info, seed = NULL)

Arguments

latent_info

Latent factor info from prepare_latent_for_hmc

seed

Optional random seed

Value

Numeric vector of initial parameter values


Is an outer-grid setting marked as a default?

Description

Is an outer-grid setting marked as a default?

Usage

is_auto_grid(x)

Arguments

x

Any object.

Value

TRUE when x carries the auto_grid() marker.

See Also

auto_grid()

Examples

is_auto_grid(auto_grid(c(0.5, 1, 2)))
is_auto_grid(c(0.5, 1, 2))

Check if adjacency matrix is connected

Description

Check if the spatial graph defined by the adjacency matrix is fully connected. A disconnected graph can cause identifiability issues.

Usage

is_connected(adjacency)

Arguments

adjacency

Adjacency matrix

Value

Logical; TRUE if connected


Proper-CAR Laplace at given hyperparameters

Description

Single-point Laplace for a proper-CAR areal field at a fixed ⁠(tau, rho)⁠, the conditional counterpart of the nested CAR_proper integrator. Reuses the shared make_car_proper_latent_blocks factory + dense spec solver via cpp_laplace_fit_car_proper, so the mode + log-marginal equal the nested kernel at that one grid cell. tau / rho default to the spec's fields (or tau = 1, rho = midpoint of the eigenvalue-derived rho_bounds) and are recorded on the result.

Usage

laplace_car_proper_at(
  y,
  n_trials,
  X,
  spatial,
  family = "binomial",
  phi = 1,
  tau = NULL,
  rho = NULL,
  re_idx = NULL,
  n_re_groups = 0L,
  sigma_re = 1,
  max_iter = 100L,
  tol = 1e-06,
  n_threads = 1L,
  offset = NULL,
  weights = NULL
)

Approximation-reliability diagnostics for a deterministic nested-Laplace fit

Description

[Deprecated]

Use diagnostics(), which returns this table for any fit whose draws are an i.i.d. approximation sample. The sections below document that table; they remain the reference for its columns and attributes.

Per-parameter reliability diagnostics for a fit whose posterior draws are i.i.d. samples from a deterministic approximation (the nested-Laplace grid-mixture posterior ⁠sum_k w_k N(mode_k, V_k)⁠), where the between-chain Gelman-Rubin Rhat that diagnostics() reports for a chain fit does not apply. This is the accessor that plays Rhat's role for the deterministic engine: it answers "did the approximation work", not "did the chains mix".

The headline is a Pareto-smoothed importance-sampling (PSIS) reliability diagnostic for the OUTER hyperparameter-grid integration. The nested integrator scores its hyperparameter grid against the exact inner-Laplace marginal posterior with a generalized-Pareto fit to the upper tail of the importance ratios ⁠log p_target(theta) - log q_proposal(theta)⁠ (see tulpa_psis()); the resulting tail-shape pareto_k is the "did the outer integration work" number – k-hat < 0.5 good, 0.5-0.7 usable, ⁠>= 0.7⁠ unreliable (Vehtari et al. 2024; Yao et al. 2018). It is computed at fit time and read back here; a fit that did not run the diagnostic, or whose grid proposal degenerated, reports it as NA and is assessed on the grid quadrature reliability instead.

pareto_k scores the outer integration only. A high pareto_k on a fit whose grid quadrature is healthy (ess_grid well above 1, largest cell weight modest) flags outer-integration (CI-width) calibration in the right-skewed hyperparameter tail and does not by itself invalidate the point estimates, which the grid quadrature governs.

outer_regime qualifies what a high pareto_k means, and is the reason a bare threshold on pareto_k is not a reliability verdict. A sharp hyperparameter posterior collapses the grid onto ~1 cell (ess_grid near 1); the outer integration has then degenerated to a point evaluation at the modal hyperparameter, so pareto_k is scoring how well a Gaussian at that mode stands in for the hyperparameter marginal, not how well a grid integrated it. Where the dominant cell is INTERIOR to the grid the collapse is benign – the grid bracketed the mode, the estimate is empirical Bayes there, and only integrated hyperparameter uncertainty is missing. Where it sits at a grid BOUNDARY the grid may simply be too narrow: grid_edge_axes / grid_edge_sides name the axes to widen. On a fit whose pareto_k cleared the good band, the outer diagnostic also fits a skew-normal proposal and reports the marginal's estimated skewness as outer_skew_max, so an inflated k-hat that was purely the symmetric proposal's mismatch with a skewed variance-component marginal is both corrected and explained. A skew-normal has Gaussian tails, so this can never mask a genuinely heavy-tailed target.

The grid quadrature reliability – the effective sample size ess_grid = 1 / sum(w_k^2) of the outer integration weights and the largest single cell weight – is always computed from the stored grid: a grid that collapses onto one cell (ess_grid near 1) integrates no hyperparameter uncertainty, while a spread grid does.

A SEPARATE layer – the inner Gaussian Laplace approximation to the latent-field conditional posterior pi(x | theta, y), which pareto_k does not cover – is scored by inner_skew: the leading-order Edgeworth skewness estimate gamma_3 (Rue, Martino & Chopin 2009 Sec 3.2.3) at the fitted MAP grid cell, computed when control$diagnose_skew = TRUE (the default) on the fitting call. Reading a high pareto_k alone as "the fit is broken" conflates the two layers: an occu_cover batch flagged 42/78 species "unreliable" on outer k-hat alone when their point estimates, governed by the healthy inner layer, were fine – the reliability attribute is the combined verdict that names which layer degrades, if either does.

Each parameter row also carries the rank-normalized split-Rhat and bulk / tail effective sample size of the draws (Vehtari et al. 2021). On i.i.d. draws these sit at ~1.00 and ~n_draws by construction; they are reported, clearly as i.i.d.-draw Monte-Carlo diagnostics and not chain mixing, to document that the reported posterior summaries are not Monte-Carlo-limited.

A posterior sample is what those per-parameter rows are computed from, and nothing else here needs one: every reliability quantity above is read off the fit. So a fit that carries no draws – a default single-block nested-Laplace fit, whose posterior is the retained outer-grid mixture rather than a sample – reports the full band with an empty per-parameter body and n_draws = NA, and records why in the param_table_declined attribute. tulpa_posterior_draws() samples that mixture where the rows are wanted. The one case that still returns NULL is a fit with neither draws nor any reliability quantity, such as a plain Laplace fit with no outer grid to score.

Usage

laplace_diagnostics(fit, pars = NULL)

Arguments

fit

A tulpa_fit (or subclass, e.g. a tobs_fit) whose draws are an i.i.d. approximation sample (⁠$draws_kind == "iid"⁠).

pars

Optional character vector of parameter names to restrict to.

Value

A data frame with one row per parameter – parameter, mean, sd, ess_bulk, ess_tail, rhat – carrying attributes:

pareto_k

the outer PSIS reliability k-hat (NA if not computed).

pareto_k_band

"good" / "ok" / "unreliable" / NA.

pareto_k_declined, pareto_k_declined_note

when pareto_k is NA, WHY: "not_requested", "not_applicable", "unguessable_axis" (naming the axis – a permanent limitation of that family, so read ess_grid instead), "draws_too_few", "grid_too_small", "no_varying_axis", "degenerate_proposal", or "internal_inconsistency" (an engine bug worth reporting), plus a one-line reading of it.

pareto_k_is_ess

importance-sampling ESS on the smoothed weights.

ess_grid, n_grid, rel_ess_grid, max_weight

grid quadrature reliability.

outer_regime

"spread" / "collapsed_interior" / "collapsed_edge" – whether the outer grid integrated hyperparameter uncertainty at all, and if not whether its dominant cell is interior (empirical Bayes at the mode: point estimates sound, hyperparameter uncertainty not integrated) or against a grid boundary (widen it).

grid_edge_axes, grid_edge_sides

for an edge collapse, the axes the dominant cell sits against and on which side.

outer_skew_max

largest estimated |skewness| of the hyperparameter marginal, computed only when the k-hat triggered the skew-normal proposal rescue (NA means the Gaussian proposal already fit, not "symmetric and unchecked").

outer_regime_note

a one-line reading of a collapsed regime, or absent on a spread grid.

grid_railed_axes

outer axes whose OWN marginal is maximal at one of their own endpoints, as axis:side – the span does not contain that axis's posterior mode, so its marginal is a truncated tail at any spacing. Reported whether or not the engine was allowed to, able to, or built to move the axis.

grid_placement, grid_recentred_axes, grid_placement_declined, grid_placement_note

whether the outer grid was re-centred, on which axes, and – when it was not – why.

scope

the outer diagnostic's scope string.

inner_skew_max

the largest ⁠|gamma_3|⁠ among the scored latent indices (NA if control$diagnose_skew = FALSE or nothing scored).

inner_skew_band

"good" / "ok" / "unreliable" / NA, banded on inner_skew_max by the general skewness-magnitude convention (Bulmer 1979) – not a Rue-Martino-Chopin-specific cutoff.

inner_skew_scored, inner_skew_probed

how many of the probed latent indices returned a finite gamma_3 vs how many were probed.

inner_skew_declined, inner_skew_arms_declined, inner_skew_declined_note

when nothing was scored, WHY: "coupled_arm" (STRUCTURAL – the coupled arms have neither a per-observation sum nor a cell third-derivative tensor to read, so the outer k-hat is the only reliability number this fit has), "curvature3_unavailable", "no_finite_contribution", "no_probe_indices", "not_requested", "backend_unsupported", "solve_failed", or "not_converged" (the probe re-solve stopped short of a mode, so neither inner score has a point to read); the arms (1-based) a joint fit had no oracle for, which is also set on a PARTIALLY scored fit; and a one-line reading.

inner_pareto_k, inner_pareto_k_band

the inner-Laplace importance k-hat over the probed subspace, and its band on the same convention as the outer k-hat. Available wherever a mode was found, including a fit gamma_3 cannot score.

inner_pareto_k_rel_ess, inner_pareto_k_is_ess

the smallest realized importance efficiency and effective sample size across the probed indices – how much correcting the inner Gaussian actually needs, which is what makes the scale-free shape above readable.

inner_pareto_k_uniform

TRUE when no probed index carried a material correction, i.e. the inner Gaussian reproduces the conditional posterior over the sampled region.

inner_pareto_k_scored, inner_pareto_k_probed

how many probed indices returned a finite k-hat vs how many were probed.

inner_pareto_k_declined, inner_pareto_k_declined_note

when it is NA, WHY, from the same closed vocabulary the outer k-hat uses.

reliability

the combined whole-fit verdict: "reliable" only when both layers are good; otherwise names which layer is scoped or flags both as unreliable. The inner layer enters through the worse of its two scores, so a fit whose cubic term declined is still assessed.

and a trailing summary attribute (a one-row data frame of the headline numbers) for printing.

Scope

The PSIS pareto_k diagnoses the OUTER (hyperparameter) integration: whether the Gaussian-proposal-over-grid approximation of the marginal hyperparameter posterior p(theta | data) can be importance-corrected to the exact inner marginal. This is the dominant approximation in nested Laplace and the one with an exactly evaluable target. A full latent-space PSIS against the exact joint posterior pi(x) is not computed: the latent prior marginal ⁠p(x) = integral p(x | theta) p(theta) dtheta⁠ has no closed form, and for the marginalized-occupancy / cover-hurdle likelihoods the exact joint density is evaluable only inside the C++ kernel, so a stored fit cannot reconstruct it. The grid quadrature reliability is the complementary stored-fit number.

The inner layer carries a SECOND score, inner_pareto_k, which needs no likelihood derivative at all and therefore answers where inner_skew declines. The inner Gaussian at the fitted hyperparameter is an importance proposal for the exact conditional posterior, and the joint density is the target, so PSIS on that ratio scores the inner approximation directly. It is computed on the same probed indices along the same conditional-mean curve, one dimension per index, since importance sampling degrades with dimension and a k-hat over the whole latent field would report n_x rather than the approximation. A Pareto shape index is scale-free, so it is banded only on indices whose realized importance efficiency shows a correction worth describing; inner_pareto_k_uniform records that none did, which is what a well-approximated inner layer looks like.

inner_skew diagnoses the INNER (latent-field) Laplace: whether the Gaussian approximation to pi(x_i | theta, y) is itself a good fit, at each scored latent index i. gamma_3 is exact for a gaussian-family coefficient (the log-likelihood is exactly quadratic in eta) and declines to NA – never a silently-wrong 0 ("perfectly Gaussian") – whenever no per-observation third-derivative oracle is available: a coupled multi-process likelihood (e.g. tulpaObs's occu_cover, whose arms combine non-separably through a CellCouplingSpec) or a family with no registered third derivative. Only the requested latent indices are scored (every arm's fixed-effects coefficients by default – see control$skew_idx), since each index costs one extra linear solve; the full latent field is not scored by default on a large spatial field.

References

Vehtari, Simpson, Gelman, Yao & Gabry (2024). Pareto smoothed importance sampling. JMLR 25(72):1-58.

Yao, Vehtari, Simpson & Gelman (2018). Yes, but did it work?: Evaluating variational inference. ICML, PMLR 80:5581-5590.

Vehtari, Gelman, Simpson, Carpenter & Burkner (2021). Rank-normalization, folding, and localization: an improved Rhat for assessing convergence of MCMC. Bayesian Analysis 16(2):667-718.

Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392.

See Also

diagnostics() (the front door, which returns this table for i.i.d. fits), tulpa_psis().

Examples


set.seed(1)
n <- 200L; x <- rnorm(n)
y <- rbinom(n, 1, plogis(-0.2 + 0.6 * x))
# `mode = "laplace"` returns a mode + covariance and carries no draws; a
# sampled deterministic backend is what this table describes.
fit <- tulpa(y ~ x, data.frame(y = y, x = x), family = "binomial",
             mode = "smc")
diagnostics(fit)


NNGP Laplace at given hyperparameters

Description

Single-point Laplace approximation for a Matern/exponential GP spatial field at fixed (sigma2_gp, phi_gp). Used by dispatch_laplace_spatial when spatial$type == "gp". The neighbor structure is read straight off the validated spec – call validate_gp(spatial, data) first if constructing manually.

Usage

laplace_gp_at(
  y,
  n_trials,
  X,
  spatial,
  family = "binomial",
  phi = 1,
  sigma2_gp = NULL,
  phi_gp = NULL,
  re_idx = NULL,
  n_re_groups = 0L,
  sigma_re = 1,
  max_iter = 100L,
  tol = 1e-06,
  n_threads = 1L,
  offset = NULL,
  weights = NULL
)

Arguments

y

Response vector.

n_trials

Trial sizes (binomial).

X

Fixed-effects design matrix.

spatial

A tulpa_gp spec, validated (i.e., neighbor_info populated).

family

Distribution family.

phi

Dispersion parameter (negbin / gamma only).

sigma2_gp

Marginal variance (NULL -> 1.0).

phi_gp

Range / decay parameter (NULL -> 1.0).

max_iter

Newton iterations.

tol

Newton tolerance.

n_threads

OpenMP threads.

weights

Optional per-observation likelihood weights (length length(y)), scaling each row's log-density, score and Fisher curvature.

Value

The raw cpp_laplace_fit_gp result list, augmented with sigma2_gp, phi_gp, and the spatial spec.


HSGP Laplace at given hyperparameters

Description

Single-point Laplace for a Hilbert-space GP field at a fixed ⁠(sigma2, lengthscale)⁠, the conditional counterpart of the nested HSGP integrator. Reuses the shared make_hsgp_block factory + dense spec solver (DENSE_BASIS scatter) via cpp_laplace_fit_hsgp, so the mode equals the nested kernel at that one grid cell. sigma2 / lengthscale default to the spec's fields (or 1) and are recorded on the result.

Usage

laplace_hsgp_at(
  y,
  n_trials,
  X,
  spatial,
  family = "binomial",
  phi = 1,
  sigma2 = NULL,
  lengthscale = NULL,
  re_idx = NULL,
  n_re_groups = 0L,
  sigma_re = 1,
  max_iter = 100L,
  tol = 1e-06,
  n_threads = 1L,
  offset = NULL,
  weights = NULL
)

SPDE Laplace at given hyperparameters

Description

Single-point Laplace approximation for an SPDE spatial field at a fixed (range, sigma). Used by both dispatch_laplace_spatial (single-point path) and fit_spde (single-point branch) so the call site stays a single source of truth.

Usage

laplace_spde_at(
  y,
  n_trials,
  X,
  spatial,
  family = "binomial",
  phi = 1,
  range = NULL,
  sigma = NULL,
  re_idx = NULL,
  n_re_groups = 0L,
  sigma_re = 1,
  max_iter = 100L,
  tol = 1e-06,
  n_threads = 1L,
  offset = NULL,
  weights = NULL
)

Arguments

y

Response vector.

n_trials

Trial sizes (binomial).

X

Fixed-effects design matrix.

spatial

A tulpa_spatial object of type "spde".

family

Distribution family.

phi

Dispersion parameter (negbin / gamma only).

range

Spatial range (NULL -> use spatial$prior_range[1]).

sigma

Marginal SD (NULL -> use spatial$prior_sigma[1]).

max_iter

Newton iterations.

tol

Newton tolerance.

n_threads

OpenMP threads.

weights

Optional per-observation likelihood weights (length length(y)), scaling each row's log-density, score and Fisher curvature.

Value

The raw cpp_laplace_fit_spde result list (mode, log_det_Q, log_marginal, n_iter, converged), augmented with range, sigma, and the spatial spec for downstream prediction.


Mark an expression as a latent block in a tulpa formula

Description

Wraps a user-defined latent block (currently a tgmrf() object) so the formula parser can recognise and route it to the inference layer. The call is structural – it is never executed at fit time. The parser evaluates the inner expression in the formula's environment, removes the latent(...) term from the fixed-effects formula, and attaches the resulting object to parsed$latent_blocks.

Usage

latent(block)

Arguments

block

A tgmrf (or, in the future, tgeneric) object.

Value

Returns block invisibly. Outside a formula context the call is a no-op pass-through.


Create a latent factor specification

Description

Define latent factors for capturing unmeasured shared structure between all processes. Factors are observation-level random effects that enter both linear predictors when shared = TRUE (default).

Usage

latent_factor(
  n_factors = 1L,
  prior = NULL,
  shared = TRUE,
  constraint = c("sum_to_zero", "first_zero"),
  scale = TRUE
)

Arguments

n_factors

Integer; number of latent factors. Default is 1. More factors capture more complex unmeasured structure but increase computational cost and risk overfitting.

prior

Prior for factor standard deviations. Default is a PC prior with P(sigma > 1) = 0.01, which shrinks toward simpler models.

shared

Logical; if TRUE (default), latent factors enter both all process linear predictors identically. If FALSE, factors only affect the first process.

constraint

Identifiability constraint for factors:

  • "sum_to_zero" (default): Factors sum to zero across observations

  • "first_zero": First observation's factor is fixed to zero

scale

Logical; if TRUE (default), factor loadings are standardized to have unit variance before applying sigma.

Details

Why Use Latent Factors?

When modeling multiple processes, they often share unmeasured confounders. For example, in relative abundance data, both the focal species count and total count might be affected by:

Without accounting for these shared drivers, estimates can be biased. Latent factors capture this shared structure without requiring the confounders to be measured.

Mathematical Model

For observation i with K latent factors, on each of two model arms (processes 1 and 2):

\eta^{(1)}_i = X^{(1)}_i \beta^{(1)} + \sum_{k=1}^{K} f_{ik} \sigma_k + \ldots

\eta^{(2)}_i = X^{(2)}_i \beta^{(2)} + \sum_{k=1}^{K} f_{ik} \sigma_k + \ldots

where:

Because factors enter both linear predictors identically (when shared), they cancel in derived quantities (e.g., ratios, differences):

\eta^{(1)}_i - \eta^{(2)}_i

This means factors capture shared effects that would otherwise bias derived quantities.

Choosing n_factors

Relationship to Random Effects

Latent factors differ from random effects in several ways:

You can use both together: random effects for known grouping, factors for residual unmeasured confounding.

Value

A tulpa_latent object consumed by ratio / multi-arm model packages built on tulpa (e.g. tulpaRatio); not read by the single-response tulpa() front door (use latent(tgmrf(...)) for a single-response latent Gaussian block).

See Also

prior_pc() for prior specification; latent() / tgmrf() for a single-response in-formula latent Gaussian block

Examples

# Basic latent factor (single shared factor)
latent_factor()

# Two latent factors
latent_factor(n_factors = 2)

# Custom prior (more regularization)
latent_factor(n_factors = 1, prior = prior_pc(U = 0.5, alpha = 0.01))

# Numerator-only factor (not shared)
latent_factor(n_factors = 1, shared = FALSE)

# The spec is consumed by a ratio / multi-arm model package (tulpaRatio owns
# the two-arm `species | total ~ ...` formula and the negbin/negbin ratio
# family); it is that package's fitter, not the single-response tulpa() front
# door, that reads the `latent_factor()` object.


Extract latent factor posteriors from fit

Description

Extract latent factor posteriors from fit

Usage

latent_factors(fit, summary = TRUE, probs = c(0.025, 0.5, 0.975))

Arguments

fit

A tulpa_fit object

summary

Logical; if TRUE, return summary statistics. If FALSE, return full posterior draws.

probs

Quantiles for summary. Default is c(0.025, 0.5, 0.975).

Value

If summary = TRUE, a data frame with columns for observation, factor, and summary statistics. If summary = FALSE, a matrix of posterior draws.

Examples

## Not run: 
# `fit` is a ratio / multi-arm model fitted with latent factors by a consumer
# package (tulpaRatio owns the two-arm formula and the negbin/negbin ratio
# family and reads the latent_factor() spec):

# Get summary
factors <- latent_factors(fit)
head(factors)

# Get full posterior draws
factor_draws <- latent_factors(fit, summary = FALSE)

## End(Not run)


Description

Links available as a ⁠<family>_<link>⁠ suffix.

Usage

link_names()

Family codes the link layer accepts beyond the bare registry names.

Description

The cross product of the link-capable base families and the links, minus each family's own canonical form (which is already a registry name).

Usage

linked_family_names()

Log-likelihood at the posterior mean

Description

Log-likelihood at the posterior mean

Usage

## S3 method for class 'tulpa_fit'
logLik(object, ...)

Arguments

object

A tulpa_fit object.

...

Ignored.

Value

A logLik object.


Numerically stable log(mean(exp(x)))

Description

Numerically stable log(mean(exp(x)))

Usage

logmeanexp(x)

Numerically stable log(exp(a) + exp(b)) elementwise (vectors / scalars)

Description

Numerically stable log(exp(a) + exp(b)) elementwise (vectors / scalars)

Usage

logsumexp_pair(a, b)

Metropolis-Adjusted Langevin Algorithm (MALA)

Description

Sample from a posterior using MALA: a Langevin proposal driven by the gradient of the log posterior, corrected by a Metropolis- Hastings accept/reject step. Each iteration is one log-posterior + one gradient evaluation. The natural stepping stone between random-walk MH and HMC: cheaper per iteration than HMC (no leapfrog integration), better-mixing than RWMH because the drift term moves toward higher density.

Step size epsilon is adapted via dual averaging during warmup to target an acceptance rate of 0.574 (Roberts & Rosenthal 1998 optimal for high-dimensional Gaussians).

Usage

mala(
  log_posterior,
  grad_log_posterior,
  init,
  n_iter = 2000L,
  warmup = n_iter%/%2L,
  epsilon = 0.1,
  target_accept = 0.574,
  mass_diag = NULL,
  thin = 1L,
  verbose = FALSE
)

Arguments

log_posterior

Function ⁠function(theta) -> numeric⁠ returning the unnormalized log posterior.

grad_log_posterior

Function ⁠function(theta) -> numeric vector⁠ returning the gradient at theta.

init

Numeric vector: initial state (must give finite log_posterior).

n_iter

Total iterations including warmup (default 2000).

warmup

Warmup iterations (step-size adaptation + discarded; default n_iter / 2).

epsilon

Initial step size (default 0.1). Adapted during warmup.

target_accept

Target acceptance during warmup adaptation (default 0.574, the Roberts & Rosenthal 1998 optimum).

mass_diag

Optional preconditioner: a length-d vector of per-dimension variances, used as the diagonal inverse-mass M^-1. The proposal is N(theta + (eps^2/2) * M^-1 * grad, eps^2 * M^-1), so entry j scales the proposal variance along dimension j and the noise standard deviation is eps * sqrt(mass_diag[j]). Default rep(1, d). For posteriors with very different scales across dimensions, set this to (an estimate of) the posterior variances, i.e. the squared posterior SDs.

thin

Keep every thin-th post-warmup sample (default 1).

verbose

Print acceptance + step-size summary at end (default FALSE).

Value

A list with class tulpa_fit carrying:

Tier

Tier 1 (Exact). The MH step makes the chain asymptotically correct. Mixing depends on whether the gradient gives useful local geometry – poor for posteriors with very different scales across dimensions (use a preconditioner or HMC instead).

References

Roberts, G. O., & Tweedie, R. L. (1996). Exponential convergence of Langevin distributions and their discrete approximations. Bernoulli, 2(4), 341-363.

Roberts, G. O., & Rosenthal, J. S. (1998). Optimal scaling of discrete approximations to Langevin diffusions. JRSS B, 60(1), 255-268.

Examples

log_post <- function(t) -0.5 * sum((t - c(1, 2))^2)
grad <- function(t) -(t - c(1, 2))
fit <- mala(log_post, grad, init = c(0, 0), n_iter = 1000)
colMeans(fit$draws)  # near c(1, 2)
fit$mean_accept      # should adapt toward 0.574


Marginal fixed-effect Hessian for spatial-field Laplace fits

Description

The raw fixed-effect block of the joint Hessian gives the conditional precision on \beta \mid u^*, which under-states uncertainty; this returns the marginal precision instead.

Details

The correct marginal precision comes from a Schur complement on the joint Hessian at the mode:

H_\beta^{\mathrm{marg}} = X'WX - X'WZ (Z'WZ + Q_u)^{-1} Z'WX

where Z is the obs->latent map (SPDE: projection matrix A; NNGP: indicator from obs to unique-location field) and Q_u is the spatial precision at the fitted hyperparameters.

These helpers rebuild the spatial precision in R from the spec and fitted hyperparameters, then solve via sparse Cholesky. The shape matches what cpp_laplace_fit_spde / cpp_laplace_fit_gp use internally – Q construction here is a 1:1 port of spde_qbuilder.h (orphan ridge included).


MCMC convergence diagnostics

Description

[Deprecated]

Use diagnostics(), which reads a fit's draws provenance and returns the diagnostic that applies – chain mixing for MCMC draws, approximation reliability for deterministic fits. The name mcmc_diagnostics() described only one of the two branches it already routed between.

Usage

mcmc_diagnostics(
  fit,
  pars = NULL,
  measures = c("rhat", "ess_bulk", "ess_tail"),
  probs = c(0.05, 0.95)
)

Arguments

fit

A tulpa_fit (or subclass) carrying posterior ⁠$draws⁠. Multiple chains are recognised from a 3D ⁠[iter, chain, param]⁠ draws array, a ⁠$chain_id⁠ row map, or an ⁠$n_chains⁠ count over chain-major rows.

pars

Optional character vector of parameter names to restrict to.

measures

Character vector selecting which diagnostics to compute, in output-column order. Available: "rhat", "rhat_bulk", "rhat_fold", "ess_bulk", "ess_tail", "ess_mean", "ess_sd", "mcse_mean", "mcse_sd", "ess_quantile", "mcse_quantile". Defaults to the core set c("rhat", "ess_bulk", "ess_tail"). Applies to chain fits; the approximation-reliability table has a fixed set of columns.

probs

Numeric probabilities for the quantile-based measures ("ess_quantile", "mcse_quantile"); each expands to one column named e.g. ess_q5, ess_q95. Default c(0.05, 0.95). Chain fits only.

Value

The value of diagnostics() for fit.


MCMC chain draws from a fit

Description

Returns a fit's posterior draws only when they form a genuine MCMC chain (⁠$draws_kind == "chain"⁠, or an untagged legacy fit); for an i.i.d. / approximation fit (nested Laplace, VI, SMC, ...) it returns NULL, because chain diagnostics do not apply. This is the accessor diagnostics() gates on. For the provenance-agnostic posterior sample used by summaries, see posterior_sample().

Usage

mcmc_draws(fit)

Arguments

fit

A tulpa_fit (or subclass) carrying posterior ⁠$draws⁠.

Value

The chain draws matrix/array, or NULL for a non-chain fit.

See Also

posterior_sample(), diagnostics()


Model-averaged predictions

Description

Combine fitted values from several models using native model weights computed from the pointwise PSIS-LOO (or WAIC) elpd via tulpa_psis() – no loo package dependency. "loo" / "waic" give stacking weights (the simplex-optimal predictive combination); "pbma" / "pbma+" give pseudo-BMA(+) weights. Every model must carry an ⁠[n_draws x n_obs]⁠ pointwise log-likelihood (fit$draws$log_lik) over the same observations.

Usage

model_average(
  ...,
  weights = c("loo", "waic", "pbma", "pbma+"),
  fitted_fn = fitted
)

Arguments

...

Named tulpa_fit objects fitted to the same observations.

weights

"loo" (stacking, default), "waic", "pbma", or "pbma+".

fitted_fn

Function extracting a length-n_obs fitted vector from a fit (default fitted()).

Value

A list with averaged (the weighted fitted vector), weights (the named model weights), and comparison (the compare_models() table).

References

Yao, Vehtari, Simpson & Gelman (2018). Using stacking to average Bayesian predictive distributions. Bayesian Analysis 13(3):917-1007.

See Also

compare_models().

Examples


set.seed(1)
df <- data.frame(x = rnorm(120))
df$y <- rpois(120, exp(0.4 + 0.5 * df$x))
f1 <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc",
            control = list(n_iter = 500L, warmup = 250L, seed = 1L))
f2 <- tulpa(y ~ 1, data = df, family = "poisson", mode = "hmc",
            control = list(n_iter = 500L, warmup = 250L, seed = 1L))
ma <- model_average(full = f1, null = f2, weights = "waic")
ma$weights


Moran's I test for spatial autocorrelation in residuals

Description

Tests whether residuals exhibit spatial structure after model fitting. Supports inverse-distance and k-nearest-neighbour weight matrices. No external dependencies (uses normal approximation for inference).

Usage

moran_i(
  object,
  coords,
  weights = c("inverse", "knn"),
  k = 10L,
  resid_type = "pearson",
  alternative = c("two.sided", "greater", "less")
)

Arguments

object

A fitted model, or a numeric vector of residuals

coords

N x 2 coordinate matrix (required)

weights

Weight scheme: "inverse" or "knn"

k

Number of neighbours for knn (default 10)

resid_type

Residual type if extracting from model (default "pearson")

alternative

"two.sided", "greater", or "less"

Value

An htest object with Moran's I, expected I, and p-value

Examples

set.seed(1)
coords <- cbind(runif(50), runif(50))
resid  <- rnorm(50)
moran_i(resid, coords)


Number of divergent transitions

Description

Counts divergent transitions recorded by an HMC/NUTS fit, reading whichever field the backend populated (⁠$diagnostics$n_divergent⁠, ⁠$diagnostics$divergent_idx⁠, ⁠$diagnostics$divergent⁠, or the top-level ⁠$n_divergent⁠ / ⁠$divergent⁠).

Usage

n_divergent(fit)

Arguments

fit

A tulpa_fit object.

Value

Integer count of divergent transitions (0 if none are recorded).

See Also

plot_divergences(), check_diagnostics()


Remove all latent(...) calls from a formula's parse tree

Description

Mirror of nobars() for latent(...) calls.

Usage

no_latent_terms(term)

Arguments

term

A language object (formula term)

Value

A language object with all latent(...) calls removed, or NULL if nothing remains.


Remove all bar terms from a formula's parse tree

Description

Recursively rewrites the formula AST, removing any | or || nodes found inside parentheses. Returns the fixed-effects-only formula.

Usage

nobars(term)

Arguments

term

A language object (formula term)

Value

A language object with all bar terms removed, or NULL if nothing remains


Number of observations in a tulpa fit

Description

Number of observations in a tulpa fit

Usage

## S3 method for class 'tulpa_fit'
nobs(object, ...)

Arguments

object

A tulpa_fit object.

...

Ignored.

Value

Integer observation count.


Map cell identifiers to graph node indices

Description

Translate a vector of original cell identifiers into the 1-based node indices of a tulpa_adjacency graph, by key (not by row order). Use it to add the node-index column the model's spatial grouping bar needs to the observation data, which typically has many rows per cell and a different row order than the graph.

Usage

node_index(graph, ids)

Arguments

graph

A tulpa_adjacency object from adjacency().

ids

A vector of cell identifiers to look up (matched against graph$ids).

Value

An integer vector the same length as ids, giving each one's node index in graph (NA for identifiers absent from the graph).

See Also

adjacency().

Examples

grid <- expand.grid(x = 1:3, y = 1:3)
grid$cell <- paste0("c", seq_len(nrow(grid)))
g <- adjacency(grid, id = "cell")

obs <- data.frame(cell = c("c5", "c1", "c9"))
obs$cell_idx <- node_index(g, obs$cell)
obs


Parse a single random effect bar term

Description

Takes a | or || language object and extracts the grouping variable(s), effect terms (intercept, slopes), and correlation structure. The bar operator drives the correlated flag: | -> TRUE (LKJ-Cholesky on the joint slope vector), || -> FALSE (diagonal covariance, one sigma per coefficient). This matches lme4 / glmmTMB and lets downstream packages branch on correlated to choose between independent-sigma and Cholesky parameterizations.

Usage

parse_bar_term(bar_term)

Arguments

bar_term

A language object: |(lhs, rhs) or ||(lhs, rhs)

Details

Nested grouping (1 | a/b) is expanded into one spec per level (a, then a:b). || is preserved as a single spec rather than split into multiple | bars, so the original user intent (one logical RE term with diagonal covariance) round-trips through the parsed object.

Value

A list of RE specs (one per grouping level). Each spec has:


Pathfinder: variational warm-start via L-BFGS + ELBO scoring

Description

Single-path Pathfinder (Zhang, Carpenter, Gelman, Vehtari 2022): run L-BFGS toward the posterior mode, fit a Gaussian at the optimum using the inverse-Hessian estimate, and report draws plus the ELBO. Cheap, derivative-only, embarrassingly parallel – meant as an HMC warm-start, an initialiser for imh_laplace(), or a quick sanity check on the Laplace approximation.

This implementation is single-path only. The full multi-path Pathfinder (K parallel L-BFGS runs + mixture proposal + Pareto- smoothed importance reweighting) is a follow-on. The single-path version is what most users want as a Laplace-equivalent diagnostic.

Usage

pathfinder(
  log_posterior,
  init,
  grad_log_posterior = NULL,
  n_draws = 1000L,
  max_iter = 100L,
  tol = 1e-06,
  verbose = FALSE
)

Arguments

log_posterior

Function ⁠function(theta) -> numeric⁠ returning the unnormalized log posterior at theta.

init

Numeric vector: initial point for L-BFGS. Should be in the support of the posterior (finite log_posterior).

grad_log_posterior

Optional function returning the gradient of log_posterior at theta. If NULL, gradients are computed numerically via stats::optim's built-in finite differences.

n_draws

Number of draws from the fitted Gaussian (default 1000).

max_iter

L-BFGS iteration cap (default 100).

tol

Gradient-norm tolerance for L-BFGS convergence (default 1e-6).

verbose

Print L-BFGS / ELBO summary at end (default FALSE).

Value

A list with class tulpa_fit carrying:

Tier

Tier 2 (Structured). The output is a Gaussian approximation, not samples from the exact posterior – same epistemic class as tulpa_laplace(). Pair with imh_laplace() for an exact-tier upgrade.

References

Zhang, L., Carpenter, B., Gelman, A., & Vehtari, A. (2022). Pathfinder: parallel quasi-Newton variational inference. Journal of Machine Learning Research, 23(306), 1-49.

See Also

imh_laplace() for an exact-tier MH using the Pathfinder Gaussian as proposal; bridge_sampling() for marginal-likelihood estimation on the resulting draws.

Examples

# Toy: 2-D conjugate normal.
y <- c(0.5, -0.7)
log_post <- function(t) {
  sum(dnorm(y, t, 1, log = TRUE)) +
    sum(dnorm(t, 0, sqrt(10), log = TRUE))
}
pf <- pathfinder(log_post, init = c(0, 0), n_draws = 2000)
pf$mode      # near c(0.45, -0.64)
pf$elbo


PIT (Probability Integral Transform) residuals

Description

For each observation, computes the quantile of the observed value within the posterior predictive distribution. If the model is correct, PIT residuals follow Uniform(0, 1).

Usage

pit_residuals(object, ...)

## Default S3 method:
pit_residuals(object, observed = NULL, nsim = 250L, seed = 123L, ...)

Arguments

object

A fitted model with a simulate() method, or a matrix of simulated values (n_obs x nsim)

...

Passed to methods.

observed

Observed response vector (required if object is a matrix)

nsim

Number of simulations (default 250)

seed

Random seed (default 123)

Details

For integer-valued responses, a randomisation step avoids discrete artefacts: the residual is drawn uniformly between P(sim < obs) and P(sim <= obs).

Value

Numeric vector of length n_obs with values in ⁠[0, 1]⁠


Plot fixed-effect posteriors

Description

Plot fixed-effect posteriors

Usage

## S3 method for class 'tulpa_fit'
plot(x, type = c("density", "trace", "pairs", "smooth"), term = NULL, ...)

Arguments

x

A tulpa_fit object.

type

One of "density", "trace", "pairs", "smooth". The Laplace tier has no draws, so it always shows the Gaussian densities of the fixed effects. "smooth" draws the fitted curve of each s(...) term and requires a fit carrying one.

term

For type = "smooth", which smoother to draw: index or covariate name. NULL (default) draws every one. Ignored by the other types.

...

Passed to plotting functions.

Value

The input x, returned invisibly. Called for the side effect of producing base-graphics plots of the fixed-effect posteriors.


Plot method for tulpa_prior_predict

Description

Density overlay of prior predictive draws. Requires bayesplot; falls back to base graphics matplot of a subset of draws otherwise.

Usage

## S3 method for class 'tulpa_prior_predict'
plot(x, process = 1L, max_draws = 50L, ...)

Arguments

x

A tulpa_prior_predict object

process

Process index or name (multi-process families)

max_draws

Maximum draws to overlay. Default 50.

...

Passed through.

Value

A ggplot object (via bayesplot) when bayesplot is installed; otherwise NULL invisibly, after drawing a base-graphics overlay. Called for the density overlay of the prior predictive draws.


Plot method for spatiotemporal effects

Description

Plot method for spatiotemporal effects

Usage

## S3 method for class 'tulpa_st_summary'
plot(x, type = "heatmap", ...)

Arguments

x

Spatiotemporal effects object

type

Plot type: "heatmap" (default), "time_series", or "spatial_map"

...

Additional arguments passed to plotting functions

Value

A ggplot object when ggplot2 is installed; otherwise NULL invisibly, after drawing a base-graphics plot. Called for the side effect of visualizing the spatiotemporal interaction effects.


Plot method for tulpa_svc_posterior

Description

Plot method for tulpa_svc_posterior

Usage

## S3 method for class 'tulpa_svc_posterior'
plot(x, term = 1, type = "mean", ...)

Arguments

x

A tulpa_svc_posterior object

term

Which term to plot (name or index). Default: first term.

type

Plot type: "mean" (default), "sd", or quantile (e.g., "q50")

...

Additional arguments passed to plotting functions

Value

A ggplot object when ggplot2 is installed; otherwise NULL invisibly, after drawing a base-graphics map. Called for the side effect of mapping the selected spatially-varying coefficient.


Plot method for tulpa_temporal_posterior

Description

Plot method for tulpa_temporal_posterior

Usage

## S3 method for class 'tulpa_temporal_posterior'
plot(x, component = NULL, type = "ribbon", ...)

Arguments

x

A tulpa_temporal_posterior object

component

Which component to plot (for multi-scale). Default: first.

type

Plot type: "ribbon" (default) or "line"

...

Additional arguments passed to plotting functions

Value

A ggplot object when ggplot2 is installed; otherwise NULL invisibly, after drawing a base-graphics plot. Called for the side effect of plotting the temporal-effect posterior.


Plot method for tulpa_tvc_posterior

Description

Plot method for tulpa_tvc_posterior

Usage

## S3 method for class 'tulpa_tvc_posterior'
plot(x, term = 1, type = "ribbon", ...)

Arguments

x

A tulpa_tvc_posterior object

term

Which term to plot (name or index). Default: first term.

type

Plot type: "ribbon" (default) or "line"

...

Additional arguments passed to plotting functions

Value

A ggplot object when ggplot2 is installed; otherwise NULL invisibly, after drawing a base-graphics plot. Called for the side effect of plotting the selected temporally-varying coefficient.


Plot Autocorrelation Functions

Description

Creates autocorrelation function (ACF) plots for selected parameters. High autocorrelation indicates slow mixing and low effective sample size.

Usage

plot_acf(fit, pars = NULL, lags = 25, n_pars = 6)

Arguments

fit

A tulpa_fit object.

pars

Character vector of parameter names. If NULL, selects worst-mixing parameters based on ESS.

lags

Maximum number of lags to compute (default: 25).

n_pars

Maximum number of parameters to plot (default: 6).

Details

Ideal ACF plots show rapid decay to zero. Slow decay indicates high autocorrelation, which reduces effective sample size and may indicate poor mixing.

Value

A ggplot object (if ggplot2 available) or base R plot (invisible).

See Also

plot_ess(), diagnostics()

Examples


set.seed(123)
df <- data.frame(x = rnorm(60))
df$y <- rpois(60, exp(0.5 + 0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc",
             control = list(n_iter = 500L, warmup = 250L, n_chains = 2L,
                            seed = 1L))
plot_acf(fit)
plot_acf(fit, lags = 10)



Base R ACF plot

Description

Base R ACF plot

Usage

plot_acf_base(draws, pars, lags)

Diagnostic Plotting Functions for tulpa Models

Description

Visual diagnostic tools for MCMC convergence assessment. All functions provide base R fallbacks when ggplot2/bayesplot are unavailable.

Creates a combined diagnostic figure with Rhat, ESS, trace plot, and energy/ACF panels. Requires the patchwork package for layout.

Usage

plot_diagnostics(fit, pars = NULL)

Arguments

fit

A tulpa_fit object.

pars

Character vector of parameter names for trace plot. If NULL, uses the parameter with worst Rhat.

Details

Creates a 2x2 grid:

Value

A combined plot (ggplot + patchwork) or NULL if requirements not met.

See Also

diagnostic_summary(), plot_rhat(), plot_ess()

Examples


set.seed(123)
df <- data.frame(x = rnorm(60))
df$y <- rpois(60, exp(0.5 + 0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc",
             control = list(n_iter = 500L, warmup = 250L, n_chains = 2L,
                            seed = 1L))
plot_diagnostics(fit)



Plot Divergent Transitions

Description

Creates visualizations to investigate divergent transitions. Parallel coordinates and scatter plots highlight where in parameter space divergences occur.

Usage

plot_divergences(fit, pars = NULL, type = c("parcoord", "scatter"))

Arguments

fit

A tulpa_fit object (HMC backend).

pars

Character vector of parameter names. If NULL, uses variance parameters.

type

Plot type: "parcoord" (parallel coordinates) or "scatter".

Details

Divergent transitions indicate regions of high posterior curvature that the sampler cannot efficiently explore. Common causes:

Value

A ggplot object or base R plot (invisible).

See Also

plot_pairs(), n_divergent()

Examples


set.seed(123)
df <- data.frame(x = rnorm(60))
df$y <- rpois(60, exp(0.5 + 0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc",
             control = list(n_iter = 500L, warmup = 250L, n_chains = 2L,
                            seed = 1L))
plot_divergences(fit)
plot_divergences(fit, type = "scatter")



Parallel coordinates plot for divergences

Description

Parallel coordinates plot for divergences

Usage

plot_divergences_parcoord(draws_df, pars)

Base R parallel coordinates

Description

Base R parallel coordinates

Usage

plot_divergences_parcoord_base(scaled, pars, divergent)

Scatter plot matrix for divergences

Description

Scatter plot matrix for divergences

Usage

plot_divergences_scatter(draws_df, pars)

Plot Energy Diagnostic (E-BFMI)

Description

Creates overlaid histograms of marginal energy and energy transition, along with the E-BFMI statistic. Low E-BFMI indicates poor exploration of the posterior.

Usage

plot_energy(fit)

Arguments

fit

A tulpa_fit object (HMC backend).

Details

E-BFMI (Energy Bayesian Fraction of Missing Information) compares the distribution of energy levels to energy transitions. Values below 0.3 indicate the sampler may not be exploring the full posterior.

Value

A ggplot object (if ggplot2 available) or base R plot.

See Also

diagnostic_summary(), check_diagnostics()

Examples


set.seed(123)
df <- data.frame(x = rnorm(60))
df$y <- rpois(60, exp(0.5 + 0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc",
             control = list(n_iter = 500L, warmup = 250L, n_chains = 2L,
                            seed = 1L))
plot_energy(fit)



Base R energy plot

Description

Base R energy plot

Usage

plot_energy_base(energy, energy_diff, e_bfmi, status)

Plot Effective Sample Size Diagnostic

Description

Creates a visual display of effective sample size (ESS) for all parameters, expressed as a ratio of ESS to total samples. Low ESS indicates high autocorrelation.

Usage

plot_ess(fit, type = c("bulk", "tail"), threshold = 400, pars = NULL)

Arguments

fit

A tulpa_fit object.

type

Type of ESS: "bulk" (default) or "tail".

threshold

Minimum acceptable ESS (default: 400).

pars

Character vector of parameter names to include.

Details

ESS/iter ratio interpretation:

Value

A ggplot object (if ggplot2 available) or base R plot (invisible).

See Also

plot_rhat(), diagnostic_summary(), diagnostics()

Examples


set.seed(123)
df <- data.frame(x = rnorm(60))
df$y <- rpois(60, exp(0.5 + 0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc",
             control = list(n_iter = 500L, warmup = 250L, n_chains = 2L,
                            seed = 1L))
plot_ess(fit)
plot_ess(fit, type = "tail")



Base R ESS plot

Description

Base R ESS plot

Usage

plot_ess_base(diag, threshold, type)

Plot spatial predictions as a map

Description

Create publication-ready maps from a tulpa spatial fit: the fitted response surface or the prediction uncertainty (credible-interval width), on the response scale.

Usage

plot_map(
  x,
  coords = NULL,
  what = c("fitted", "uncertainty"),
  summary = c("median", "mean", "q2.5", "q97.5", "sd"),
  newdata = NULL,
  title = NULL,
  palette = "viridis",
  points = FALSE,
  point_color = "grey30",
  point_size = 0.5,
  na_color = "transparent",
  crs = NULL,
  legend_title = NULL,
  ...
)

Arguments

x

A tulpa_fit object with spatial structure, or a data frame containing predictions with coordinate columns.

coords

A data frame or matrix with spatial coordinates (columns named 'x'/'y', 'X'/'Y', 'lon'/'lat', 'longitude'/'latitude', or 'Easting'/'Northing'). Required if x is a data frame.

what

What to plot: "fitted" (default, the response-scale point prediction) or "uncertainty" (the 95% credible-interval width).

summary

Which summary statistic for what = "fitted": "median" (default) / "mean" (both the point prediction), "q2.5" / "q97.5" (the credible bounds), or "sd" (the link-scale standard error).

newdata

Optional data frame with prediction locations and covariates. If NULL and x is a tulpa_fit, uses fitted values at observed locations.

title

Plot title. If NULL, auto-generated based on what.

palette

Color palette: "viridis" (default), "magma", "plasma", "inferno", "cividis", "mako", "rocket", or a custom vector of colors.

points

Logical; if TRUE, overlay observation points. Default FALSE.

point_color

Color for observation points. Default "grey30".

point_size

Size for observation points. Default 0.5.

na_color

Color for NA values. Default "transparent".

crs

Coordinate reference system (proj4 string or EPSG code). If NULL, uses planar coordinates.

legend_title

Title for the color legend. If NULL, auto-generated.

...

Additional arguments passed to ggplot2 theme functions.

Details

This function provides a streamlined workflow for visualizing spatial predictions from tulpa models. It handles:

For custom maps or more control, extract predictions using predict() and use ggplot2 directly with geom_stars() or geom_sf().

Value

A ggplot2 object that can be further customized.

Required packages

This function requires ggplot2. For raster-style maps, stars and sf are also needed. Install with:

install.packages(c("ggplot2", "stars", "sf"))

See Also

predict.tulpa_fit() for predictions at new locations

Examples


if (requireNamespace("ggplot2", quietly = TRUE)) {
  set.seed(123)
  n_sites <- 20
  df <- data.frame(
    y = rbinom(n_sites, 20, 0.4),
    elevation = rnorm(n_sites),
    site = factor(seq_len(n_sites)),
    lon = runif(n_sites),
    lat = runif(n_sites)
  )
  adj <- matrix(0, n_sites, n_sites)
  for (i in 1:(n_sites - 1)) adj[i, i + 1] <- adj[i + 1, i] <- 1
  fit <- tulpa(
    y ~ elevation + spatial(site),
    data = df,
    family = "binomial",
    n_trials = rep(20L, n_sites),
    spatial = spatial_car(adj, group_var = "site"),
    mode = "laplace"
  )
  # Areal (CAR/ICAR) fits carry no point coordinates, so pass `coords`:
  cc <- df[, c("lon", "lat")]
  plot_map(fit, coords = cc)                      # fitted response surface
  plot_map(fit, what = "uncertainty", coords = cc)
  plot_map_panel(fit, coords = cc)                # both side by side
}



Plot multiple maps in a grid

Description

Create a multi-panel figure with the fitted-value map and the prediction-uncertainty map side by side.

Usage

plot_map_panel(x, newdata = NULL, ncol = 2, ...)

Arguments

x

A tulpa_fit object with spatial structure.

newdata

Optional data frame with prediction locations.

ncol

Number of columns in the grid. Default 2.

...

Additional arguments passed to plot_map().

Value

A patchwork object (if patchwork is installed) or a list of ggplots.

Examples


if (requireNamespace("ggplot2", quietly = TRUE)) {
  set.seed(123)
  n_sites <- 20
  df <- data.frame(
    y = rbinom(n_sites, 20, 0.4),
    elevation = rnorm(n_sites),
    site = factor(seq_len(n_sites)),
    lon = runif(n_sites),
    lat = runif(n_sites)
  )
  adj <- matrix(0, n_sites, n_sites)
  for (i in 1:(n_sites - 1)) adj[i, i + 1] <- adj[i + 1, i] <- 1
  fit <- tulpa(
    y ~ elevation + spatial(site),
    data = df,
    family = "binomial",
    n_trials = rep(20L, n_sites),
    spatial = spatial_car(adj, group_var = "site"),
    mode = "laplace"
  )
  cc <- df[, c("lon", "lat")]
  plot_map_panel(fit, coords = cc)
  plot_map_panel(fit, coords = cc, ncol = 1)
}



Plot Bivariate Parameter Posteriors (Pairs Plot)

Description

Creates a pairs plot showing bivariate relationships between parameters. Divergent transitions (if present) are highlighted to help identify problematic posterior regions.

Usage

plot_pairs(
  fit,
  pars = NULL,
  highlight_divergent = TRUE,
  n_pars = 5,
  alpha = 0.3
)

Arguments

fit

A tulpa_fit object.

pars

Character vector of parameter names. If NULL, selects main variance parameters.

highlight_divergent

Logical; highlight divergent transitions in red (default: TRUE).

n_pars

Maximum number of parameters (default: 5).

alpha

Point transparency (default: 0.3).

Details

Pairs plots help identify:

Value

A ggplot object (if ggplot2/GGally available) or base R plot.

See Also

plot_divergences(), diagnostics()

Examples


set.seed(123)
df <- data.frame(x = rnorm(60))
df$y <- rpois(60, exp(0.5 + 0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc",
             control = list(n_iter = 500L, warmup = 250L, n_chains = 2L,
                            seed = 1L))
plot_pairs(fit)
plot_pairs(fit, n_pars = 2)



Base R pairs plot

Description

Base R pairs plot

Usage

plot_pairs_base(draws_df, pars, has_divergent, alpha)

Plot Rhat Convergence Diagnostic

Description

Creates a visual display of Rhat values for all parameters, with color-coding to highlight convergence issues. Rhat values > 1.01 indicate potential convergence problems.

Usage

plot_rhat(fit, threshold = 1.01, pars = NULL)

Arguments

fit

A tulpa_fit object.

threshold

Rhat threshold for warnings (default: 1.01).

pars

Character vector of parameter names to include. If NULL (default), includes all main parameters (excludes high-dimensional RE/spatial).

Details

Color coding:

Value

A ggplot object (if ggplot2 available) or base R plot (invisible).

See Also

plot_ess(), diagnostic_summary(), diagnostics()

Examples

# Diagnostic plots require a fitted tulpa model
# See tulpa() examples for fitting models


set.seed(123)
df <- data.frame(x = rnorm(60))
df$y <- rpois(60, exp(0.5 + 0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson", mode = "hmc",
             control = list(n_iter = 500L, warmup = 250L, n_chains = 2L,
                            seed = 1L))
plot_rhat(fit)



Base R Rhat plot

Description

Base R Rhat plot

Usage

plot_rhat_base(diag, threshold)

Fit a post-hoc linear model on estimated parameters

Description

Useful for exploring drivers of occupancy/detection/abundance variation after model fitting. Fits a weighted linear model and optionally generates bootstrap confidence intervals.

Usage

post_hoc_lm(
  formula,
  data,
  weights = NULL,
  n_boot = 1000L,
  probs = c(0.025, 0.975)
)

Arguments

formula

Model formula (e.g., psi_hat ~ trait1 + trait2).

data

A data.frame with response and predictors.

weights

Optional weights (e.g., inverse of standard errors).

n_boot

Number of bootstrap replicates for CI (default 1000, 0 to skip).

probs

Quantile probabilities for bootstrap CI (default 0.025, 0.975).

Value

A list of class "post_hoc_lm" with:

summary

data.frame of coefficient estimates and CIs

lm_fit

the underlying lm object

boot_coefs

matrix of bootstrap coefficient samples (if n_boot > 0)

R2

R-squared from the fitted model

Examples

# Explore drivers of per-site estimates after fitting a model.
site <- data.frame(
  psi_hat = c(0.2, 0.5, 0.8, 0.4, 0.6, 0.3),
  se      = c(0.05, 0.04, 0.06, 0.05, 0.03, 0.05),
  trait   = c(1.0, 2.5, 3.8, 1.9, 3.1, 1.2)
)
fit <- post_hoc_lm(psi_hat ~ trait, data = site,
                   weights = 1 / site$se^2, n_boot = 200L)
fit

Posterior predictive replicates

Description

Draw replicated responses from the posterior predictive distribution: the linear predictor is rebuilt per posterior draw (fixed effects, formula random effects, offset, and a posterior-mean SPDE field when present) and pushed through the family's sampling distribution.

Fits carrying posterior draws use them directly (fixed and random effects jointly per draw). The Laplace tier samples the fixed effects from the Gaussian approximation N(coef(fit), vcov(fit)) and holds the random effects at their posterior mode, so its replicates understate the RE posterior uncertainty. At newdata the prediction is population level (random effects at zero), matching predict.tulpa_fit().

Usage

posterior_predict(object, ...)

## S3 method for class 'tulpa_fit'
posterior_predict(
  object,
  newdata = NULL,
  ndraws = NULL,
  n_trials = NULL,
  seed = NULL,
  ...
)

Arguments

object

A tulpa_fit object from tulpa().

...

Passed to methods.

newdata

Optional data frame of covariates to predict at. Population level (fixed effects only); NULL (default) replicates at the training data with random effects and offset included.

ndraws

Number of posterior draws to use. Defaults to all stored draws, or 400 on the draw-free Laplace tier.

n_trials

Binomial / beta-binomial trial counts for the replicates. Defaults to the training trials when newdata is NULL, else 1.

seed

Optional integer seed (RNG state is restored on exit).

Value

A ⁠ndraws x n_obs⁠ numeric matrix of replicated responses.

See Also

pp_check(), which uses these replicates; simulate.tulpa_fit().

Examples


set.seed(1)
d <- data.frame(y = rpois(100, 4), x = rnorm(100))
fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace")
yrep <- posterior_predict(fit, ndraws = 100)
dim(yrep)  # 100 x 100


Posterior parameter sample from a fit

Description

Returns a fit's posterior draws for summary purposes (quantiles, derived quantities, density plots), regardless of how they were produced – an MCMC chain, a nested-Laplace node mixture, or a variational sample all answer here. For the chain-only view used by convergence diagnostics, see mcmc_draws().

Usage

posterior_sample(fit)

Arguments

fit

A tulpa_fit (or subclass) carrying posterior ⁠$draws⁠.

Details

A fit that carries no draws returns NULL with a message naming its backend, the posterior representation it carries instead, and the accessor that turns that representation into draws where one exists – an absent draws matrix is a property of the backend, not a failure of the accessor, and saying which is the difference between a diagnosable answer and a bare NULL.

Value

The posterior draws matrix/array, or NULL if the fit carries none.

See Also

mcmc_draws(), tulpa_posterior_draws(), diagnostics()

Examples


set.seed(1)
df <- data.frame(x = rnorm(80))
df$y <- rpois(80, exp(0.5 + 0.3 * df$x))
fit <- tulpa(y ~ x, data = df, family = "poisson")
dim(posterior_sample(fit))


Posterior predictive check

Description

Generate posterior predictive checks for a fitted tulpa model. Compares observed data to replicated data from the posterior predictive distribution.

Usage

pp_check(object, ...)

## S3 method for class 'tulpa_fit'
pp_check(
  object,
  type = c("dens_overlay", "scatter", "intervals", "stat"),
  component = NULL,
  stat = mean,
  ndraws = 50,
  ...
)

Arguments

object

A tulpa_fit object

...

Additional arguments passed to bayesplot functions

type

Type of check: "dens_overlay", "scatter", "intervals", "stat"

component

Which component: "numerator", "denominator", or "both"

stat

Function for "stat" type (default: mean)

ndraws

Number of posterior draws to use (default: 50 for plots)

Value

A ggplot object

Examples

# pp_check is a generic; model packages (e.g. tulpaObs, tulpaRatio) provide
# the posterior-predictive method for their fits.

set.seed(123)
n <- 200L
df <- data.frame(y = rpois(n, 5), x = rnorm(n),
                 site = factor(rep(1:10, each = 20)))
fit <- tulpa(y ~ x + (1 | site), data = df, family = "poisson",
             mode = "hmc", control = list(n_iter = 500, warmup = 250))
# Density overlay (dispatches to the model package's pp_check method).
if (requireNamespace("bayesplot", quietly = TRUE)) {
  pp_check(fit, type = "dens_overlay")
}



Single component pp_check

Description

Single component pp_check

Usage

pp_check_single(y, yrep, type, stat, ndraws, title_suffix, ...)

Predict at new covariate values (population level)

Description

Prediction of the linear predictor at newdata, on the link or response scale. The fixed-effect part is ⁠X beta⁠ with credible bounds from the fixed-effect covariance (vcov()). For a fit carrying a continuous spatial field, the posterior-mean field is interpolated (kriged) to the newdata coordinates and added to the linear predictor by default, so predict() gives the conditional (location-specific) prediction. Three continuous field families are supported: an SPDE Matern field (spatial_spde()), projected through the mesh; a Hilbert-space GP field (spatial_gp(approx = "hsgp")), where the Laplacian basis is re-evaluated at the new coordinates (with the training centring / boundary); and a GP / NNGP field (spatial_gp()), interpolated by the NNGP conditional mean at each new location's nearest training locations. The HSGP and GP/NNGP fields are marginalised over the hyperparameter grid (not plugged in at the posterior mean). Ordinary random effects are held at zero (population level); add group effects from ranef() when needed.

Usage

## S3 method for class 'tulpa_fit'
predict(
  object,
  newdata = NULL,
  type = c("link", "response"),
  se.fit = FALSE,
  level = 0.95,
  include_field = TRUE,
  ...
)

Arguments

object

A tulpa_fit object.

newdata

Data frame of covariates (and, for an SPDE fit, the coordinate columns named in the spec's coordinate formula). If NULL, predicts at the training design (requires ⁠$model_matrix⁠).

type

"link" (linear predictor) or "response" (mean scale). For a binomial fit the "response" scale here is the per-trial success probability g^{-1}(eta) (there is no n_trials at newdata); this differs from fitted(), which returns the trial-scaled expected count at the training design.

se.fit

If TRUE, also return the link-scale standard error and credible bounds. With an included SPDE field the SE propagates the joint (fixed-effect, field) posterior precision at the fitted hyperparameters – including the cross term – conditional on ⁠(range, sigma)⁠ (a nested fit's hyperparameter-grid spread is not propagated, so the bound is mildly optimistic when that posterior is wide). Integer-nu, no-RE SPDE fits only; other layouts decline with an explanation.

level

Credible-interval level (default 0.95).

include_field

For a continuous-spatial fit (SPDE, HSGP, or GP/NNGP), add the kriged field to the prediction (default TRUE). FALSE gives the fixed-effect (population) prediction. Ignored for fits with no continuous field. For an HSGP or GP/NNGP fit the field is added to the point prediction but its uncertainty is not yet propagated into se.fit (the interval reflects the fixed-effect covariance only).

...

Ignored.

Value

If se.fit = FALSE, a numeric vector. If se.fit = TRUE, a data frame with fit, se.fit (link scale), lower, upper on the requested scale.


Prepare latent factor data for HMC

Description

Prepare latent factor data for HMC

Usage

prepare_latent_for_hmc(latent, N)

Arguments

latent

A validated tulpa_latent object (or NULL)

N

Number of observations

Value

List with latent factor data for C++


Prepare map data from data frame

Description

Prepare map data from data frame

Usage

prepare_map_data_from_df(x, coords, what, summary)

Prepare map data from tulpa_fit object

Description

Prepare map data from tulpa_fit object

Usage

prepare_map_data_from_fit(fit, what, summary, newdata, coords = NULL)

Print method for diagnostic summary

Description

Print method for diagnostic summary

Usage

## S3 method for class 'tulpa_diagnostic_summary'
print(x, ...)

Arguments

x

A tulpa_diagnostic_summary object.

...

Ignored.

Value

The input x, returned invisibly. Called for the side effect of printing the diagnostic summary to the console.


Print method for Geweke test

Description

Print method for Geweke test

Usage

## S3 method for class 'tulpa_geweke'
print(x, ...)

Arguments

x

A tulpa_geweke object.

...

Ignored.

Value

The input x, returned invisibly. Called for the side effect of printing the Geweke convergence diagnostic to the console.


Print method for tulpa_gp

Description

Print method for tulpa_gp

Usage

## S3 method for class 'tulpa_gp'
print(x, ...)

Arguments

x

A tulpa_gp object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing the Gaussian-process spatial specification to the console.


Print method for tulpa_hsgp

Description

Print method for tulpa_hsgp

Usage

## S3 method for class 'tulpa_hsgp'
print(x, ...)

Arguments

x

A tulpa_hsgp object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing the Hilbert-space GP spatial specification to the console.


Print method for tulpa_latent

Description

Print method for tulpa_latent

Usage

## S3 method for class 'tulpa_latent'
print(x, ...)

Arguments

x

A tulpa_latent object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing the latent factor specification to the console.


Print method for tulpa_multiscale

Description

Print method for tulpa_multiscale

Usage

## S3 method for class 'tulpa_multiscale'
print(x, ...)

Arguments

x

A tulpa_multiscale object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing the multi-scale spatial specification to the console.


Print method for nested-Laplace fits

Description

Compact one-screen summary of a tulpa_nested_laplace() or tulpa_nested_laplace_joint() fit: the hyperparameters integrated over, the outer-grid size, the outer Pareto-\hat{k} accuracy diagnostic when present, and the wall-clock timing line ("fit in 5h 25m (grid 2h 09m)") when fit$timing is attached. Inherited by the single-block joint and multi-block joint subclasses.

Usage

## S3 method for class 'tulpa_nested_laplace'
print(x, ...)

Arguments

x

A tulpa_nested_laplace fit (or a joint subclass).

...

Ignored.

Value

x, invisibly.


Print method for tulpa_prior

Description

Print method for tulpa_prior

Usage

## S3 method for class 'tulpa_prior'
print(x, ...)

Arguments

x

A tulpa_prior object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing the prior specification to the console.


Print method for tulpa_prior_predict

Description

Print method for tulpa_prior_predict

Usage

## S3 method for class 'tulpa_prior_predict'
print(x, ...)

Arguments

x

A tulpa_prior_predict object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing a summary of the prior predictive draws to the console.


Print method for tulpa_priors

Description

Print method for tulpa_priors

Usage

## S3 method for class 'tulpa_priors'
print(x, ...)

Arguments

x

A tulpa_priors object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing the full prior specification to the console.


Print method for tulpa_rsr

Description

Print method for tulpa_rsr

Usage

## S3 method for class 'tulpa_rsr'
print(x, ...)

Arguments

x

A tulpa_rsr object

...

Passed to underlying print method

Value

The input x, returned invisibly. Called for the side effect of printing the spatial specification and its restricted-spatial-regression modifier to the console.


Print method for tulpa_simulate

Description

Print method for tulpa_simulate

Usage

## S3 method for class 'tulpa_simulate'
print(x, ...)

Arguments

x

A tulpa_simulate object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing a summary of the simulated datasets to the console.


Print method for tulpa_spatial

Description

Print method for tulpa_spatial

Usage

## S3 method for class 'tulpa_spatial'
print(x, ...)

Arguments

x

A tulpa_spatial object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing the spatial specification to the console.


Print method for tulpa_svc

Description

Print method for tulpa_svc

Usage

## S3 method for class 'tulpa_svc'
print(x, ...)

Arguments

x

A tulpa_svc object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing the spatially-varying-coefficient specification to the console.


Print method for tulpa_svc_posterior

Description

Print method for tulpa_svc_posterior

Usage

## S3 method for class 'tulpa_svc_posterior'
print(x, ...)

Arguments

x

A tulpa_svc_posterior object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing a summary of the spatially-varying-coefficient posterior to the console.


Print method for tulpa_temporal

Description

Print method for tulpa_temporal

Usage

## S3 method for class 'tulpa_temporal'
print(x, ...)

Arguments

x

A tulpa_temporal object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing the temporal specification to the console.


Print method for tulpa_temporal_gp

Description

Print method for tulpa_temporal_gp

Usage

## S3 method for class 'tulpa_temporal_gp'
print(x, ...)

Arguments

x

A tulpa_temporal_gp object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing the Gaussian-process temporal specification to the console.


Print method for tulpa_temporal_multiscale

Description

Print method for tulpa_temporal_multiscale

Usage

## S3 method for class 'tulpa_temporal_multiscale'
print(x, ...)

Arguments

x

A tulpa_temporal_multiscale object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing the multi-scale temporal specification to the console.


Print method for tulpa_temporal_posterior

Description

Print method for tulpa_temporal_posterior

Usage

## S3 method for class 'tulpa_temporal_posterior'
print(x, ...)

Arguments

x

A tulpa_temporal_posterior object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing a summary of the temporal-effect posterior to the console.


Print method for tulpa_tvc

Description

Print method for tulpa_tvc

Usage

## S3 method for class 'tulpa_tvc'
print(x, ...)

Arguments

x

A tulpa_tvc object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing the temporally-varying-coefficient specification to the console.


Print method for tulpa_tvc_posterior

Description

Print method for tulpa_tvc_posterior

Usage

## S3 method for class 'tulpa_tvc_posterior'
print(x, ...)

Arguments

x

A tulpa_tvc_posterior object

...

Ignored

Value

The input x, returned invisibly. Called for the side effect of printing a summary of the temporally-varying-coefficient posterior to the console.


Description

Thin wrapper around format() for tulpa_prior objects. Each ⁠prior_*()⁠ constructor attaches a subclass (e.g. tulpa_prior_normal) so adding a new distribution is one new ⁠format.tulpa_prior_<dist>()⁠ method – no central if/else to extend.

Usage

print_prior(prior, indent = "")

Arguments

prior

A tulpa_prior object

indent

Indentation string


Beta prior

Description

Specify a beta prior for a parameter bounded in (0, 1).

Usage

prior_beta(alpha = 1, beta = 1)

Arguments

alpha

First shape parameter. Must be positive.

beta

Second shape parameter. Must be positive.

Details

Value

A tulpa_prior object

Examples

prior_beta(1, 1)   # Uniform
prior_beta(2, 2)   # Symmetric, centered at 0.5
prior_beta(5, 2)   # Skewed toward 1 (for high autocorrelation)


Exponential prior

Description

Specify an exponential prior for a positive parameter.

Usage

prior_exponential(rate = 1)

Arguments

rate

Rate parameter (lambda). Must be positive.

Value

A tulpa_prior object

Examples

prior_exponential(1)


Build a prior list for tulpa_nested_laplace() from a tulpa spec object

Description

Validates a tulpa_temporal or tulpa_spatial specification against data, then converts it to the prior list shape consumed by tulpa_nested_laplace(). Mainly an internal helper for callers that already have a fitted spec; users typically pass spec + data directly to tulpa_nested_laplace() instead.

Supported spec types:

SPDE is the one continuous field not built here – it carries its own (range, sigma) FEM integrator; call fit_spde() with the spatial_spde() spec.

Usage

prior_from_spec(spec, data)

Arguments

spec

A tulpa_temporal or tulpa_spatial object.

data

Data frame the spec resolves time/group/site indices against.

Value

A prior list ready for tulpa_nested_laplace().


Gamma prior

Description

Specify a gamma prior for a positive parameter.

Usage

prior_gamma(shape = 2, rate = 0.1)

Arguments

shape

Shape parameter (alpha). Must be positive.

rate

Rate parameter (beta). Must be positive.

Details

Mean = shape/rate, Variance = shape/rate^2.

Value

A tulpa_prior object

Examples

prior_gamma(2, 0.1)  # Mean = 20, weakly informative
prior_gamma(1, 1)    # Exponential(1)


Half-Cauchy prior

Description

Specify a half-Cauchy prior for a positive parameter. Has heavier tails than half-normal, often used for variance parameters.

Usage

prior_half_cauchy(scale = 2.5)

Arguments

scale

Scale parameter. Must be positive.

Value

A tulpa_prior object

Examples

prior_half_cauchy(2.5)


Half-normal prior

Description

Specify a half-normal (truncated at 0) prior for a positive parameter.

Usage

prior_half_normal(sd = 1)

Arguments

sd

Scale parameter. Must be positive.

Value

A tulpa_prior object

Examples

prior_half_normal(1)


Normal prior

Description

Specify a normal prior for a parameter.

Usage

prior_normal(mean = 0, sd = 2.5)

Arguments

mean

Prior mean. Default 0.

sd

Prior standard deviation. Must be positive.

Value

A tulpa_prior object

Examples

prior_normal(0, 2.5)
prior_normal(0, 1)


Penalized complexity (PC) prior

Description

Specify a PC prior for a positive parameter (typically a standard deviation). PC priors shrink toward simpler models by penalizing deviation from a base model.

Usage

prior_pc(U = 1, alpha = 0.01)

Arguments

U

Upper bound. P(x > U) = alpha.

alpha

Tail probability. Default 0.01.

Details

The PC prior is specified via: P(sigma > U) = alpha

This implies an exponential prior with rate = -log(alpha) / U.

Value

A tulpa_prior object

References

Simpson, D., Rue, H., Riebler, A., Martins, T. G., & Sorbye, S. H. (2017). Penalising model component complexity: A principled, practical approach to constructing priors. Statistical Science, 32(1), 1-28.

Examples

prior_pc(U = 1, alpha = 0.01)    # P(sigma > 1) = 0.01
prior_pc(U = 0.5, alpha = 0.05)  # Tighter, P(sigma > 0.5) = 0.05


Prior predictive simulation

Description

Draw datasets from the prior predictive distribution: parameters are sampled from their priors (no data conditioning) and pushed through the model's linear predictor and the family's simulator.

Useful for checking whether priors imply plausible data ranges before fitting.

Usage

prior_predict(
  formula,
  family,
  data,
  priors = NULL,
  n_draws = 100,
  seed = NULL,
  ...
)

Arguments

formula

A model formula (e.g., y ~ x + (1 | g)). For multi-process families, a list of formulas keyed by process name.

family

A tulpa_family object exposing a simulate_fn (see tulpa_family()). Model packages (tulpaRatio, tulpaObs) provide families; tests can build a minimal one with tulpa_family().

data

Data frame containing covariates and grouping factors. Used for dimensions and design matrices; the response column may be absent or NA.

priors

Prior specification (tulpa_priors()). If NULL, uses defaults.

n_draws

Number of prior parameter draws. Default 100.

seed

Optional integer seed for reproducibility.

...

Passed to family$simulate_fn.

Value

A tulpa_prior_predict object: a list with

Examples

# Toy Gaussian family for illustration
fam <- tulpa_family(
  name = "gaussian",
  simulate_fn = function(eta, params, n_obs, ...) {
    rnorm(n_obs, eta[[1]], params$sigma_y)
  },
  extra_params = list(sigma_y = prior_half_normal(1))
)
df <- data.frame(y = rep(0, 20), x = rnorm(20))
pp <- prior_predict(y ~ x, fam, df, n_draws = 50, seed = 1)
length(pp$y)  # 50


Show default priors for a tulpa family

Description

Display the default prior specifications used for each model family. Useful for understanding what priors are applied before fitting and as a starting point for customization.

Usage

priors_default(family = NULL, spatial = FALSE, temporal = FALSE)

Arguments

family

A tulpa family object (e.g. a ratio family constructor from a model package such as tulpaRatio). If NULL (default), shows defaults for all families.

spatial

Logical; if TRUE, include spatial priors. Default FALSE.

temporal

Logical; if TRUE, include temporal priors. Default FALSE.

Details

Default priors in tulpa follow these principles:

Value

Invisibly returns a tulpa_priors object with the defaults. Primarily called for its side effect of printing.

See Also

tulpa_priors() for creating custom priors

Examples

# Defaults for all families (no family argument)
priors_default()

# Family-specific defaults take a tulpa_family object. Model packages
# (e.g. tulpaRatio) register rich families; a minimal one is enough here.
fam <- tulpa_family(
  name = "poisson_gamma",
  simulate_fn = function(eta, params, n_obs, ...) rpois(n_obs, exp(eta[[1]]))
)
priors_default(fam)

# Including spatial parameters
priors_default(fam, spatial = TRUE)

# Use as a starting point for customization
my_priors <- priors_default(fam)
my_priors$beta <- prior_normal(0, 1)  # Tighter prior on fixed effects


Random-effect summaries

Description

Random-effect summaries

Usage

ranef(object, ...)

## S3 method for class 'tulpa_fit'
ranef(object, ...)

Arguments

object

A tulpa_fit object.

...

Ignored.

Details

What each backend reports for a group effect follows what it computes:

The source column says per row which of these produced it: "sampled" for a posterior draw summary, "mixture" for the node mixture, "mode" for a conditional mode. A fit whose backend never forms a per-group posterior at all (the adaptive Gauss-Hermite inner marginal integrates each group out by quadrature) errors with that reason rather than returning an empty table, which would be indistinguishable from a model with no random effects. A model that genuinely has none returns a zero-row data frame.

Value

Data frame with one row per random-effect coefficient: term (the group level, and the coefficient for a random slope), estimate, sd, the 2.5% / 97.5% bounds conf.low / conf.high, and source (which construction the row came from). sd and the bounds are NA on a backend that reports a point per group (see Details).

Examples


set.seed(1)
df <- data.frame(x = rnorm(100), g = factor(rep(1:10, 10)))
df$y <- rpois(100, exp(0.3 * df$x))
fit <- tulpa(y ~ x + (1 | g), data = df, family = "poisson")
ranef(fit)


Rational Approximation Coefficients for a Fractional SPDE

Description

Returns the coefficient descriptor for an SPDE Matern field of operator order alpha = nu + 1 (in 2D). For integer nu the construction is exact and no rational approximation is needed. For fractional nu it returns the rational-SPDE roots from the BRASIL best-rational approximation (see Details).

Usage

rational_spde_coefficients(nu, m = 4L, lambda_range = c(1e-04, 10000))

Arguments

nu

Matern smoothness parameter. A positive number; may be fractional (e.g. 0.5, 1.5, 2.5).

m

Rational approximation order (number of numerator / denominator factors) used for fractional nu. Higher m lowers the approximation error of the field's spectral density. Default 4.

lambda_range

The generalized-eigenvalue spectrum c(l_min, l_max) of the FEM operator ⁠CiL = C^{-1}(kappa^2 C + G)⁠ over which the rational approximation is fitted. The approximation acts on the normalized interval ⁠[l_min / l_max, 1]⁠. Used for fractional nu only.

Details

For integer nu (1, 2, 3, ...) the operator order alpha = nu + 1 is an integer and the precision is assembled directly from integer powers of the FEM operator ⁠L = kappa^2 C + G⁠ – an exact construction.

For fractional nu the field uses the operator-based rational SPDE approximation (Bolin & Kirchner 2020). With beta = alpha / 2 and m_beta = max(1, floor(beta)), the field-mode variance of the assembled precision ⁠Q = Pl' C^{-1} Pl⁠ (field ⁠u = Pr x⁠, x ~ N(0, Q^{-1})) tracks the Matern spectral density ⁠l^{-2 beta}⁠ when ⁠prod(1 - l rc) / (l^{m_beta - 1} prod(1 - l rb))⁠ approximates l^{-beta_rem} on the scaled spectrum, beta_rem = beta - (m_beta - 1). The roots come from the degree-⁠(m, m)⁠ best uniform (minimax) rational approximation of x^{-beta_rem}, computed by the BRASIL algorithm (Hofreither 2021); its numerator zeros map to rc = 1 / zero and denominator poles to rb = 1 / pole. The field assembly from these roots is .spde_rational_assemble(); it is validated against the Matern spectral density in test-spde-rational.R.

Value

For integer nu, a list with is_integer = TRUE, the operator order alpha, beta = alpha / 2, m = 0, and empty poles / weights. For fractional nu, a list with is_integer = FALSE, alpha, beta, the rSPDE roots rb (denominator factors, drive Pl) and rc (numerator factors, drive Pr), the integer power m_beta, the remaining fractional exponent beta_rem, the scale constant scale, the rational order m, and the approximation error.

References

Bolin, D. & Kirchner, K. (2020). The rational SPDE approach for Gaussian random fields with general smoothness. Journal of Computational and Graphical Statistics, 29(2), 274-285.

Hofreither, C. (2021). An algorithm for best rational approximation based on barycentric rational interpolation. Numerical Algorithms, 88, 365-388.


PC + LKJ hyperprior for a random-effect covariance

Description

Construct the default weakly-informative hyperprior used by tulpa_re_cov_nested() for one covariance block: independent Penalized-Complexity (PC) priors on the marginal standard deviations sigma_i together with an LKJ prior on the correlation matrix R (correlated block) or no correlation (diagonal block), returned as a log_prior_theta function in the block's integration coordinates.

Usage

re_cov_pc_lkj_prior(
  n_coefs,
  prior_sigma = c(3, 0.05),
  eta = 2,
  correlated = TRUE
)

Arguments

n_coefs

Number of coefficients c in the RE block.

prior_sigma

c(U, alpha) giving P(sigma_i > U) = alpha (default c(3, 0.05)), applied independently to every marginal SD.

eta

LKJ shape (default 2). eta = 1 is uniform on correlation matrices; larger values favour weaker correlations. Ignored for a diagonal block.

correlated

TRUE (default) for a full covariance block (log-Cholesky coordinates, LKJ prior); FALSE for a diagonal / uncorrelated block (log-SD coordinates, no correlation). For n_coefs = 1 the two coincide.

Details

For a correlated block the prior is specified on the natural scale, ⁠p(sigma, R) = LKJ(R | eta) * prod_i PC(sigma_i)⁠, then pushed to the log-Cholesky coordinates theta of ⁠Sigma = L L'⁠ by the exact change-of-variables Jacobian. For a diagonal (uncorrelated) block the LKJ factor drops and ⁠theta_i = log sigma_i⁠ with Jacobian ⁠sum_i theta_i⁠.

PC prior (Simpson et al. 2017) on each marginal SD: exponential with rate lambda = -log(alpha) / U, so P(sigma_i > U) = alpha – the prior_sigma = c(U, alpha) convention also used by the SPDE prior in tulpa.

LKJ prior (Lewandowski et al. 2009) on the correlation matrix: p(R) proportional to det(R)^(eta - 1). eta = 1 is uniform over correlation matrices; eta > 1 concentrates toward the identity. The normalizing constant is dropped (constant across the grid, so it cancels when the integration weights are renormalized).

Jacobian (correlated block): with theta packing ⁠log L_ii⁠ on the diagonal and the raw strict-lower entries of L, the change of variables from ⁠(sigma, R)⁠ to theta adds ⁠sum_i (c + 2 - i) * log L_ii - c * sum_i log sigma_i⁠ to ⁠log p(sigma, R)⁠. (Composition of the log-diagonal map, the standard Cholesky-to-covariance Jacobian ⁠2^c prod_i L_ii^(c+1-i)⁠, and the covariance-to-⁠(sigma, R)⁠ Jacobian; verified against numerical differentiation in test-re-cov-prior.R.)

Value

A ⁠function(theta)⁠ returning the scalar log prior density in the block's integration coordinates, suitable for one block of the log_prior_theta argument of tulpa_re_cov_nested().

See Also

tulpa_re_cov_nested()


Objects exported from other packages

Description

These objects are imported from other packages. Follow the links below to see their documentation.

generics

glance(), tidy()


Residuals from a tulpa fit

Description

Population-level residuals from the fixed-effect fitted mean: "response" is y - E[y | eta] on the response scale (trial-scaled for binomial, offset included); "pearson" additionally scales by the family standard deviation sqrt(Var(y | eta)) at the fitted linear predictor. Random effects are held at zero, matching fitted().

Usage

## S3 method for class 'tulpa_fit'
residuals(object, type = c("pearson", "response"), ...)

Arguments

object

A tulpa_fit object carrying ⁠$y⁠ and ⁠$model_matrix⁠.

type

"pearson" (default) or "response".

...

Ignored.

Value

Numeric vector of length nobs(object).


Resolve the R fitter function for a backend (errors if unreachable).

Description

Looks up the fitter name string in the registry and resolves it lazily, so the registry stays independent of source-file load order.

Usage

resolve_backend_fitter(backend)

Resolve a parsed RE spec to a grouping factor

Description

Three paths, mirroring resolve_group_rhs:

Usage

resolve_group_factor(re_spec, data, env)

Resolve the RHS of a bar term into one or more group specs

Description

Three cases:

Usage

resolve_group_rhs(rhs)

Arguments

rhs

A language object (RHS of | or ||).

Value

List of group specs.


Pool multiple imputation draws via Rubin's rules

Description

Pools per-imputation block fits via Rubin's rules. When every draw also carries a per-coefficient skewness vector ⁠$gamma⁠, the third cumulant is pooled by the law of total cumulants

\kappa_3 = E[\kappa_3(X | k)] + 3\,\mathrm{Cov}(\mu_k, \sigma_k^2) + \kappa_3(\mu_k),

giving a pooled skewness ⁠$gamma⁠ alongside the usual ⁠$mean⁠ / ⁠$se⁠. If any draw is missing ⁠$gamma⁠, the third-cumulant path is skipped.

Usage

rubins_pool(draws)

Arguments

draws

List of K draws. Each draw is a named list of submodel results, each with beta (numeric vector), se (numeric vector), and optionally gamma (numeric vector, same length as beta).

Value

A named list of pooled submodel summaries, each with mean, se, V_within, V_between, V_total, K (number of draws that contributed), and when applicable gamma (pooled skewness) and kappa3 (pooled third cumulant).


Simulation-based calibration

Description

Scores whether an inference algorithm's posterior is CALIBRATED, by reading the whole marginal CDF rather than one or two nominal levels. Draw a truth from the distribution the fit updates, simulate a data set at that truth, fit, and take the probability integral transform (PIT) of the truth under the reported posterior. Under exact inference those PIT values are exactly Uniform(0, 1), so the entire ECDF is the measurement (Talts et al. 2018).

Usage

sbc(object, ...)

## Default S3 method:
sbc(object, ...)

## S3 method for class 'character'
sbc(
  object = c("prior_predictive", "posterior"),
  simulator = NULL,
  fitter = NULL,
  model = NULL,
  n_sim = 100L,
  quantities = NULL,
  flat_prior = character(),
  level = 0.95,
  seed = 0L,
  n_ref = NULL,
  control = list(),
  ...
)

## S3 method for class 'sbc'
summary(object, baseline = NULL, ...)

## S3 method for class 'sbc'
plot(x, arm = NULL, quantity = NULL, folded = FALSE, ...)

Arguments

object

An sbc result.

...

Ignored.

simulator, fitter

The prior-predictive callbacks; see the contract above. Required for experiment = "prior_predictive".

model

The list of posterior-SBC callbacks. Required for experiment = "posterior".

n_sim

Number of simulations.

quantities

Optional character vector restricting which quantities are scored. Default scores every quantity every arm reports.

flat_prior

Character vector of scored quantities held FIXED under a flat prior, admitted by a structural argument the caller is asserting. Prior-predictive only.

level

Simultaneous band level.

seed

Offset added to the simulation index, so simulation s runs the callbacks at seed + s.

n_ref

Optional; when supplied it is passed to fitter (or to model$arms) as n_ref, the number of reference values a rank predictive is formed against. The callback must accept it.

control

List of knobs: progress (default FALSE) and rand_seed, the pinned stream the within-atom randomizing uniforms come from (default the driver's own, so a result is reproducible and the fits are not perturbed by asking for the diagnostic).

baseline

Optional arm name. When given, the summary also carries the PAIRED CRPS differences of every other arm against it, seed by seed. A negative delta is the arm scoring better. This is refused on an experiment where the CRPS is not a proper posterior score.

x

An sbc result.

arm, quantity

Optional character vectors selecting which panels to draw. Default draws every (arm, quantity).

folded

Plot the folded PIT instead of the raw one.

Value

An object of class sbc:

pit

one row per (simulation, arm, quantity): the truth, the raw and folded PIT, the CRPS and the predictive kind.

report

one row per (arm, quantity): the KS distance, whether the ECDF stayed inside the simultaneous band, the exact uniformity p-value, the same three folded, and the mean CRPS with its standard error.

bands

the calibrated simultaneous band, by sample size.

premises

what each guard concluded.

crps_role

the role the CRPS column has under this experiment.

The two experiments

"prior_predictive"

ordinary SBC. theta ~ p(theta), y ~ p(y | theta), fit, PIT. It reports self-consistency AVERAGED over the whole generative distribution.

"posterior"

calibration CONDITIONAL on an observed data set (Sailynoja et al. 2026, Algorithm 2). ⁠theta' ~ pi(theta | y_obs)⁠, ⁠y ~ pi(y | theta')⁠, and the PIT is taken under the AUGMENTED posterior pi(theta | y, y_obs). That is ordinary SBC with pi(theta | y_obs) in the role of the prior, so the same band, the same folded read and the same proper score all carry over – and it needs no proper prior.

A fixed-truth sweep is not an SBC experiment and is not offered here: its PIT is not uniform under correct inference and its CRPS is a descriptive loss, not a proper posterior score.

What is reported

Three reads of the same PIT sample, per (arm, quantity):

raw

the PIT ECDF against an exact SIMULTANEOUS band. A pointwise binomial band is not simultaneous – at n = 100, holding each order statistic at 95 percent holds all of them together at 0.4471 – so the band here is calibrated by bisection against the exact crossing probability of the uniform order statistics.

folded

⁠2 |u - 1/2|⁠, also uniform, and where a symmetric over- or under-dispersion shows after cancelling in the raw ECDF.

CRPS

the strictly proper score, closed form for the nested tier's own Gaussian mixture, paired seed by seed through summary(x, baseline = ).

Every discrete PIT (a rank, a grid axis, a draw set) is randomized within its atom, ⁠u = F(theta^-) + V P(theta)⁠, so one uniform reference and one band serve every quantity. Reading rank / n_ref against a continuous uniform is the classic silent SBC bug.

The callback contract

For experiment = "prior_predictive":

simulator(seed)

returns a list carrying theta, a named numeric vector of the true values of the scored quantities, plus whatever the fitter needs. It must be a pure function of its seed.

fitter(d)

returns a named list of ARMS, each a named list over quantities, each entry a predictive built by sbc_mixture() and its siblings. Several arms read off one solve per seed, so an arm-to-arm difference carries no fit-to-fit noise.

For experiment = "posterior", model is a list of six callbacks – data_obs, fit(data), draw_theta(fit, seed), simulate(theta, seed), pool(data_obs, replicate), arms(fit, data) – plus an optional group_ids(data) used to verify the fresh-groups premise below. The driver hands draw_theta and simulate DIFFERENT seeds, so set.seed(seed) at the top of each is the correct fixture; sharing one makes the replicate's noise a function of the truth, which is not ⁠p(y | theta')⁠.

Two guards

The prior-predictive experiment draws the truth from the prior, so an IMPROPER prior cannot be used – and the nested-Laplace door puts no prior on the fixed effects. A scored quantity whose truth does not move across simulations was not drawn from a proper prior, and sbc() errors on it before spending the fits, pointing at experiment = "posterior". A location parameter whose flat prior leaves the PIT uniform by a structural argument is admitted through flat_prior, which is itself checked and travels on the result.

The posterior experiment rests on two premises, each of which silently invalidates the result when broken. The augmented posterior must condition on BOTH data sets, so a pool() returning no more than the replicate (or no more than the observed data) is refused. And the replicate must be conditionally independent of the observed data given theta, which for a hierarchical model whose group effects are integrated out means FRESH groups. Supply group_ids and the observable half of that is verified – the group LABELS are disjoint – and omit it and the result records the premise as unverified rather than assumed. The other half, that the simulator drew those groups' effects from the prior rather than conditionally on the observed data, is not visible from outside the callback and is not claimed.

References

Talts, Betancourt, Simpson, Vehtari & Gelman (2018). Validating Bayesian inference algorithms with simulation-based calibration. arXiv:1804.06788.

Sailynoja, Schmitt, Buerkner & Vehtari (2026). Posterior SBC: simulation-based calibration checking conditional on data. Statistics and Computing 36:78. doi:10.1007/s11222-026-10825-9

Grimit, Gneiting, Berrocal & Johnson (2006). The continuous ranked probability score for circular variables and its application to mesoscale forecast ensemble verification. QJRMS 132(621C):2925-2942.

See Also

sbc_mixture() for the predictive shapes a fitter reports, diagnostics() for the single-fit reliability band that screens what this measures, pit_residuals() for single-fit posterior-predictive PIT residuals (a different quantity).

Examples

# A conjugate normal-normal model, whose posterior is exact, so its PIT must
# be uniform: the harness scoring itself.
sim <- function(seed) {
  set.seed(seed)
  mu <- rnorm(1)
  list(y = rnorm(10L, mu, 1), theta = c(mu = mu))
}
fitter <- function(d) {
  v <- 1 / (1 + length(d$y))
  list(exact  = list(mu = sbc_normal(v * sum(d$y), sqrt(v))),
       narrow = list(mu = sbc_normal(v * sum(d$y), sqrt(v) / 2)))
}
res <- sbc("prior_predictive", simulator = sim, fitter = fitter, n_sim = 60L)
res
summary(res, baseline = "exact")

Predictive shapes an SBC fitter reports

Description

The tagged representations sbc() reads. A fitter (or a posterior-SBC arms) callback returns a named list of ARMS, each a named list over quantities, and each entry is one of these – the shape the backend actually reports for that quantity. Everything downstream (the PIT, the CRPS, drawing from a predictive) dispatches on the kind tag, so a new backend shape is one entry in three switches rather than a parallel scorer.

Usage

sbc_mixture(mu, var, w = NULL)

sbc_normal(mean, sd)

sbc_discrete(support, probs)

sbc_rank(rank, n_ref)

sbc_draws(x)

Arguments

mu, var, w

Component means, variances and weights. w defaults to equal weights and is normalized.

mean, sd

Mean and standard deviation of a single Gaussian.

support, probs

Finite support and its probabilities, normalized.

rank, n_ref

The rank in 0:n_ref of the truth among n_ref reference values, and that reference count.

x

Posterior draws.

Details

These are the extension point, not alternative front doors: sbc() is the verb, and these are the argument type it consumes.

sbc_mixture() is what an outer hyperparameter grid defines for a fixed effect – component k is N(mu_k, var_k) with weight w_k, which is exactly the mixture a nested-Laplace fit reports. sbc_normal() is the one-component case, with its own constructor so a collapsed-moment read says what it is. sbc_discrete() is a distribution on a finite support, which is what a discrete hyperparameter grid defines for its own axis. sbc_rank() is a rank of the truth among n_ref reference values, which is what a joint log-likelihood comparison against posterior draws produces – it needs no entry in the simulator's theta, since the comparison against the truth already happened when the rank was formed. sbc_draws() is for a backend reporting no analytic marginal.

The last three have ATOMS, so their PIT is randomized within the atom by sbc(); reading a rank against a continuous uniform is the classic silent SBC bug.

Value

A list carrying a kind tag and that shape's parameters.

See Also

sbc()

Examples

sbc_normal(0.3, 0.1)
sbc_mixture(mu = c(0, 1), var = c(1, 4), w = c(0.7, 0.3))
sbc_discrete(support = c(0.5, 1, 2), probs = c(0.2, 0.5, 0.3))

Select best backend within a mode

Description

Select best backend within a mode

Usage

select_backend_for_mode(
  mode,
  family,
  n_obs,
  has_spatial,
  has_temporal,
  has_latent = FALSE,
  spatial_type = NULL
)

Select inference mode and backend

Description

Implements the mode selection logic for tulpa. Accepts either tier names (auto, exact, structured, optimized) or backend names (hmc, ess, pg, laplace, vi).

When mode is "auto", selects between Tier 1 (Exact) and Tier 2 (Structured) based on model characteristics. Never selects Tier 3 (Optimized) automatically.

Usage

select_inference_mode(
  mode,
  family,
  n_obs,
  has_spatial = FALSE,
  has_temporal = FALSE,
  has_latent = FALSE,
  spatial_type = NULL,
  temporal = NULL,
  has_re = FALSE
)

Arguments

mode

User-specified mode or backend name

family

Model family object

n_obs

Number of observations

has_spatial

Whether model has spatial effects

has_temporal

Whether model has temporal effects

has_latent

Whether model has latent factors

has_re

Whether the model has any random-effect term ((1 | g) or (1 + x | g)), scalar or slope alike

Value

List with:


Select the "main" model parameters for diagnostic display

Description

Drops per-element latent-field entries (names ending in a bracketed index, e.g. u[12], w[3]) so plots and summaries focus on scalar coefficients and hyperparameters rather than thousands of latent values.

Usage

select_main_params(param_names)

Arguments

param_names

Character vector of parameter names.

Value

The subset of param_names that are not bracketed-index entries (or the full vector if that would be empty).


Generate random effects for simulation

Description

Generate random effects for simulation

Usage

sim_random_effects(n_groups, sigma = 0.5, type = "iid")

Arguments

n_groups

Number of groups

sigma

Standard deviation

type

Type of random effects: "iid" (default), "car", "ar1"

Value

Numeric vector of random effects


Generate spatial random effects for simulation

Description

Generate spatial random effects for simulation

Usage

sim_spatial_effects(adjacency, sigma = 0.5, type = "icar")

Arguments

adjacency

Adjacency matrix

sigma

Standard deviation

type

Spatial type: "icar" (default), "bym2"

Value

Numeric vector of spatial effects


Generate temporal random effects for simulation

Description

Generate temporal random effects for simulation

Usage

sim_temporal_effects(n_times, sigma = 0.5, type = "rw1", rho = 0.7)

Arguments

n_times

Number of time points

sigma

Standard deviation

type

Temporal type: "rw1", "rw2", "ar1"

rho

Autocorrelation for AR(1). Default 0.7.

Value

Numeric vector of temporal effects


Simulate responses from a fitted tulpa model

Description

Base-R alias for posterior_predict(): each simulation is one posterior predictive replicate at the training data.

Usage

## S3 method for class 'tulpa_fit'
simulate(object, nsim = 1, seed = NULL, ...)

Arguments

object

A tulpa_fit object.

nsim

Number of simulated datasets (default 1).

seed

Optional integer seed (RNG state is restored on exit).

...

Ignored.

Value

A data frame with nsim columns (sim_1, ...), one row per observation, following the stats::simulate() convention.


Extract fitted covariate smooths

Description

The grid-marginalized posterior mean of each s(x) term's latent values, one estimate per node (bin midpoint or unique covariate value). The latent block values are read from the fit's per-grid modes, weighted by the hyperparameter grid weights.

Usage

smooth_effects(object, term = 1L)

Arguments

object

A tulpa_fit from tulpa() with s(...) term(s) in the formula.

term

Which smoother: index or covariate name. Default 1.

Value

A data frame with columns x (node location) and estimate (the posterior-mean smooth at that node), with the covariate name as an attribute "var".

Examples


set.seed(1)
d <- data.frame(x = runif(300, -2, 2))
d$y <- rpois(300, exp(0.3 + sin(2 * d$x)))
fit <- tulpa(y ~ s(x), data = d, family = "poisson")
sm <- smooth_effects(fit)
plot(sm$x, sm$estimate, type = "l")


Skew-normal CDF

Description

Cumulative distribution function of the univariate skew-normal,

F(q; \xi, \omega, \alpha) = \Phi(z) - 2 T(z, \alpha),

where z = (q - \xi) / \omega and T is Owen's T function. Owen's T is evaluated by base R numerical quadrature; no extra dependencies are required.

Usage

sn_cdf(q, sn)

Arguments

q

Numeric vector of quantiles.

sn

Skew-normal parameter list from sn_match() with elements xi, omega, alpha.

Value

Numeric vector of CDF values, same length as q.


Match three cumulants to a skew-normal parameterisation

Description

Inverts the skew-normal moment formulas to convert (mu, sigma, gamma) into (xi, omega, alpha) parameters. Returns NULL with a warning when |gamma| exceeds the skew-normal ceiling (~0.9953) – in that regime the third cumulant cannot be matched by any skew-normal and the caller should fall back to direct-quadrature quantiles.

Usage

sn_match(mu, sigma, gamma)

Arguments

mu

Posterior mean (numeric, length 1).

sigma

Posterior standard deviation (positive numeric, length 1).

gamma

Posterior skewness (numeric, length 1).

Value

Named list with elements xi, omega, alpha; or NULL if |gamma| >= .SN_GAMMA_MAX.

References

Azzalini, A. (1985). A class of distributions which includes the normal ones. Scand. J. Statist. 12: 171-178.


Skew-normal quantile

Description

Inverse of the skew-normal CDF via Newton iteration on sn_cdf() with a Brent-style bracket fallback when Newton fails to converge.

Usage

sn_quantile(p, sn, tol = 1e-10, max_iter = 60L)

Arguments

p

Numeric vector of probabilities in [0, 1].

sn

Skew-normal parameter list from sn_match().

tol

Absolute tolerance on the CDF residual (default 1e-10).

max_iter

Maximum Newton iterations (default 60).

Value

Numeric vector of quantiles, same length as p.


Areal spatially varying coefficient field

Description

Declare one or more areal (CAR / Besag) fields over a graph from an lme4-style bar formula. The bar's left-hand side lists the coefficients that vary smoothly over the graph; the right-hand side names the graph-node index. Each coefficient becomes an independent CAR field, entering the linear predictor scaled by that coefficient's per-observation design value:

\eta_i = \ldots + \sum_c X_{ic}\, z^{(c)}_{g_i},

where g_i is the graph node of observation i, z^{(c)} is the CAR field for design column c, and X_{ic} is that column's value at observation i. The intercept column is all ones, so ~ 1 || cell is the ordinary spatial intercept field; a covariate column (e.g. time) gives a spatially varying slope on that covariate (a per-region trend).

Use it inline in a tulpa() model formula, the same way a random-effect bar is written:

y ~ time + spatial(graph = adj, formula = ~ 1 + time || cell) + (1 | site)

Usage

spatial(graph, formula, proper = FALSE, shared = NULL, by = NULL)

Arguments

graph

Symmetric adjacency matrix of the spatial graph (⁠[n_node x n_node]⁠, dense or sparse). One CAR field is defined over its nodes per coefficient.

formula

One-sided formula carrying a grouping bar, e.g. ~ 1 + time || cell. The left-hand side is expanded with stats::model.matrix() into one CAR field per column (1 = intercept field; a covariate = spatially varying slope on it; ⁠0 +⁠ drops the intercept). The right-hand side must be a single bare column naming the graph node. The double bar || builds independent fields (each its own precision); a single bar | builds correlated fields – a separable multivariate CAR where the per-cell coefficient vector shares a cross-covariance Sigma (covariance ⁠Sigma (x) Q^-1⁠), so the intercept-slope correlation rho is shared across the graph.

proper

Logical; FALSE (default) builds intrinsic CAR (ICAR / Besag) fields with the sum-to-zero constraint (rho fixed at 1). TRUE builds proper CAR fields, each with its own precision ⁠Q = D - rho_car W⁠ and the spatial autocorrelation rho_car estimated from the data (one ⁠(sigma, rho_car)⁠ pair per field). summary() and print() report the per-field rho_car. Independent (||) only; correlated proper CAR (a single | with proper = TRUE) is a separate model.

shared

Optional shared-effect handle, passed through to the field blocks (see the model docs). Default NULL (shared).

by

Optional replicated-CAR factor: a bare column name (or a string) naming a factor in the model data. With L levels it builds one independent copy of the whole field per level – the field over the block-diagonal Kronecker graph ⁠I_L (x) Q⁠ (L disjoint copies of the graph) – with the hyperparameters shared across levels (one Sigma for |; one (sigma[, rho_car]) for ||). This is INLA's ⁠replicate =⁠ / mgcv's s(cell, by = ...) generalised to the varying-coefficient bar, and is orthogonal to the bar character: | / || sets the covariance among the coefficient columns within a field, while by sets how many independent replicates of the whole field exist. Default NULL (one field). Correlated proper CAR (| with proper = TRUE) stays out of scope with or without by.

Details

Each field is independent (its own precision), matching INLA's two separate f(cell, model = "besag") and f(cell.slope, time, model = "besag") fields. Nesting (a / b), interaction, or expression grouping is rejected: the grouping must be a single graph node. Add ordinary nested random effects, e.g. (1 | site), as separate terms.

Value

A tulpa_spatial_field object describing the field(s). It is expanded into one CAR block per design column at fit time, when the data is available.

See Also

spatial_car() for a single areal field passed via the ⁠spatial =⁠ argument, spatial_svc() for the coordinate-based (Gaussian-process) spatially varying coefficient.

Examples

# Chain graph over 10 cells
adj <- matrix(0, 10, 10)
for (i in 1:9) adj[i, i + 1] <- adj[i + 1, i] <- 1

# Spatial intercept plus a spatially varying time slope
f <- spatial(graph = adj, formula = ~ 1 + time || cell)
print(f)


BYM2 spatial structure

Description

Specify a Besag-York-Mollie 2 (BYM2) spatial random effect. BYM2 decomposes the spatial effect into a structured (ICAR) component and an unstructured (IID) component, with a mixing parameter controlling the proportion of variance attributable to spatial structure.

BYM2 is preferred over plain CAR when you want to:

Usage

spatial_bym2(
  adjacency,
  level = c("group", "obs"),
  group_var = NULL,
  shared = NULL,
  scale_factor = NULL,
  parameterization = c("standard", "collapsed")
)

Arguments

adjacency

Symmetric adjacency matrix (⁠[n_units x n_units]⁠).

level

Either "group" (one effect per level of group_var) or "obs" (one effect per row of the data; nrow(data) must equal nrow(adjacency)).

group_var

Name of the grouping variable in the data; required when level = "group".

shared

Optional shared-effect handle (see model docs).

scale_factor

Scaling factor for the ICAR component. If NULL (default), computed from the adjacency matrix following Riebler et al.

parameterization

"standard" (default) or "collapsed" (deprecated).

Value

A tulpa_spatial object

References

Riebler, A., Sorbye, S. H., Simpson, D., & Rue, H. (2016). An intuitive Bayesian spatial model for disease mapping that accounts for scaling. Statistical Methods in Medical Research, 25(4), 1145-1165.

Examples

# Create adjacency matrix for 10 regions (chain structure)
adj <- matrix(0, 10, 10)
for (i in 1:9) {
  adj[i, i+1] <- adj[i+1, i] <- 1
}

# Create BYM2 spatial structure
bym2 <- spatial_bym2(adj, level = "group", group_var = "region")
print(bym2)


# Disease mapping with BYM2 spatial smoothing
set.seed(456)
n_regions <- 10
epi_data <- data.frame(
  region = factor(rep(1:n_regions, each = 4)),
  age = rnorm(n_regions * 4, 50, 10)
)
epi_data$cases <- rbinom(nrow(epi_data), size = 100, prob = 0.15)

fit <- tulpa(
  cases ~ age + spatial(region),
  spatial = spatial_bym2(adj, level = "group", group_var = "region"),
  data = epi_data,
  family = "binomial",
  n_trials = rep(100L, nrow(epi_data)),
  mode = "laplace"
)
summary(fit)



CAR / ICAR spatial structure

Description

Constructs a conditional autoregressive spatial random effect from an adjacency matrix. With proper = FALSE (the default) this is the improper CAR / ICAR (type = "car"); with proper = TRUE it returns the same object as spatial_car_proper().

Usage

spatial_car(
  adjacency,
  level = c("group", "obs"),
  group_var = NULL,
  proper = FALSE,
  shared = NULL,
  parameterization = c("standard", "collapsed")
)

Arguments

adjacency

Symmetric adjacency matrix (⁠[n_units x n_units]⁠).

level

Either "group" (one effect per level of group_var) or "obs" (one effect per row of the data; nrow(data) must equal nrow(adjacency)).

group_var

Name of the grouping variable in the data; required when level = "group".

proper

If TRUE, use proper CAR (type = "car_proper"); else ICAR (type = "car").

shared

Optional shared-effect handle (see model docs).

parameterization

"standard" (default) or "collapsed" (deprecated).

Value

A tulpa_spatial object with type = "car" (or "car_proper" when proper = TRUE).

See Also

spatial_car_proper(), spatial_bym2().


Proper CAR spatial structure

Description

Convenience wrapper for spatial_car(..., proper = TRUE). Creates a proper conditional autoregressive (CAR) spatial random effect with the autocorrelation parameter rho estimated from the data.

Use this when you want spatial autocorrelation to be a parameter of the model rather than fixed at 1 (as in ICAR). rho ~= 0 collapses to IID, rho ~= 1 approaches ICAR.

Usage

spatial_car_proper(
  adjacency,
  level = c("group", "obs"),
  group_var = NULL,
  shared = NULL
)

Arguments

adjacency

Symmetric adjacency matrix (⁠[n_units x n_units]⁠).

level

Either "group" (one effect per level of group_var) or "obs" (one effect per row of the data; nrow(data) must equal nrow(adjacency)).

group_var

Name of the grouping variable in the data; required when level = "group".

shared

Optional shared-effect handle (see model docs).

Value

A tulpa_spatial object with type = "car_proper".

See Also

spatial_car() for ICAR (rho fixed at 1), spatial_bym2() for the BYM2 decomposition.

Examples

adj <- matrix(0, 10, 10)
for (i in 1:9) adj[i, i+1] <- adj[i+1, i] <- 1
spec <- spatial_car_proper(adj, level = "group", group_var = "site")
print(spec)


Gaussian process spatial structure (NNGP)

Description

Specify a Gaussian-process spatial random effect, approximated with a nearest-neighbour GP (NNGP) for scalability. Captures smooth spatial variation from point-referenced coordinates.

Usage

spatial_gp(
  coords,
  approx = c("nngp", "hsgp"),
  cov = c("exponential", "matern"),
  nu = 1.5,
  nn = 15,
  m = 6,
  c = 1.5,
  sigma_prior_U = 1,
  sigma_prior_alpha = 0.01,
  shared = NULL,
  scale_coords = TRUE,
  parameterization = c("noncentered", "centered", "collapsed")
)

Arguments

coords

A formula (~ lon + lat) or character vector naming the coordinate variables in the data. With approx = "nngp" the coordinate DIMENSION is however many are named: two for a map, one for a transect or a depth profile, three for a depth-resolved domain. The neighbour graph and the neighbour covariance both read every column. approx = "hsgp" takes exactly two, and so does any sampler mode, because both store coordinates at a fixed 2-D stride.

approx

GP approximation: "nngp" (default, a nearest-neighbour GP with the cov / nu / nn arguments) or "hsgp" (a Hilbert-space basis GP with m functions per dimension and boundary factor c).

cov

Covariance function (NNGP only). One of "exponential" or "matern".

nu

Matern smoothness parameter, one of 1.5 or 2.5. Used only when cov = "matern" (nu = 0.5 is cov = "exponential").

nn

Number of nearest neighbours used in the NNGP approximation.

m

Number of HSGP basis functions per dimension (approx = "hsgp").

c

HSGP boundary factor, ⁠>= 1⁠ (approx = "hsgp").

sigma_prior_U, sigma_prior_alpha

Penalized-complexity prior on the field's marginal standard deviation (approx = "hsgp"), calibrated so that P(sigma > sigma_prior_U) = sigma_prior_alpha. Defaults to P(sigma > 1) = 0.01. sigma_prior_U must be positive and sigma_prior_alpha must lie in ⁠(0, 1)⁠.

shared

Whether the spatial effect is shared across processes in a multi-process model. NULL (default) shares the effect; FALSE fits process-specific effects and emits a warning.

scale_coords

Logical. Standardize coordinates before fitting (default TRUE).

parameterization

Latent parameterization for the exact-NUTS field. One of "noncentered" (default; samples z ~ N(0, I) and reconstructs the field as w = f(z, sigma2, phi), avoiding the field/hyperparameter funnel), "centered" (places the NNGP density on the field directly), or "collapsed" (deprecated).

Value

A tulpa_gp object (also of class tulpa_spatial).

See Also

spatial_car(), spatial_bym2() for areal spatial effects.

Examples

# GP spatial specification from coordinate columns
spatial_gp(~ lon + lat)
spatial_gp(~ lon + lat, cov = "matern", nu = 1.5)


Multi-Scale Gaussian Process spatial structure

Description

Specify a multi-scale spatial random effect that decomposes spatial variation into local (fine-scale) and regional (broad-scale) components. Each scale has its own range and variance parameters.

This is particularly useful for large datasets (>100k observations) where spatial patterns exist at multiple scales.

Usage

spatial_multiscale(
  coords,
  scales = c("local", "regional"),
  approx = c("nngp", "hsgp"),
  m = 6L,
  c_boundary = 1.5,
  range_local = c(0.01, 1),
  range_regional = c(1, 10),
  cov = c("exponential", "matern"),
  nu = 1.5,
  nn_local = 10,
  nn_regional = 30,
  shared = NULL,
  scale_coords = TRUE,
  sampler = c("auto", "noncentered", "centered", "interweaved")
)

Arguments

coords

A one-sided formula specifying coordinate columns (e.g., ~ lon + lat), or a character vector of length 2 with column names.

scales

Character vector specifying scale names. Default: c("local", "regional").

approx

Approximation method: "nngp" (default) for Nearest Neighbor GP; "hsgp" for Hilbert Space GP (faster for smooth fields).

m

Number of HSGP basis functions per dimension (default 6). Only used when approx = "hsgp". Total basis functions will be m^2.

c_boundary

Boundary factor for HSGP domain extension (default 1.5). Only used when approx = "hsgp".

range_local

Plausible range interval for the local scale as c(lower, upper) in coordinate units. Default: c(0.01, 1) (after scaling). Under exact NUTS this is not a hard box: lower anchors a PC prior on that scale's range (P(range < lower) = 0.05, the same prior spatial_gp() uses), and the pair places the sampler's starting range at their geometric mean. The range itself is free on ⁠(0, Inf)⁠.

range_regional

Plausible range interval for the regional scale, read the same way. Default: c(1, 10) (after scaling). Keeping the two intervals separated is what identifies the scales against each other.

cov

Covariance function: "exponential" (default) or "matern".

nu

Smoothness parameter for Matern covariance, one of 1.5 or 2.5.

nn_local

Number of nearest neighbors for local scale. Default 10.

nn_regional

Number of nearest neighbors for regional scale. Default 30.

shared

Logical; if TRUE (default), spatial effects enter both all processes.

scale_coords

Logical; if TRUE (default), coordinates are scaled to unit variance before computing distances.

sampler

Latent parameterization for the exact-NUTS field. "auto" (default) and "noncentered" sample z ~ N(0, I) per scale and reconstruct each field as w = f(z, sigma2, phi), avoiding the field/hyperparameter funnel; "centered" places the NNGP density on each field directly. "interweaved" alternates between parameterizations and is not implemented on the exact-NUTS path.

Details

The multi-scale model decomposes spatial variation additively:

\eta(s) = X\beta + w_{local}(s) + w_{regional}(s)

where each component follows an independent Gaussian process:

w_{local}(s) \sim GP(0, \sigma^2_{local} C(\phi_{local}))

w_{regional}(s) \sim GP(0, \sigma^2_{regional} C(\phi_{regional}))

Identifiability: With sufficient data (>500 locations), the two scales are typically well-identified when prior ranges are non-overlapping. PC priors on variance components help prevent overfitting.

Computational cost: Approximately 1.5-2x the cost of single-scale GP, as two NNGP likelihoods must be evaluated.

Value

A tulpa_multiscale object

See Also

spatial_gp() for single-scale GP, temporal_multiscale() for multi-scale temporal effects

Examples

# Create multi-scale spatial structure
ms <- spatial_multiscale(
  ~ lon + lat,
  range_local = c(0.1, 0.5),
  range_regional = c(1, 5)
)
print(ms)


set.seed(101)
n <- 25
df <- data.frame(
  lon = runif(n, 0, 10),
  lat = runif(n, 0, 10),
  depth = rnorm(n),
  temp = rnorm(n)
)
df$count <- rpois(n, exp(1 + 0.2 * df$depth))

# Both scales are sampled by exact NUTS (mode = "exact"); the field is
# returned as gp_local[i] / gp_regional[i] draws.
fit <- tulpa(
  count ~ depth + temp,
  data = df,
  family = "poisson",
  spatial = spatial_multiscale(
    ~ lon + lat,
    range_local = c(0.1, 0.5),
    range_regional = c(1, 5),
    nn_local = 5L,
    nn_regional = 8L
  ),
  mode = "exact",
  control = list(n_iter = 60L, n_warmup = 30L, seed = 1L)
)
summary(fit)



Extract spatial range and variance from a fitted spatial model

Description

Summarises the posterior of spatial hyperparameters. For sampler-tier fits this reads the raw hyperparameter draws; for a nested-Laplace spatial fit it summarises the outer hyperparameter grid. Works with ICAR, BYM2, GP (NNGP), CAR, SPDE, and SVC spatial types.

Usage

spatial_range(object, probs = c(0.025, 0.975))

Arguments

object

A tulpa_fit object fitted with a spatial component.

probs

Quantile probabilities for the summary (default 0.025, 0.975).

Value

A data.frame with rows for each spatial hyperparameter and columns mean, sd, and one quantile column per entry of probs (named from the probability, e.g. q2.5, q97.5 for the defaults).


Restricted Spatial Regression (RSR)

Description

Apply Restricted Spatial Regression to mitigate spatial confounding. RSR orthogonalizes the spatial effect to the covariate space, preventing the spatial random effect from absorbing covariate information.

This is important when covariates are spatially smooth (e.g., climate variables, elevation) because the spatial random effect can "steal" variance from these covariates, leading to biased coefficient estimates.

Usage

spatial_rsr(spatial, restrict_to)

Arguments

spatial

A spatial specification (spatial_gp, spatial_car, etc.)

restrict_to

Formula specifying which covariates to orthogonalize against (e.g., ~ depth + temp). The spatial effect will be constrained to be orthogonal to the column space of these covariates.

Details

The RSR approach (Reich et al., 2006; Hodges & Reich, 2010) modifies the spatial random effect to be orthogonal to the fixed effect design matrix:

w_{RSR} = (I - P_X) w

where P_X = X(X'X)^{-1}X' is the projection matrix onto the column space of X.

When to use RSR:

When NOT to use RSR:

Value

A modified spatial specification with RSR enabled

References

Reich, B. J., Hodges, J. S., & Zadnik, V. (2006). Effects of residual smoothing on the posterior of the fixed effects in disease-mapping models. Biometrics, 62(4), 1197-1206.

Hodges, J. S., & Reich, B. J. (2010). Adding spatially-correlated errors can mess up the fixed effect you love. The American Statistician, 64(4), 325-334.

See Also

spatial_gp(), spatial_car()

Examples

# Create RSR spatial structure
rsr <- spatial_rsr(
  spatial_gp(~ lon + lat),
  restrict_to = ~ depth + temp
)
print(rsr)


# Areal binomial data on a chain of regions, covariate spatially confounded
set.seed(404)
n_regions <- 12
W <- matrix(0, n_regions, n_regions)
for (i in 1:(n_regions - 1)) W[i, i + 1] <- W[i + 1, i] <- 1
df <- data.frame(region = factor(rep(1:n_regions, each = 5)))
df$x <- as.integer(df$region) / 4 + rnorm(nrow(df), 0, 0.5)
df$y <- rbinom(nrow(df), 20, plogis(-0.5 + 0.6 * df$x))

# RSR orthogonalises the spatial field to x, protecting its coefficient
fit <- tulpa(
  y ~ x + spatial(region),
  data = df,
  family = "binomial",
  n_trials = rep(20L, nrow(df)),
  spatial = spatial_rsr(spatial_car(W, level = "obs"), restrict_to = ~ x),
  mode = "auto",
  control = list(n_iter = 500L, warmup = 250L)
)
summary(fit)



SPDE Spatial Field (Matern via Triangular Mesh)

Description

Specify a continuous Matern spatial field using the SPDE approach (Lindgren, Rue & Lindstrom 2011). Builds a triangular mesh from observation coordinates, computes FEM matrices, and passes them to tulpa's SPDE Laplace engine with CHOLMOD sparse solver.

Usage

spatial_spde(
  coords,
  data = NULL,
  mesh = NULL,
  boundary = NULL,
  max_edge = NULL,
  cutoff = 0,
  nu = 1,
  prior_range = c(0.5, 0.5),
  prior_sigma = c(1, 0.5)
)

Arguments

coords

A formula ~ x + y or a two-column matrix of coordinates.

data

Optional data.frame for formula evaluation.

mesh

A pre-built tulpa_mesh object. If NULL (default), a mesh is built automatically from coords.

boundary

Optional boundary: a two-column matrix of polygon vertices, an sf polygon, or NULL for convex hull with extension.

max_edge

Maximum edge length for mesh refinement. A single value or c(inner, outer).

cutoff

Minimum distance between mesh vertices. Default 0.

nu

Matern smoothness parameter. A positive number. Integer nu (1, 2, 3, ...) gives an exact FEM construction (operator order alpha = nu + 1). Fractional nu (e.g. 0.5, 1.5) uses the operator-based rational SPDE approximation with BRASIL best-rational coefficients (Bolin & Kirchner 2020; Hofreither 2021); supported by the Laplace fitter fit_spde() (single-point and nested over range/sigma). NUTS and analytic marginal SEs remain integer-only. Default 1.

prior_range

Prior for the spatial range. A numeric vector c(U, alpha) where P(range < U) = alpha. Default c(0.5, 0.5).

prior_sigma

Prior for the marginal standard deviation. A numeric vector c(U, alpha) where P(sigma > U) = alpha. Default c(1, 0.5).

Value

A tulpa_spatial object with type "spde".

Examples

set.seed(42)
coords <- cbind(runif(50), runif(50))
spec <- spatial_spde(coords)
print(spec)

SPDE Spatial Field from Custom Matrices

Description

Specify a continuous Matern spatial field using externally-provided FEM matrices. Use this with meshes from fmesher, rSPDE, or any other source.

Usage

spatial_spde_custom(
  C,
  G,
  A,
  nu = 1,
  prior_range = c(0.5, 0.5),
  prior_sigma = c(1, 0.5)
)

Arguments

C

Mass matrix (n_mesh x n_mesh sparse matrix, e.g. from fmesher::fm_fem()$c0).

G

Stiffness matrix (n_mesh x n_mesh sparse matrix, e.g. from fmesher::fm_fem()$g1).

A

Projection matrix (n_obs x n_mesh sparse matrix, e.g. from fmesher::fm_basis()).

nu

Matern smoothness parameter. A positive number; integer values give the exact FEM construction, fractional values the BRASIL rational SPDE approximation (supported by fit_spde();). Default 1.

prior_range

Prior for the spatial range. Default c(0.5, 0.5).

prior_sigma

Prior for the marginal standard deviation. Default c(1, 0.5).

Value

A tulpa_spatial object with type "spde".


Spatially varying coefficient structure

Description

Specify a spatially varying coefficient (SVC): one or more fixed-effect coefficients are allowed to vary smoothly over space, with the variation governed by a Gaussian process (NNGP or HSGP approximation).

Usage

spatial_svc(
  coords,
  terms = 1,
  cov = c("exponential", "matern"),
  nn = 15,
  shared = NULL,
  scale_coords = TRUE,
  approx = c("nngp", "hsgp"),
  m = 6,
  c_boundary = 1.5,
  parameterization = c("noncentered", "centered")
)

Arguments

coords

A formula (~ lon + lat) or character vector of length 2 naming the two coordinate variables in the data.

terms

Which coefficients vary over space. A formula, an integer vector of design-matrix column indices, or a character vector of term names. Default 1 (the intercept).

cov

Covariance function. One of "exponential" or "matern".

nn

Number of nearest neighbours used in the NNGP approximation (approx = "nngp").

shared

Whether the effect is shared across processes in a multi-process model. NULL (default) shares it; FALSE fits process-specific effects and emits a warning.

scale_coords

Logical. Standardize coordinates before fitting (default TRUE).

approx

Spatial approximation. "nngp" (nearest-neighbour GP) or "hsgp" (Hilbert-space GP).

m

Number of basis functions per dimension for the HSGP approximation (approx = "hsgp").

c_boundary

Boundary-extension factor for the HSGP domain (approx = "hsgp").

parameterization

Latent parameterization for the exact-NUTS field (approx = "nngp" only; HSGP is already non-centered by construction). "noncentered" (default) samples z_j ~ N(0, I) per term and reconstructs each field as w_j = f(z_j, sigma2_j, phi_j), removing the field/hyperparameter funnel that otherwise attenuates the field's amplitude when it is weakly identified. "centered" places the NNGP density on each term's field directly; it is marginally cheaper on a well-identified response, but on a weakly identified one it recovers only about a third of the field's spread.

Value

A tulpa_svc object (also of class tulpa_spatial).

See Also

spatial_gp() for a spatial random effect (rather than a varying coefficient).

Examples

# Intercept that varies smoothly over space
spatial_svc(~ lon + lat)


Spatiotemporal interaction specifications for tulpa

Description

Functions to specify spatiotemporal interaction effects for tulpa models. These capture dependencies that arise when spatial patterns vary over time, or when temporal trends differ across space.

Specify a spatiotemporal interaction effect for tulpa models. The interaction captures structured or unstructured deviation from the additive spatial + temporal model.

No tulpa backend fits an interaction term, so this constructor errors. The additive space-time model is fitted by tulpa(spatial = , temporal = ) and by fit_st_nested().

Usage

spatiotemporal(
  spatial,
  temporal,
  type = c("I", "II", "III", "IV", "iid", "separable"),
  shared = NULL
)

Arguments

spatial

A spatial specification from spatial_car(), spatial_bym2(), or spatial_gp().

temporal

A temporal specification from temporal_rw1(), temporal_rw2(), temporal_ar1(), or temporal_gp().

type

Interaction type:

  • "I" or "iid": Unstructured interaction (IID)

  • "II": Structured time at each location

  • "III": Structured space at each time point

  • "IV": Fully structured (Kronecker product of spatial and temporal)

  • "separable": Separable covariance (Kronecker product)

shared

Logical; if TRUE (default), spatiotemporal effect enters both all processes. Set to FALSE for process-specific effects (triggers warning about potential confounding).

Details

Spatiotemporal interactions extend the basic additive model:

\eta_{st} = X\beta + f_s(space) + f_t(time)

to include interactions:

\eta_{st} = X\beta + f_s(space) + f_t(time) + \delta_{st}

where \delta_{st} captures space-time interactions.

Interaction Types (following Knorr-Held, 2000):

Separable Models:

Type I (IID)

Independent random effect for each space-time combination:

\delta_{st} \stackrel{iid}{\sim} N(0, \sigma^2_\delta)

This is the simplest form, requiring S*T parameters but capturing no structured interaction.

Type II (Temporal structure per location)

Each location has its own temporal random effect:

\delta_{\cdot t}^{(s)} \sim RW(\sigma^2)

This captures location-specific temporal trends but assumes independence across locations.

Type III (Spatial structure per time point)

Each time point has its own spatial random effect:

\delta_{s \cdot}^{(t)} \sim ICAR(\tau)

This captures time-specific spatial patterns but assumes independence across time points.

Type IV (Full structure)

Kronecker product of spatial and temporal precision matrices:

Q_\delta = Q_s \otimes Q_t

This is the most constrained model, assuming the interaction has the same structure as the marginal effects.

Separable

For GP-based effects, assumes separable covariance:

C(\mathbf{s}_1, t_1; \mathbf{s}_2, t_2) = C_s(\mathbf{s}_1, \mathbf{s}_2) \cdot C_t(t_1, t_2)

Value

Nothing: the call always signals an error.

References

Knorr-Held, L. (2000). Bayesian modelling of inseparable space-time variation in disease risk. Statistics in Medicine, 19(17-18), 2555-2567.

See Also

spatial_car(), spatial_gp(), temporal_rw1(), temporal_ar1()


Extract spatiotemporal effects from fitted model

Description

Extract posterior distributions of spatiotemporal interaction effects from a fitted tulpa model.

Usage

spatiotemporal_effects(
  object,
  format = c("array", "long", "summary"),
  probs = c(0.025, 0.5, 0.975),
  ...
)

## S3 method for class 'tulpa_fit'
spatiotemporal_effects(
  object,
  format = c("array", "long", "summary"),
  probs = c(0.025, 0.5, 0.975),
  ...
)

Arguments

object

A tulpa_fit object fitted with spatiotemporal argument

format

Output format: "array" (default, S x T x draws), "long" (data frame with s, t, draw, value columns), or "summary" (posterior summaries).

probs

Quantiles to compute if format = "summary".

...

Ignored

Value

Spatiotemporal effects in requested format

Examples

## Not run: 
# `fit` carries a Knorr-Held interaction block, which no tulpa backend
# fits: the spec comes from a model package that configures the
# interaction itself (see spatiotemporal() for what the engine does fit).
st_effects <- spatiotemporal_effects(fit, format = "summary")
head(st_effects)

## End(Not run)


Non-separable spatiotemporal GP

Description

Specify a non-separable Gaussian Process for spatiotemporal effects. Unlike separable models where the covariance factors as C_s \otimes C_t, non-separable models allow for direct space-time interaction in the covariance.

No tulpa backend fits a joint space-time covariance, so this constructor errors. A spatial GP alongside a temporal field is fitted by tulpa(spatial = spatial_gp(...), temporal = ...).

Usage

spatiotemporal_gp(
  coords,
  time_var,
  cov_space = c("exponential", "matern", "gaussian", "spherical"),
  cov_time = c("exponential", "matern", "gaussian"),
  nonsep_type = c("product", "sum", "gneiting", "cressie_huang"),
  nn = 15,
  shared = NULL
)

Arguments

coords

A one-sided formula specifying coordinate columns (e.g., ~ lon + lat), or a character vector of length 2.

time_var

Name of the time variable in data.

cov_space

Spatial covariance: "exponential" (default), "matern", "gaussian", or "spherical".

cov_time

Temporal covariance: "exponential" (default), "matern", or "gaussian".

nonsep_type

Non-separability type:

  • "product": C_{st} = C_s \cdot C_t (separable, for reference)

  • "sum": C_{st} = C_s + C_t

  • "gneiting": Gneiting (2002) non-separable class

  • "cressie_huang": Cressie-Huang (1999) non-separable class

nn

Number of nearest neighbors for NNGP approximation. Default 15.

shared

Logical; if TRUE (default), effect enters both processes.

Details

The non-separable covariance functions allow for more flexible space-time dependence:

Gneiting class:

C(h, u) = \frac{\sigma^2}{(a|u|^{2\alpha} + 1)^{\tau}} \exp\left(-\frac{c\|h\|^{2\gamma}}{(a|u|^{2\alpha} + 1)^{\beta\gamma}}\right)

where h is spatial lag, u is temporal lag, and parameters control the space-time interaction.

Cressie-Huang class: Constructed via Fourier transform methods to ensure positive definiteness.

Value

Nothing: the call always signals an error.

References

Gneiting, T. (2002). Nonseparable, stationary covariance functions for space-time data. Journal of the American Statistical Association, 97(458), 590-600.

Cressie, N., & Huang, H. C. (1999). Classes of nonseparable, spatio-temporal stationary covariance functions. Journal of the American Statistical Association, 94(448), 1330-1340.


Spectrum at frequency zero (AR fit)

Description

Spectrum at frequency zero (AR fit)

Usage

spectrum0_ar(x)

Posterior summary of the fixed effects

Description

Posterior summary of the fixed effects

Usage

## S3 method for class 'tulpa_fit'
summary(object, level = 0.95, ...)

Arguments

object

A tulpa_fit object.

level

Credible-interval level (default 0.95).

...

Ignored.

Value

Data frame: estimate, std.error, and lower/upper credible bounds, one row per fixed effect. Sampler tiers report empirical quantiles; the Laplace tier reports the Gaussian approximation.

On a nested-Laplace fit the estimate and standard error are the hyperparameter-grid-marginalized moments, and the bounds invert the Gaussian mixture ⁠sum_k w_k N(mu_kj, V_kjj)⁠ that grid defines, rather than reading ⁠mu +/- z sigma⁠ off the single Gaussian matching those moments. An interval_source attribute records which read produced them ("mixture_cdf", "gaussian_moment", "skew_map_cell", or "skew_map_cell/mixture_cdf" when the two are in play on different coefficients) and interval_declined says why, whenever the mixture read did not run. A retained_mass attribute gives the share of the grid weight whose cells retained a fixed-effect block: 1 on a complete grid, and below 1 on one that dropped a positive-weight cell, whose report is then the posterior conditional on the cells that remain.

Where no per-cell block was retained the estimate falls back to the grid-weighted average of the per-cell modes, restricted to the cells whose inner solve reached a mode. A fit where none did reports NA with interval_declined = "not_converged", rather than the vector its Newton started from as an estimate.

With control$skew_correct = TRUE a coefficient whose inner-Laplace gamma_3 is in the band it is valid on reports Cornish-Fisher quantiles instead, and a skew_applied attribute names which coefficients took the correction. That correction is measured at the MAP cell, so it is reported on its own and is not composed with the mixture read; a coefficient it declines keeps the mixture read rather than falling back further.

An axis_fields_dropped attribute carries the grid axes the fit's own resolved path could not read, one row per dropped field (block, type, field, path, integrates, reason). It is NULL whenever every supplied axis was used, which is the ordinary case; diagnostic_summary() reads the same record in sentences.

A beta_prior attribute carries the Gaussian fixed-effect prior the fit ran under, as list(mean, sd). It is the engine default, prior_normal(0, 2.5), whenever the caller supplied none, and NULL on the paths that express no Gaussian prior on the fixed effects.


Summary method for tulpa_svc_posterior

Description

Summary method for tulpa_svc_posterior

Usage

## S3 method for class 'tulpa_svc_posterior'
summary(object, probs = c(0.025, 0.5, 0.975), ...)

Arguments

object

A tulpa_svc_posterior object

probs

Quantiles to compute

...

Ignored

Value

A tulpa_svc_summary data frame with one row per location and term, holding the observation index, term name, coordinates, and the posterior mean, SD, and requested quantiles of each spatially-varying coefficient.


Summary method for tulpa_temporal_posterior

Description

Summary method for tulpa_temporal_posterior

Usage

## S3 method for class 'tulpa_temporal_posterior'
summary(object, probs = c(0.025, 0.5, 0.975), ...)

Arguments

object

A tulpa_temporal_posterior object

probs

Quantiles to compute

...

Ignored

Value

A tulpa_temporal_summary data frame with one row per time point (per component for multi-scale fits), holding the posterior mean, SD, and requested quantiles of the temporal effect.


Summary method for tulpa_tvc_posterior

Description

Summary method for tulpa_tvc_posterior

Usage

## S3 method for class 'tulpa_tvc_posterior'
summary(object, probs = c(0.025, 0.5, 0.975), ...)

Arguments

object

A tulpa_tvc_posterior object

probs

Quantiles to compute

...

Ignored

Value

A tulpa_tvc_summary data frame with one row per time point and term, holding the posterior mean, SD, and requested quantiles of each temporally-varying coefficient.


Extract spatially-varying coefficients from a fitted model

Description

Extract posterior distributions of spatially-varying coefficients (SVCs) from a fitted tulpa model with SVC specification.

Usage

svc(object, terms = NULL, summary = FALSE, probs = c(0.025, 0.5, 0.975), ...)

## S3 method for class 'tulpa_fit'
svc(object, terms = NULL, summary = FALSE, probs = c(0.025, 0.5, 0.975), ...)

Arguments

object

A tulpa_fit object fitted with svc argument

terms

Which SVC terms to extract. If NULL (default), extracts all.

summary

Logical; if TRUE, return summary statistics instead of full posterior draws.

probs

Quantiles to compute if summary = TRUE.

...

Ignored

Value

A tulpa_svc_posterior object containing:

See Also

spatial_svc(), plot.tulpa_svc_posterior()

Examples


set.seed(303)
n <- 25L
df <- data.frame(lon = runif(n), lat = runif(n), x = rnorm(n))
bsurf <- 0.9 * sin(2.8 * df$lon) + 0.7 * cos(2.2 * df$lat)
df$count <- rpois(n, exp(0.2 + (0.8 + bsurf) * df$x))

# The varying slope on `x` is a spatial field; SVC is exact-mode only.
fit <- tulpa(
  count ~ x,
  data = df,
  family = "poisson",
  spatial = spatial_svc(~ lon + lat, terms = ~ x - 1, nn = 5L),
  mode = "exact",
  control = list(n_iter = 80L, n_warmup = 40L, seed = 1L)
)

svc_post <- svc(fit)
summary(svc_post)



Extract temporal effects from a fitted model

Description

Extract posterior distributions of temporal effects from a fitted tulpa model with temporal specification.

Usage

temporal(
  object,
  component = "all",
  summary = FALSE,
  probs = c(0.025, 0.5, 0.975),
  ...
)

## S3 method for class 'tulpa_fit'
temporal(
  object,
  component = "all",
  summary = FALSE,
  probs = c(0.025, 0.5, 0.975),
  ...
)

Arguments

object

A tulpa_fit object fitted with temporal argument

component

Which component to extract for multi-scale models: "all" (default), "trend", "seasonal", or "short_term".

summary

Logical; if TRUE, return summary statistics instead of full posterior draws.

probs

Quantiles to compute if summary = TRUE.

...

Ignored

Details

temporal() is overloaded. Given a fitted model it is the accessor described here. Given a one-sided formula (or a named ⁠formula =⁠ / ⁠structure =⁠ argument) it is instead the inline varying-coefficient field constructor used in a tulpa() model formula, the temporal mirror of spatial(): temporal(formula = ~ 1 + x || time, structure = "rw1") declares a smooth temporal level (the intercept column) plus a temporally varying slope on each covariate column. structure is one of "rw1" (default), "rw2", or "ar1"; only the double bar || (independent fields) is supported.

Value

A tulpa_temporal_posterior object

See Also

temporal_multiscale(), temporal_rw1()

Examples


set.seed(131)
df <- data.frame(year = 1:40, x = rnorm(40))
df$count <- rpois(40, exp(1 + 0.2 * df$x))

fit <- tulpa(
  count ~ x,
  data = df,
  family = "poisson",
  temporal = temporal_multiscale("year", trend = "rw2", seasonal = 12),
  mode = "exact",
  control = list(n_iter = 200L, n_warmup = 100L, seed = 1L)
)

# Extract all temporal effects
temp_post <- temporal(fit)
summary(temp_post)



AR(p) temporal latent field (general-order autoregressive)

Description

A stationary autoregressive process of order p, w_t = \sum_{j=1}^{p} \phi_j w_{t-j} + \varepsilon_t, as a user-defined GMRF latent block for a model formula via latent(temporal_ar(...)) – the general-order extension of temporal_ar2(), sharing the same construction: the precision is the exact stationary AR(p) GMRF (the banded inverse of the Yule-Walker Toeplitz autocovariance), and stationarity is enforced by the partial-autocorrelation parameterization (Levinson-Durbin map of p unconstrained atanh_psi hyperparameters), so the hyperparameter integration never leaves the stationary region.

The block's hyperparameters are log_tau (log innovation precision) and atanh_psi1..⁠atanh_psi<p>⁠; the AR coefficients are recovered through psi_j = tanh(atanh_psi_j) and the Levinson-Durbin recursion. Note the outer integration cost grows with the hyperparameter count p + 1; orders beyond 3-4 are better served by a sampler tier.

Usage

temporal_ar(
  time_idx,
  p = 2L,
  n_times = NULL,
  prior_tau_sd = 2,
  prior_psi_sd = 1.5,
  name = NULL
)

Arguments

time_idx

Integer vector (1-based) of the time point for each observation.

p

Autoregressive order (>= 1).

n_times

Number of distinct time points; defaults to max(time_idx).

prior_tau_sd, prior_psi_sd

Prior SDs for log_tau and the atanh_psi hyperparameters (weakly-informative Gaussian). Defaults 2 / 1.5.

name

Optional block label (default ⁠ar<p>⁠).

Value

A tgmrf / tulpa_latent_block object.

See Also

temporal_ar2() (the p = 2 shorthand), temporal_ar1() for the first-order formula-integrated process, tgmrf() for the general user-defined GMRF interface.

Examples


set.seed(1)
Tt <- 150L
w <- numeric(Tt); w[1:3] <- rnorm(3)
for (t in 4:Tt) w[t] <- 0.4 * w[t-1] + 0.2 * w[t-2] - 0.2 * w[t-3] + rnorm(1, 0, 0.4)
d <- data.frame(t = seq_len(Tt), y = w + rnorm(Tt, 0, 0.3))
fit <- tulpa(y ~ latent(temporal_ar(d$t, p = 3)), data = d,
             family = "gaussian", mode = "nested_laplace")


AR1 temporal structure (First-order Autoregressive)

Description

Specify a first-order autoregressive temporal random effect. AR1 models temporal correlation where each time point depends on the previous one: phi[t] = rho * phi[t-1] + epsilon[t].

Unlike RW1/RW2, AR1 is a proper (stationary) model with an estimated autocorrelation parameter rho.

Usage

temporal_ar1(time_var, group_var = NULL, shared = NULL, rho_prior = NULL)

Arguments

time_var

A formula (~ time) or single character string naming the time variable in the data.

group_var

Optional formula (~ g) or character string naming a grouping variable. When supplied, a separate random walk is fit per group; NULL (default) fits a single walk shared across all observations.

shared

Whether the temporal effect is shared across processes in a multi-process model. NULL (default) shares the effect; FALSE fits process-specific effects and emits a warning about unshared confounding.

rho_prior

Prior for the AR(1) autocorrelation. Default NULL is a Uniform(-1, 1) prior. Supply a prior_beta(alpha, beta) to place a Beta(alpha, beta) prior on u = (rho + 1)/2 for a more informative prior (e.g. prior_beta(5, 2) favours positive autocorrelation). Honoured on both the exact-sampler and nested-Laplace paths.

Details

The AR1 process has marginal variance sigma^2 / (1 - rho^2) and correlation between time points t and s of rho^|t-s|.

The precision matrix is tridiagonal and full rank, so no constraints are needed.

Value

A tulpa_temporal object

Examples

# Create temporal AR1 specification
temporal_ar1("year")
temporal_ar1("year", group = "site")


# AR1 temporal correlation
set.seed(127)
df <- data.frame(
  year = rep(1:20, each = 3),
  x = rnorm(60)
)
f <- as.numeric(arima.sim(list(ar = 0.7), 20, sd = 0.4))
df$count <- rpois(60, exp(1 + 0.3 * df$x + f[df$year]))

fit <- tulpa(
  count ~ x,
  data = df,
  family = "poisson",
  temporal = temporal_ar1("year"),
  mode = "auto"
)
summary(fit)



AR(2) temporal latent field (second-order autoregressive)

Description

A stationary second-order autoregressive process w_t = \phi_1 w_{t-1} + \phi_2 w_{t-2} + \varepsilon_t as a user-defined GMRF latent block, for use in a model formula via latent(temporal_ar2(...)). It reuses tulpa's nested-Laplace / NUTS machinery (no dedicated C++ kernel). The precision is the exact stationary AR(2) GMRF; stationarity is enforced by the PACF parameterization, so the hyperparameter grid never leaves the stationarity region.

The block's hyperparameters are log_tau (log innovation precision) and atanh_psi1, atanh_psi2 (unconstrained partial autocorrelations); the AR coefficients are recovered as phi2 = tanh(atanh_psi2), phi1 = tanh(atanh_psi1) (1 - phi2).

Usage

temporal_ar2(
  time_idx,
  n_times = NULL,
  prior_tau_sd = 2,
  prior_psi_sd = 1.5,
  name = "ar2"
)

Arguments

time_idx

Integer vector (1-based) of the time point for each observation.

n_times

Number of distinct time points; defaults to max(time_idx).

prior_tau_sd, prior_psi_sd

Prior SDs for log_tau and the two atanh_psi hyperparameters (weakly-informative Gaussian). Defaults 2 / 1.5.

name

Optional block label.

Value

A tgmrf / tulpa_latent_block object.

See Also

temporal_ar1() for the first-order (formula-integrated) process; tgmrf() for the general user-defined GMRF interface.

Examples


set.seed(1)
Tt <- 120L
w <- numeric(Tt); w[1:2] <- rnorm(2)
for (t in 3:Tt) w[t] <- 0.5 * w[t-1] + 0.3 * w[t-2] + rnorm(1, 0, 0.4)
d <- data.frame(t = seq_len(Tt), y = w + rnorm(Tt, 0, 0.3))
fit <- tulpa(y ~ latent(temporal_ar2(d$t)), data = d, family = "gaussian",
             mode = "nested_laplace")


Extract temporal correlation parameters from a fitted model

Description

Returns posterior summary for temporal hyperparameters (tau, rho for AR1, sigma/lengthscale for temporal GP).

Usage

temporal_corr(object, probs = c(0.025, 0.975))

Arguments

object

A tulpa_fit object fitted with a temporal component.

probs

Quantile probabilities (default 0.025, 0.975).

Value

A data.frame with rows for each temporal hyperparameter.


Gaussian Process temporal structure

Description

Specify a Gaussian Process (GP) temporal random effect for irregularly-spaced or continuous time points. Unlike RW1/RW2/AR1 which assume equally-spaced observations, GP temporal effects model correlation as a function of time distance.

This is particularly useful for:

Usage

temporal_gp(
  time_var,
  cov = c("exponential", "matern", "gaussian", "periodic"),
  nu = 1.5,
  period = NULL,
  group_var = NULL,
  shared = NULL,
  scale_coords = TRUE,
  parameterization = c("noncentered", "centered")
)

Arguments

time_var

Name of the time variable in data. Can be a formula (e.g., ~ year) or a character string (e.g., "year"). Should be numeric (continuous time) or convertible to numeric.

cov

Covariance function: "exponential" (default, rough), "matern" (tunable smoothness), "gaussian" (very smooth), or "periodic" (for seasonal patterns).

nu

Smoothness parameter for Matern covariance, one of:

  • 0.5: equivalent to exponential (rough)

  • 1.5: once differentiable (moderate smoothness)

  • 2.5: twice differentiable (smooth) These are the smoothnesses with a closed form; anything between them needs a Bessel function of the second kind and is rejected. Ignored for non-Matern covariance functions.

period

Period for periodic covariance (e.g., 12 for monthly, 365 for daily data with annual cycle). Only used when cov = "periodic".

group_var

Optional name of grouping variable for panel data. If provided, separate GPs are estimated for each group.

shared

Logical; if TRUE (default), temporal effect enters both all processes.

scale_coords

Logical; if TRUE (default), time values are scaled to unit variance before computing distances.

parameterization

Parameterization for GP effects: "noncentered" (default) stores z ~ N(0,1) and scales by covariance (better for weakly-informed effects); "centered" stores effects directly (better for strongly-informed effects).

Details

The GP temporal model adds a time-correlated random effect:

\eta(t) = X\beta + f(t)

where f(t) follows a Gaussian process:

f(t) \sim GP(0, \sigma^2 C(|t - t'|; \phi))

The correlation function C(d; \phi) depends on time distance d:

Implementation: the exponential kernel (equivalently Matern with nu = 0.5) is an Ornstein-Uhlenbeck process, so its joint density factorizes into a first-order Markov chain and evaluates in O(T) with no matrix. The other kernels have no finite-dimensional state-space form and are evaluated from a dense T x T Cholesky, where T is the number of distinct time points.

The field is fit on the sampler path: its two hyperparameters (log_sigma2_temporal_gp, logit_phi_temporal_gp) are sampled jointly with the field and the fixed effects, so pass a sampler mode such as mode = "hmc". There is no nested-Laplace kernel for it.

Value

A tulpa_temporal_gp object

See Also

temporal_rw1(), temporal_ar1() for equally-spaced temporal effects, spatial_gp() for spatial GP effects

Examples

# Create GP temporal specification
temporal_gp("timestamp")
temporal_gp("day", cov = "matern", nu = 1.5)
temporal_gp("month", cov = "periodic", period = 12)


# Irregularly-spaced time series
set.seed(140)
times <- sort(runif(40, 0, 10))
df <- data.frame(
  time = times,
  x = rnorm(40),
  y = rbinom(40, 1, 0.5)
)

fit <- tulpa(
  y ~ x,
  data = df,
  family = "binomial",
  temporal = temporal_gp("time"),
  mode = "hmc",
  control = list(n_iter = 200, warmup = 100, n_chains = 1)
)



Multi-scale temporal structure

Description

Specify a temporal random effect that decomposes variation into separate scales: a smooth trend, an optional seasonal cycle, and a short-term component. Each scale uses its own prior, letting slow and fast dynamics be modelled jointly.

Usage

temporal_multiscale(
  time_var,
  trend = c("rw2", "rw1", "none"),
  seasonal = NULL,
  short_term = c("ar1", "iid", "none"),
  group_var = NULL,
  shared = NULL
)

Arguments

time_var

Single character string naming the time variable in the data.

trend

Prior for the smooth long-term trend. One of "rw2", "rw1", or "none".

seasonal

Optional integer period (⁠>= 2⁠) of a seasonal cycle, e.g. 12 for monthly data with an annual cycle. NULL (default) omits the seasonal component.

short_term

Prior for the short-term component. One of "ar1", "iid", or "none".

group_var

Optional character string naming a grouping variable for group-specific temporal effects.

shared

Whether the effect is shared across processes in a multi-process model. NULL (default) shares it; FALSE fits process-specific effects and emits a warning.

Details

At least one of trend, seasonal, or short_term must be active.

Value

A tulpa_temporal_multiscale object.

See Also

temporal_rw1(), temporal_rw2(), temporal_ar1() for single-scale temporal priors.

Examples

# Trend + annual seasonal cycle + AR1 short-term component on monthly data
temporal_multiscale("month", trend = "rw2", seasonal = 12, short_term = "ar1")


Restricted temporal regression (RTR)

Description

The temporal analogue of spatial_rsr(): constrain a temporal random effect to be orthogonal to a set of covariates, so a temporally smooth covariate does not have its fixed-effect coefficient attenuated by a confounded temporal field, u_{RTR} = (I - P_X) u.

No tulpa backend applies that projection to a temporal field, so this constructor errors rather than returning a specification that would fit as the unrestricted temporal model. spatial_rsr() is the wired spatial analogue.

Usage

temporal_rtr(temporal, restrict_to)

Arguments

temporal

A tulpa_temporal specification (e.g. temporal_rw1(), temporal_ar1()).

restrict_to

A one-sided formula giving the covariate space the temporal effect is made orthogonal to, e.g. ~ x.

Value

Nothing: the call always signals an error.

See Also

spatial_rsr(), temporal_rw1(), temporal_ar1()


RW1 temporal structure (First-order Random Walk)

Description

Specify a first-order random walk temporal random effect. RW1 penalizes first differences, so adjacent time points are smoothed toward each other: phi[t] - phi[t-1] ~ N(0, sigma^2).

Usage

temporal_rw1(time_var, group_var = NULL, cyclic = FALSE, shared = NULL)

Arguments

time_var

A formula (~ time) or single character string naming the time variable in the data.

group_var

Optional formula (~ g) or character string naming a grouping variable. When supplied, a separate random walk is fit per group; NULL (default) fits a single walk shared across all observations.

cyclic

Logical. If TRUE, the random walk wraps around so the last time point is a neighbour of the first (cyclic boundary, e.g. month of year). Default FALSE.

shared

Whether the temporal effect is shared across processes in a multi-process model. NULL (default) shares the effect; FALSE fits process-specific effects and emits a warning about unshared confounding.

Details

The precision matrix is rank T - 1 (one constraint needed). RW1 is the least smooth of the random-walk priors; for smoother trends see temporal_rw2(), and for a stationary alternative see temporal_ar1().

Value

A tulpa_temporal object.

See Also

temporal_rw2(), temporal_ar1() for other temporal priors.

Examples

# Create temporal RW1 specification
temporal_rw1("year")
temporal_rw1("month", cyclic = TRUE)


RW2 temporal structure (Second-order Random Walk)

Description

Specify a second-order random walk temporal random effect. RW2 penalizes deviations from linearity, resulting in smoother trends than RW1: phi[t] - 2*phi[t-1] + phi[t-2] ~ N(0, sigma^2).

Usage

temporal_rw2(time_var, group_var = NULL, cyclic = FALSE, shared = NULL)

Arguments

time_var

A formula (~ time) or single character string naming the time variable in the data.

group_var

Optional formula (~ g) or character string naming a grouping variable. When supplied, a separate random walk is fit per group; NULL (default) fits a single walk shared across all observations.

cyclic

Logical. If TRUE, the random walk wraps around so the last time point is a neighbour of the first (cyclic boundary, e.g. month of year). Default FALSE.

shared

Whether the temporal effect is shared across processes in a multi-process model. NULL (default) shares the effect; FALSE fits process-specific effects and emits a warning about unshared confounding.

Details

RW2 produces smoother curves than RW1 because it penalizes the second derivative (curvature) rather than the first derivative (slope). It requires at least 3 time points.

The precision matrix is rank T-2 (two constraints needed).

Value

A tulpa_temporal object

Examples

# Create temporal RW2 specification
temporal_rw2("year")


# Smooth temporal trend
set.seed(126)
df <- data.frame(
  year = rep(1:20, each = 4),
  x = rnorm(80)
)
trend <- sin(seq(0, 2, length.out = 20))
df$count <- rpois(80, exp(1 + 0.3 * df$x + trend[df$year]))

fit <- tulpa(
  count ~ x,
  data = df,
  family = "poisson",
  temporal = temporal_rw2("year"),
  mode = "auto"
)
summary(fit)



Time-varying coefficient structure

Description

Specify a time-varying coefficient (TVC): one or more fixed-effect coefficients are allowed to evolve over time, with the evolution governed by a temporal prior (rw1, rw2, ar1, or a GP).

Usage

temporal_tvc(
  time_var,
  terms = 1,
  structure = c("rw1", "rw2", "ar1", "gp"),
  group_var = NULL,
  shared = NULL,
  sigma_prior_U = 1,
  sigma_prior_alpha = 0.01
)

Arguments

time_var

Single character string naming the time variable in the data.

terms

Which coefficients vary over time. A formula, an integer vector of design-matrix column indices, or a character vector of term names. Default 1 (the intercept).

structure

Temporal prior governing how the coefficients evolve. One of "rw1", "rw2", "ar1", or "gp".

group_var

Optional character string naming a grouping variable for group-specific time-varying coefficients.

shared

Whether the effect is shared across processes in a multi-process model. NULL (default) shares it; FALSE fits process-specific effects and emits a warning.

sigma_prior_U, sigma_prior_alpha

Penalized-complexity prior on each varying coefficient's marginal standard deviation, calibrated so that P(sigma > sigma_prior_U) = sigma_prior_alpha. Defaults to P(sigma > 1) = 0.01. sigma_prior_U must be positive and sigma_prior_alpha must lie in ⁠(0, 1)⁠.

Value

A tulpa_tvc object.

See Also

temporal_rw1(), temporal_rw2(), temporal_ar1() for the underlying temporal priors.

Examples

# Intercept that drifts as a first-order random walk over year
temporal_tvc("year", structure = "rw1")


Test for over- or underdispersion

Description

Compares the variance of observed data to the variance expected under the fitted model (via simulation). Ratio > 1 = overdispersion; < 1 = underdispersion. Equivalent to DHARMa::testDispersion().

Usage

test_dispersion(object, ...)

## Default S3 method:
test_dispersion(
  object,
  observed = NULL,
  nsim = 250L,
  seed = 123L,
  alternative = c("two.sided", "greater", "less"),
  ...
)

Arguments

object

A fitted model with simulate() method

...

Passed to methods.

observed

Observed response vector (optional – extracted from fit)

nsim

Number of simulations (default 250)

seed

Random seed (default 123)

alternative

"two.sided", "greater", or "less"

Value

An htest object with dispersion ratio and p-value


Test for outliers (simulation envelope)

Description

Counts how many observations fall outside the min-to-max range of all simulations. Under a correct model, the expected number is approximately 2 * N / (nsim + 1). Equivalent to DHARMa::testOutliers().

Usage

test_outliers(object, ...)

## Default S3 method:
test_outliers(object, observed = NULL, nsim = 250L, seed = 123L, ...)

Arguments

object

A fitted model with simulate() method

...

Passed to methods.

observed

Observed response vector (optional)

nsim

Number of simulations (default 250)

seed

Random seed (default 123)

Value

An htest object (binomial test)


Test uniformity of PIT residuals

Description

If the model is correct, PIT residuals should follow Uniform(0, 1). Applies a Kolmogorov-Smirnov test against that null. Equivalent to DHARMa::testUniformity().

Usage

test_uniformity(
  object,
  observed = NULL,
  nsim = 250L,
  seed = 123L,
  plot = FALSE
)

Arguments

object

A fitted model, a numeric vector of PIT residuals, or a matrix of simulations

observed

Observed data (if object is simulations matrix)

nsim

Number of simulations (default 250)

seed

Random seed (default 123)

plot

If TRUE, draws a QQ plot

Value

An htest object (KS test result)


Test for zero inflation

Description

Compares the number of zeros in observed data to the distribution expected under the fitted model. Equivalent to DHARMa::testZeroInflation().

Usage

test_zero_inflation(object, ...)

## Default S3 method:
test_zero_inflation(object, observed = NULL, nsim = 250L, seed = 123L, ...)

Arguments

object

A fitted model with simulate() method

...

Passed to methods.

observed

Observed response vector (optional)

nsim

Number of simulations (default 250)

seed

Random seed (default 123)

Value

An htest object with zero-inflation ratio and p-value


User-defined GMRF latent block

Description

tgmrf() lets a user define a Gaussian Markov random field as a latent block in a tulpa model by supplying two R closures: a precision-matrix factory Q(theta) and a log-prior on theta prior(theta). The resulting object plugs into a formula via latent(tgmrf(...)) and is consumed by every tulpa inference tier (Laplace, EM+Laplace, VI, nested_laplace+CCD, NUTS, IMH-Laplace).

This is the latent-side dual of LikelihoodSpec. LikelihoodSpec lets a model package own the observation model; tgmrf() lets a script own a single latent block.

Usage

tgmrf(
  Q,
  prior,
  init,
  mu = NULL,
  graph = NULL,
  bounds = NULL,
  obs_idx = NULL,
  name = NULL
)

Arguments

Q

A function ⁠function(theta)⁠ returning a sparse precision matrix of class Matrix::sparseMatrix (typically dgCMatrix). The matrix must be square and symmetric. Called once at registration with theta = init to infer n_latent and capture the sparsity pattern.

prior

A function ⁠function(theta)⁠ returning a finite numeric scalar – the log-prior density at theta.

init

Numeric vector of starting values for theta. Names, if present, become the canonical theta names.

mu

Optional function ⁠function(theta)⁠ returning a numeric vector of length n_latent. Default NULL is equivalent to a zero mean.

graph

Optional sparse matrix specifying the upper bound on Q's sparsity pattern. If supplied, the registration check verifies that the nonzero pattern of Q(init) is a subset.

bounds

Optional list with components lower and upper, each a numeric vector of length length(init). Used by nested_laplace + CCD to build the outer grid; unused by NUTS / VI.

obs_idx

Optional integer vector mapping each observation to a latent slot in ⁠[1, n_latent]⁠. If NULL (default), the fit-time driver assumes N == n_latent and uses seq_len(N) – i.e. one observation per latent slot, in row order.

name

Optional character; cosmetic label used by print() / summary().

Details

For a Gaussian latent block z ~ N(mu(theta), Q(theta)^{-1}):

\log p(z\mid\theta) = \tfrac{1}{2}\log\det Q(\theta) - \tfrac{1}{2}(z - \mu(\theta))^\top Q(\theta)(z - \mu(\theta)) + \mathrm{const}

the gradient -Q(z - mu) and Hessian -Q wrt z are closed form, so the user never writes gradient code. The Laplace inner step needs only Q(theta) and mu(theta) at numeric theta. NUTS and VI additionally use a forward finite-difference gradient on theta, costing dim(theta) extra Q calls per outer step – cheap for the typical dim(theta) <= 5.

Value

An object of class c("tgmrf", "tulpa_latent_block") with components:

See Also

latent() for the formula slot that consumes a tgmrf block.

Examples

# Periodic AR(1) block, wrap-around tridiagonal precision.
periodic_ar1 <- function(n) {
  tgmrf(
    Q = function(theta) {
      sigma <- exp(theta[1]); rho <- tanh(theta[2])
      d <- rep((1 + rho^2) / sigma^2, n)
      o <- rep(-rho / sigma^2, n)
      M <- Matrix::bandSparse(
        n, k = c(-1, 0, 1), diagonals = list(o, d, o)
      )
      M[1, n] <- M[n, 1] <- -rho / sigma^2
      methods::as(M, "CsparseMatrix")
    },
    prior = function(theta) {
      stats::dnorm(theta[1], 0, 1, log = TRUE) +
        stats::dnorm(theta[2], 0, 1, log = TRUE)
    },
    init = c(log_sigma = 0, atanh_rho = 0),
    name = "periodic_ar1"
  )
}

blk <- periodic_ar1(20)
print(blk)


User-defined GMRF latent block, compiled C++ backend

Description

tgmrf_cpp() is the compiled-C++ analogue of tgmrf(). Where tgmrf() takes R closures Q(theta) / prior(theta), tgmrf_cpp() takes a user-written .cpp file that defines the same kernels as templated C++ (so the same source compiles against every AD type tulpa uses). The returned object has the same S3 class as tgmrf()c("tgmrf", "tulpa_latent_block") – so every downstream consumer (formula parser, inference layers, S3 methods) treats the two paths identically. Only the dispatch on Q(theta) differs: registry-stored function pointer (tgmrf_cpp()) vs Rcpp::Function callback (tgmrf()).

The user .cpp file must include ⁠<tulpa/tgmrf.h>⁠ and call the TULPA_REGISTER_TGMRF(id, Q_fn, mu_fn, log_prior_fn) macro once. See inst/examples/tgmrf_periodic_ar1.cpp for a worked example.

Usage

tgmrf_cpp(
  cpp_file,
  id,
  init,
  mu = NULL,
  graph = NULL,
  bounds = NULL,
  obs_idx = NULL,
  name = NULL,
  cache_dir = tulpa_cache_dir(),
  rebuild = FALSE
)

Arguments

cpp_file

Absolute path to the user's .cpp file. The file is compiled via Rcpp::sourceCpp() with caching keyed on digest::sha256(file_contents) + TULPA_ABI_VERSION so repeated calls with an unchanged source skip the rebuild.

id

Character: the stable id used as the registry key. Must match the first argument of TULPA_REGISTER_TGMRF in the user's .cpp.

init, mu, graph, bounds, obs_idx, name

Same arguments as tgmrf(). init is required and supplies the canonical theta names. mu is currently ignored on the C++ path – the mu kernel registered by TULPA_REGISTER_TGMRF is the source of truth; pass the argument here only to keep call sites symmetric with tgmrf().

cache_dir

Directory for Rcpp's sourceCpp cache. Defaults to a per-user location under tools::R_user_dir("tulpa", "cache").

rebuild

Force a recompile even if the cached DLL is up to date. Default FALSE.

Value

An object of class c("tgmrf", "tulpa_latent_block") with the same fields as tgmrf() plus backend = "cpp" and cpp_id = id. The Q / prior fields are R wrapper closures around the registered C++ kernels, so callers (e.g. NUTS-over-theta) that hold the object can still call block$Q(theta) directly.

See Also

tgmrf() for the R-closure path; latent() for the formula slot that consumes a tgmrf block.

Examples

## Not run: 
# Compile a user latent block against tulpa's AD types; see
# inst/examples/tgmrf_periodic_ar1.cpp for a complete block.
blk <- tgmrf_cpp("my_block.cpp", id = "ar1", init = c(0, 0), graph = my_graph)

## End(Not run)

Tidy fixed-effect table (broom-compatible)

Description

Tidy fixed-effect table (broom-compatible)

Usage

## S3 method for class 'tulpa_fit'
tidy(x, conf.level = 0.95, ...)

Arguments

x

A tulpa_fit object.

conf.level

Interval level (default 0.95).

...

Ignored.

Value

Data frame: term, estimate, std.error, conf.low, conf.high.

Examples


set.seed(1)
df <- data.frame(x = rnorm(100), g = factor(rep(1:10, 10)))
df$y <- rpois(100, exp(0.3 * df$x))
fit <- tulpa(y ~ x + (1 | g), data = df, family = "poisson")
tidy(fit)


Fit a tulpa model

Description

Single entry point for fitting a Bayesian hierarchical model. tulpa() parses the formula, builds the model matrices, selects an inference backend through the tier/mode system (see inference_mode_info()), assembles the arguments that backend needs, and dispatches.

The fit conditions on the random-effect standard deviations sigma_re (and, for non-Gaussian dispersion, phi): both the Laplace (Tier 2) and the sampler (Tier 1) paths target the posterior given these. Integrating over the hyperparameters is the role of the nested-Laplace / EM layer.

Usage

tulpa(
  formula,
  data,
  family = "gaussian",
  mode = "auto",
  sigma_re = NULL,
  n_trials = NULL,
  weights = NULL,
  phi = 1,
  estimate_phi = FALSE,
  phi2 = NULL,
  beta_prior = NULL,
  re_prior = NULL,
  ziformula = NULL,
  zi_prior = NULL,
  warm_start = NULL,
  spatial = NULL,
  temporal = NULL,
  control = list(),
  ...
)

Arguments

formula

A model formula. Fixed effects, (1 | g) / (1 + x | g) random effects, and offset(...) terms are recognised.

data

A data frame.

family

Character family name: one of family_names() ("binomial", "poisson", "neg_binomial_2", "gaussian", "beta", ...), or a categorical response family – "multinomial" (baseline-category logit via tulpa_multinomial()), "ordinal" (cumulative logit via tulpa_ordinal()), or "ordinal_probit" (cumulative probit). Categorical families take fixed-effect models only.

mode

Inference mode or backend. "auto" (default) picks the most reliable Tier 1/Tier 2 method expected to finish; a tier ("exact", "structured") or a backend name ("laplace", "mala", ...) forces it. "eb" estimates the random-effect covariance(s) by empirical Bayes instead of conditioning on sigma_re (see tulpa_eb()); it is opt-in by name, because its intervals are conditional on that estimate rather than marginal over it.

sigma_re

Random-effect SDs to condition on: length 1 (recycled) or one per RE term. Defaults to 1 per term with a message. Ignored by the backends that determine the covariance themselves ("eb", re_cov_nested, re_cov_gibbs, gibbs, agq), which warn if it is supplied anyway.

n_trials

Binomial denominators (length nrow(data)), or NULL.

weights

Optional observation weights (non-negative numeric vector, length nrow(data)): each observation's log-likelihood contribution is scaled by its weight (prior / frequency weights, e.g. survey weights or aggregated-data counts – a weight of 2 is equivalent to duplicating the row). Supported on the non-spatial Laplace path (mode = "laplace") and the log-posterior samplers (mala, imh_laplace, pathfinder); other backends reject weights loudly.

phi

Dispersion/precision passed to the family (residual variance for gaussian and lognormal, size for neg_binomial_2, precision for beta, scale for t). The variance convention holds across every backend; the SD-parameterized compiled kernels receive sqrt(phi) at the boundary.

estimate_phi

Estimate the dispersion from the data instead of conditioning on phi, which then supplies the starting value. log(phi) joins the empirical-Bayes maximization as one further coordinate carrying the exact derivative of the Laplace log-marginal, so the estimate is ML-II: the hyperprior covers the random-effect covariances only and the dispersion enters unpenalized. fit$phi is the estimate and fit$phi_estimated distinguishes it from a conditioned value.

Available under mode = "eb", and for the families whose dispersion derivative is registered (see tulpa_eb()). Any other mode errors rather than fitting at the starting value under a name that says otherwise.

phi2

Optional second dispersion: the Student-t degrees of freedom (family = "t"; default 4 when NULL) or the Tweedie variance power (family = "tweedie", required – a defaulted power would be a statistical decision the caller never made). Supported on the non-spatial Laplace path, the random-effect covariance paths (mode = "eb" and the nested Sigma integrator, which thread it into their inner Laplace solve), the log-posterior samplers, and the ModelData samplers. Backends without a phi2 channel refuse it rather than fit at the family's default. estimate_phi covers phi alone; phi2 is always conditioned on.

beta_prior

Optional list(mean, sd) Gaussian prior on the fixed effects. NULL takes the engine default, prior_normal(0, 2.5), on every backend that carries a fixed-effect prior – the prior is a modelling statement, so the backend mode = "auto" selects does not change it. The nested-Laplace and SPDE paths hold their own field-conditional prior and reject a supplied beta_prior. The resolved prior is reported on the fit as ⁠$beta_prior⁠.

re_prior

Optional list() of random-effect / variance-component hyperpriors (statistical, so they live in the signature rather than in control). Recognised entries, each consumed by the backend that needs it: hyperprior ("flat" default or "pc_lkj", mode = "laplace" random slopes and mode = "eb" – see tulpa_re_cov_nested()), prior_sigma (PC-prior anchor c(U, alpha) on a free RE covariance SD, used when hyperprior = "pc_lkj"), eta (LKJ concentration for a correlated RE covariance, same condition), prior_df / prior_scale (inverse-Wishart on the RE covariance, control$re_cov = "gibbs"), prior_sigma_scale (half-Cauchy scale on the RE SD for mode = "gibbs"), and sigma_re_scale (half-Cauchy scale on the RE / BYM2 SD for the ModelData samplers).

ziformula

Optional one-sided formula for the zero-inflation probability, e.g. ~ 1 for a constant structural-zero rate or ~ x to model it. The response becomes a mixture: with probability ⁠plogis(X_zi beta_zi)⁠ the observation is a structural zero, otherwise it is drawn from family. Available for the count families with a compiled zero-inflated kernel; paired with truncated_poisson or truncated_neg_binomial_2 it is the hurdle model, since the base P(Y = 0) is then 0 and the mixture degenerates to the two-part likelihood. Backends that do not carry the mixture refuse it rather than fit the model without it.

zi_prior

Optional list(sd) Gaussian prior on the zero-inflation coefficients, beta_zi ~ N(0, sd^2); NULL (default) uses 2.5. One scalar SD applies to the whole block, and the mean is fixed at 0, because that is what the compiled kernels carry. The prior is what identifies the logit where a level contributes no zeros – there the likelihood is monotone in that coefficient and alone would send it to -Inf. sd = Inf removes the penalty. Ignored without ziformula.

warm_start

Optional starting point for the NUTS sampler, from a cheaper fit of the same model: "eb" or "laplace" fits one first, or pass an existing fit from either mode. The sampler then starts at that mode with an inverse mass read off its curvature, instead of at the origin with a structural one. Chains after the first are dispersed around the mode at the fit's own scale, so between-chain spread – which rhat() compares against – is not collapsed by the shared starting point. Only the NUTS/HMC backends take one; the rest error rather than ignore it. Not available under a spatial, temporal or GP field, whose hyperparameters neither source fit estimates.

The variance-component slots take an adapting mass by default, because a plug-in fit estimates no curvature for them. Passing a fit from tulpa_eb() with marginal = TRUE supplies one: its theta_cov gives each log_sigma_re slot a posterior variance to start from. This applies to uncorrelated terms, whose hyperparameter coordinates are the log standard deviations the sampler holds; a correlated term stays adapting, since its log-Cholesky coordinates are not the sampler's.

spatial

Optional spatial-field spec. How it is addressed depends on the field family:

  • Areal ("icar", "car", "bym2", "car_proper"): a list with type and adjacency, paired with a spatial(col) term in formula naming the per-observation unit column. Term and spec must be supplied together.

  • Continuous (spatial_gp(~ lon + lat) for an NNGP field, spatial_gp(~ lon + lat, approx = 'hsgp') for a Hilbert-space GP, spatial_spde(~ lon + lat, data) for a Matern SPDE field): the spec object carries the coordinate columns (the SPDE spec also carries the mesh + FEM matrices), so no spatial(col) term is used – observations are mapped to locations from their coordinates.

The mode selects how the spatial hyperparameter is handled:

  • mode = "nested_laplace", "structured", and "auto" (when not the binomial Gibbs case below) integrate the hyperparameter – the designed Tier 2 path, mirroring latent(...) blocks. Areal icar/car/ bym2/car_proper and continuous gp/nngp/hsgp go through tulpa_nested_laplace(); SPDE is redirected to fit_spde(), which integrates ⁠(range, sigma)⁠ with its own CCD / grid design.

  • mode = "laplace" conditions on a fixed hyperparameter via tulpa_laplace() (the cheap explicit fit).

  • mode = "gibbs" routes the areal icar/bym2 cases through the binomial Polya-Gamma samplers (Tier 1 exact); mode = "auto" picks this for a binomial icar/bym2 field.

temporal

Optional temporal field spec (temporal_rw1(), temporal_rw2(), or temporal_ar1()), integrated by nested Laplace. A plain field routes the single-block temporal kernel; a group_var panel spec fits a separate walk per group sharing one hyperparameter; combined with an areal spatial field it forms an additive space-time joint prior.

control

Optional list of backend tuning arguments (e.g. n_iter, warmup, epsilon for mala; n_draws for pathfinder).

...

Reserved for future statistical arguments. Nothing is read from it today, so any entry errors: a stray name here is a misspelled argument or a tuning knob that belongs in control.

Value

A tulpa_fit object carrying the backend's output plus inference_mode, inference_tier, backend, selection_reason, formula, and family. Two field-name conventions to know when reaching into the object directly (the generic accessors handle both): on nested-Laplace fits ⁠$weights⁠ is the hyperparameter GRID weights; user observation weights are stored as ⁠$obs_weights⁠. ⁠$draws⁠ is a draws matrix on engine fits, while model-package fits may carry a list (⁠$y_rep⁠, ⁠$log_lik⁠) under the same name.

Coverage

References

Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392. Hoffman & Gelman (2014). The No-U-Turn Sampler: adaptively setting path lengths in Hamiltonian Monte Carlo. JMLR 15(47):1593-1623.

See Also

inference_mode_info(), tulpa_laplace(), mala(), pathfinder()

Examples


set.seed(1)
n <- 200L
g <- sample(letters[1:12], n, replace = TRUE)
d <- data.frame(
  y = rbinom(n, 1, plogis(-0.3 + 0.6 * rnorm(n))),
  x = rnorm(n),
  g = g
)
# Random-intercept logistic GLMM, Laplace tier.
fit <- tulpa(y ~ x + (1 | g), data = d, family = "binomial", mode = "laplace")
coef(fit)
summary(fit)


Environment variables tulpa reads

Description

Switches read from the process environment rather than from a fitter's control list, because each one changes how the compiled kernels partition work rather than what model is fitted. Each is read once, when the package's shared library loads, so it has to be set before library(tulpa) – setting it later in a session has no effect.

Reproducibility of a parallel scatter

TULPA_GRID_WORKSTEAL=0

Forces the serial per-cell coupling scatter. The work-stealing partition is pinned by the grid geometry, so a grid solved with several outer threads already reproduces the serial reduction; this is the escape hatch if that ever has to be verified against a plain sequential pass.

TULPA_COUPLING_FORCE_PARALLEL

Set (to any value) to take the chunked parallel reduce on every coupled cell instead of only where the cell count pays for the per-chunk partial gradient and Hessian buffers. A small grid then exercises the parallel path. The reduce runs in fixed chunk order, so the answer is identical either way, which is what tests/testthat/test-coupling-force-parallel.R asserts.

Test-suite tiers

TULPA_FAST, TULPA_SLOW_TESTS and TULPA_FULL_RECOVERY select which cost tier of the test suite runs; they are read by the suite, not by the package, and are described in tests/testthat/README.md.


Replicate an areal graph across the levels of a factor (replicated CAR)

Description

Build the block-diagonal Kronecker graph ⁠I_L (x) Q⁠ and the level-offset node index for a replicated areal field: one independent copy of the graph per level of a by factor, all sharing one precision. This is the graph-side counterpart to tulpa_bar_field_specs() – that helper expands the coefficient columns and is graph-agnostic, while replication needs the graph, so it is a sibling rather than a new argument. A downstream package composes the two (column expansion x replication) from the one implementation rather than re-deriving the Kronecker remap.

Usage

tulpa_bar_field_replicate(adjacency, node, by)

Arguments

adjacency

Symmetric adjacency matrix of the base graph (⁠[n_node x n_node]⁠, dense or sparse).

node

Integer vector of 1-based graph-node indices, one per observation (the resolved bar right-hand side).

by

A vector of the same length as node giving each observation's replication level; coerced to a factor. With L distinct levels the field is replicated L times.

Value

A list:

adjacency

the ⁠[L*n_node x L*n_node]⁠ block-diagonal Kronecker adjacency ⁠I_L (x) Q⁠ (the base graph for L == 1).

index

integer vector, one per observation: the node index offset into its level's copy (node + (level - 1) * n_node).

n_levels

the number of replication levels L.

n_nodes

the base graph node count n_node.

levels

the factor levels of by, in replicate order.

See Also

tulpa_bar_field_specs() for the coefficient-column expansion, spatial() for the inline areal field constructor whose ⁠by =⁠ argument this powers.

Examples

adj <- matrix(0, 4, 4)
for (i in 1:3) adj[i, i + 1] <- adj[i + 1, i] <- 1
node <- rep(1:4, times = 2)
lev  <- rep(c("a", "b"), each = 4)
rep_info <- tulpa_bar_field_replicate(adj, node, lev)
dim(rep_info$adjacency)   # 8 x 8 (I_2 (x) Q)
rep_info$index            # level b nodes offset by 4


Expand a varying-coefficient bar into per-column field specs

Description

Expand the left-hand side of an lme4-style varying-coefficient bar (~ 1 + w || node) against a data frame into one descriptor per design matrix column. This is the same expansion spatial() and the inline temporal() field constructor use internally to turn a bar into one CAR / temporal field per design column, exposed so a downstream package can reuse the one implementation rather than re-parsing the bar grammar.

The bar's right-hand side names the node index (the graph node for a spatial field, the time index for a temporal field); it is not expanded but is returned as the node attribute. The left-hand side is expanded with stats::model.matrix(): the intercept column (1) is the unweighted (all-ones) field, and each covariate column is a varying coefficient whose per-observation weight is that column's design value (⁠0 +⁠ drops the intercept).

Usage

tulpa_bar_field_specs(formula, data)

Arguments

formula

A one-sided formula carrying a single grouping bar, e.g. ~ 1 + w || cell. Use tulpa_is_spatial_bar() to test first. The right-hand side must be a single bare column naming the node index; nesting (a / b), interaction (a:b), or expression grouping is rejected.

data

A data frame whose columns the bar left-hand side and the node index refer to. The per-column weight vectors are evaluated against it, so they have nrow(data) entries.

Value

A list with one element per design-matrix column, each a list:

column_name

character; "Intercept" for the intercept column, otherwise the model.matrix() column name (e.g. "w").

weight

a numeric vector of length nrow(data) for a covariate column (the per-observation design value scaling that field), or NULL for the intercept column (which is the all-ones, unweighted field).

is_intercept

logical; TRUE for the intercept column.

The list carries two attributes: node (character, the node-index column named by the bar right-hand side) and correlated (logical, TRUE for a single |, FALSE for a double ||).

See Also

tulpa_is_spatial_bar() for the recognizer, spatial() for the inline areal field constructor that consumes this expansion.

Examples

d <- data.frame(cell = rep(1:5, each = 4), w = rnorm(20))

# Intercept plus a varying slope on w
specs <- tulpa_bar_field_specs(~ 1 + w || cell, d)
length(specs)                 # 2
specs[[1]]$column_name        # "Intercept"
is.null(specs[[1]]$weight)    # TRUE (unweighted field)
specs[[2]]$column_name        # "w"
identical(specs[[2]]$weight, d$w)  # TRUE
attr(specs, "node")           # "cell"


Build model matrices from a parsed formula

Description

Takes a parsed formula and data frame, and constructs:

Usage

tulpa_build_model_data(parsed, data)

Arguments

parsed

A tulpa_parsed_formula object

data

A data frame

Value

A list with:


Remove compiled blocks from the tgmrf_cpp() cache

Description

Deletes cached compilation output under tulpa_cache_dir(). The cache holds build artefacts only – a removed entry is rebuilt by the next tgmrf_cpp() call on the same source – so it can be emptied at any time.

Usage

tulpa_cache_clear(older_than = 0)

Arguments

older_than

Remove only entries whose last modification is more than this many days ago. 0 (the default) removes every entry.

Value

The number of entries removed, invisibly.

See Also

tulpa_cache_dir() for the location.

Examples

## Not run: 
# Deletes the caller's own cached builds, so it is not run on check.
tulpa_cache_clear()              # empty the cache
tulpa_cache_clear(older_than = 30)  # keep the last 30 days

## End(Not run)


Default cache directory for tgmrf_cpp()-compiled DLLs

Description

Reports the per-user directory under tools::R_user_dir("tulpa", "cache") where tgmrf_cpp() keeps the objects it compiles. Reporting the path does not create it: the directory appears the first time a compile needs it, and tulpa_cache_clear() removes what it holds.

Usage

tulpa_cache_dir(create = FALSE)

Arguments

create

Create the directory if it is missing. FALSE (the default) only reports the path.

Value

Absolute path (character) to the cache directory.

See Also

tulpa_cache_clear() to remove cached objects.

Examples

# Where compiled tgmrf_cpp() blocks are cached. Reporting creates nothing.
tulpa_cache_dir()


Validate a control = list() surface against its canonical key set

Description

Front-door fitters across the ⁠tulpa*⁠ ecosystem carry their perf / numerical / tuning knobs in a single control list (design principle 6). Without a name check a misspelled knob is a silent no-op – control$adaptve_grid fits the default and reports nothing. This validates the names a caller supplied against the whitelist of what the target fitter actually reads, and errors listing the allowed set.

Usage

tulpa_check_control(control, allowed, where)

Arguments

control

The control list to validate. NULL and empty lists pass.

allowed

Character vector of accepted knob names.

where

Name of the calling fitter, used in the error message.

Details

Consumer packages (tulpaRatio, tulpaObs) call this with their own key registry rather than reimplementing the check.

Value

invisible(NULL), called for the side effect of erroring on an unknown or unnamed knob.

Examples

tulpa_check_control(list(max_iter = 50), c("max_iter", "tol"), "my_fit")
try(tulpa_check_control(list(max_itr = 50), c("max_iter", "tol"), "my_fit"))


Model criteria from a pointwise log-likelihood

Description

Turn an ⁠[n_draws x n_obs]⁠ pointwise log-likelihood into the standard Bayesian goodness-of-fit currency: WAIC, DIC, conditional predictive ordinates (CPO) and their sum LPML, PSIS-LOO, all from one matrix. PSIS-LOO reuses the native tulpa_psis() smoothing, so the CPO and LOO numbers are the same computation (CPO_i = exp(elpd_loo_i), LPML = elpd_loo). The input may be a matrix or a streaming tulpa_loglik() so EVA-scale fits are processed in observation blocks.

Usage

tulpa_criteria(
  log_lik,
  criteria = c("waic", "loo", "cpo", "lpml", "dic"),
  loglik_at_mean = NULL,
  group = NULL,
  chunk_size = NULL,
  pointwise = FALSE
)

Arguments

log_lik

An ⁠[n_draws x n_obs]⁠ numeric matrix of pointwise log-likelihoods, or a tulpa_loglik() streaming wrapper.

criteria

Which criteria to compute. Any of "waic", "loo", "cpo", "lpml", "dic". "loo", "cpo", and "lpml" share the single PSIS pass; "dic" additionally needs loglik_at_mean.

loglik_at_mean

Optional length-n_obs vector of pointwise log-likelihoods evaluated at the posterior mean of the parameters, supplied by the caller (the model package knows the parameterization). Required for DIC's plug-in deviance; without it the DIC fields are NA.

group

Optional length-n_obs grouping (an integer / factor / character vector). The LOO unit is one column of log_lik: with group = NULL (the default) every column is its own fold (leave-one-row-out, e.g. per plot / per visit) and the result is byte-identical to the ungrouped call. When supplied, the per-draw pointwise log-likelihoods are summed within group to a ⁠[n_draws x n_groups]⁠ matrix before PSIS, so each fold is a whole group (leave-one-group-out cross-validation, LOGO-CV). Use it to switch the estimand from per-row to per-group LOO – e.g. on a cell-compressed hierarchical fit, leave out a whole cell rather than one of its rows. WAIC's variance term, lppd, elpd_loo, cpo and pareto_k are all computed on the grouped matrix. DIC is a plug-in deviance over all observations and is unaffected by group.

chunk_size

Number of observation columns to process per block. The default streams the whole matrix at once when materialized, else picks a block sized to a few million entries.

pointwise

If TRUE, also return the per-observation vectors (elpd_waic, p_waic, elpd_loo, pareto_k, cpo) for plotting / stacking.

Details

p_waic is the well-known positively-biased variance estimator at low draw counts; the result records n_draws and the count of observations with p_waic_i > 0.4 (the loo heuristic for an unreliable WAIC), and the PSIS-LOO elpd_loo is the more stable figure to report when that count is non-trivial.

The LOO unit is whatever one column of log_lik holds. If the consumer built the matrix with one column per row (plot / visit), the default is per-row LOO; if a column already carries a whole group's compressed likelihood, leaving it out drops that group and pareto_k can blow up by construction. The group argument makes the unit explicit: supply it to aggregate columns into folds and report leave-one-group-out CV (LOGO-CV) instead, a different and deliberate estimand.

Value

A tulpa_criteria object: a list with the requested scalar scores (each estimate paired with its standard error where defined), n_draws / n_obs, the PSIS pareto_k summary, and – when pointwise = TRUE – a pointwise data frame.

References

Vehtari, Gelman & Gabry (2017). Practical Bayesian model evaluation using leave-one-out cross-validation and WAIC. Statistics and Computing 27(5):1413-1432. Watanabe (2010). Spiegelhalter et al. (2002). Geisser & Eddy (1979).

See Also

tulpa_psis() for the smoothing core, tulpa_pit() for the probability-integral-transform companion, compare_models() for model comparison.

Examples

# A draws x observations log-likelihood matrix (here built directly;
# in practice extracted from a fitted model's posterior draws).
set.seed(1)
y  <- rnorm(40)
mu <- matrix(rnorm(200 * 40, sd = 0.2), 200, 40)
ll <- dnorm(matrix(y, 200, 40, byrow = TRUE), mean = mu, log = TRUE)
tulpa_criteria(ll)
tulpa_criteria(ll, criteria = "waic", pointwise = TRUE)$pointwise[1:3, ]

Simulation-Based Diagnostics for tulpa Models

Description

Model-checking tools based on posterior predictive simulation. These are native R implementations equivalent to DHARMa's test suite, with no external dependencies. They work with any model that provides simulate(), fitted(), and residuals() methods.

Value

The diagnostic functions documented in this family return their individual results (a test-statistic object, a data frame of residuals, or a check_model summary); see each function's own help page.


Dispatch a fit to the backend chosen by the mode/tier system.

Description

The routing spine. Resolves mode to a concrete backend via select_inference_mode(), asserts the backend is R-reachable (fails loudly otherwise), then calls its fitter with fitter_args. The caller supplies fitter_args matching the backend's input contract (⁠BACKEND_REGISTRY$<backend>$input⁠).

The selected mode/tier/backend are stamped onto the returned fit (without overwriting any the fitter already set), so the inference contract is always visible in the output.

Usage

tulpa_dispatch(
  mode,
  fitter_args = list(),
  family = NULL,
  n_obs = NULL,
  has_spatial = FALSE,
  has_temporal = FALSE,
  has_latent = FALSE,
  spatial_type = NULL,
  temporal = NULL
)

Arguments

mode

User-specified mode ("auto", a tier, or a backend name).

fitter_args

Named list of arguments forwarded to the backend fitter.

family, n_obs, has_spatial, has_temporal, has_latent, spatial_type, temporal

Model characteristics forwarded to select_inference_mode().

Value

The fitter's result, with inference_mode, inference_tier, backend, and selection_reason ensured.


Posterior draws as a 3D array

Description

Assembles a fitted model's posterior draws into an ⁠[iteration, chain, parameter]⁠ array – the layout expected by bayesplot and analogous to posterior::as_draws_array(). Multiple chains are recognised from a 3D draws array, a ⁠$chain_id⁠ row map, or an ⁠$n_chains⁠ count over chain-major rows; a single pooled chain yields a one-chain array.

Usage

tulpa_draws_array(fit)

Arguments

fit

A tulpa_fit (or subclass) carrying posterior ⁠$draws⁠.

Details

As with posterior_sample(), a fit that carries no draws returns NULL with a message naming its backend and the representation it carries instead.

Value

A numeric array with dimensions ⁠[n_iter, n_chain, n_param]⁠ and the parameter names on the third dimension, or NULL if the fit carries no draws. Chains of unequal length are truncated to the shortest.

See Also

posterior_sample(), tulpa_posterior_draws(), diagnostics()


Empirical-Bayes random-effect covariances

Description

Estimate one or more random-effect covariances Sigma by maximizing the Laplace marginal likelihood over them (plus the hyperprior), then report the fixed effects conditional on the maximizer. This is the plug-in ("ML-II" / empirical-Bayes) counterpart of tulpa_re_cov_nested(), which integrates over Sigma instead of fixing it at the maximizer.

Usage

tulpa_eb(
  y,
  n_trials = NULL,
  X,
  re_terms,
  family = "binomial",
  phi = 1,
  phi2 = NULL,
  prior_sigma = c(3, 0.05),
  eta = 2,
  hyperprior = c("flat", "pc_lkj"),
  log_prior_theta = NULL,
  beta_prior = NULL,
  offset = NULL,
  n_quad = 1L,
  marginal = FALSE,
  estimate_phi = FALSE,
  X_zi = NULL,
  zi_prior_sd = 2.5,
  control = list()
)

Arguments

y, n_trials, X, family, phi

Passed to tulpa_laplace() for the inner solve. n_trials = NULL defaults to 1 (binary / single-trial).

re_terms

Either a single random-effect term or a list of them; see tulpa_re_cov_nested() for the per-term fields.

phi2

Optional second dispersion, threaded into every inner tulpa_laplace() solve: the Student-t degrees of freedom (family = "t", default 4 when NULL) or the Tweedie variance power (family = "tweedie", required – a defaulted power would be a statistical decision the caller never made). A phi2 supplied for any other family errors rather than being ignored. It is conditioned on, never estimated: estimate_phi covers phi alone.

prior_sigma, eta

Hyperparameters of the PC + LKJ prior used when hyperprior = "pc_lkj" (see re_cov_pc_lkj_prior()). Ignored when hyperprior = "flat" or log_prior_theta is supplied. When active, the prior is part of the maximized objective, so it regularizes the estimate: with few groups it is what keeps a block off the sigma = 0 boundary.

hyperprior

"flat" (default) or "pc_lkj". "flat" maximizes with log_prior_theta the zero function – an unpenalized maximum-marginal- likelihood estimate, which can reach the sigma = 0 boundary on small designs (see the "lower end of the search bracket" warning). "pc_lkj" builds the PC + LKJ prior from prior_sigma / eta, regularizing the estimate away from that boundary. Ignored when log_prior_theta is supplied. Must match hyperprior on the paired tulpa_re_cov_nested() call for the two to share theta_hat.

log_prior_theta

Optional ⁠function(theta)⁠ returning a scalar log prior density on the full stacked parameter vector, overriding hyperprior entirely. Default NULL, which defers to hyperprior.

beta_prior

Optional Gaussian prior on the fixed effects, threaded into every inner tulpa_laplace() solve (list(mean, sd)).

offset

Optional observation-level offset on the linear predictor (length length(y)), e.g. log(exposure) for a rate model. Not supported with n_quad > 1, which errors rather than dropping it.

n_quad

Quadrature order for the inner marginal. 1 (default) uses the joint-field Laplace inner solve. ⁠> 1⁠ refines it with n_quad-point adaptive Gauss-Hermite quadrature, which requires a single shared grouping factor; see tulpa_re_cov_nested().

marginal

Report fixed-effect intervals that carry the hyperparameter uncertainty, instead of the intervals conditional on theta_hat. The posterior for theta is taken as Gaussian around the maximizer with covariance solve(H_theta), the inner mode is linearized in theta, and the law of total variance adds ⁠J solve(H_theta) J'⁠ to the conditional covariance, where ⁠J = d mode / d theta⁠. Both H_theta and J come from one central-difference stencil over the outer objective, costing ⁠1 + 2k^2⁠ further inner solves for k hyperparameter coordinates (k is 1 for a scalar (1 | g) block and 3 for a correlated (1 + x | g) one). Default FALSE. Widens intervals; never narrows them. When the variance components themselves are the target, or the correction's two approximations look strained (a strongly skewed variance-component marginal), integrate with tulpa_re_cov_nested() instead.

estimate_phi

Estimate the family's dispersion alongside the random-effect covariances, instead of conditioning on phi. When TRUE the supplied phi is the starting value and fit$phi is the estimate, with fit$phi_estimated distinguishing the two cases. log(phi) joins the maximization as one further coordinate, carrying the exact derivative of the Laplace log-marginal with respect to it, so the cost is one more coordinate for BFGS and not a second optimization.

The dispersion enters unpenalized – the hyperprior covers the covariance coordinates only – so this is the ML-II estimate of phi, not a MAP under an undeclared prior.

Available for every family carrying a dispersion, which is every front-door family except poisson, binomial and truncated_poisson – those have no free dispersion at all, so estimating one is a category error rather than a missing feature, and it is refused. Needs n_quad = 1.

Alongside X_zi both mixture kinds are covered. A hurdle (a zero-truncated base) has zero branch log(pi), which carries no dispersion, so the base family's registered derivative is already the mixture's. Genuine zero inflation has zero branch ⁠log(pi + (1 - pi) P(Y = 0))⁠, which depends on phi through P(Y = 0) and couples it to both linear predictors; that branch is supplied for neg_binomial_2, the only untruncated mixture family here with a free dispersion. Other untruncated bases are refused rather than handed the base derivative under a model it does not describe.

X_zi

Optional zero-inflation design matrix (length(y) rows), making the model a two-process mixture: each observation is a structural zero with probability ⁠plogis(X_zi beta_zi)⁠ and otherwise follows family. Paired with a zero-truncated family it is the hurdle model. The random effects enter the count predictor only, and the maximization is over the same covariance coordinates – the mixture changes the inner solve, not the outer objective's parameters. The ZI coefficients are reported alongside the count ones in coef() / vcov(), so the fixed block is ncol(X) + ncol(X_zi) wide. Needs n_quad = 1: the adaptive Gauss-Hermite inner marginal runs through a single-predictor oracle.

zi_prior_sd

Prior SD on beta_zi, keeping the logit identified where a level carries no zeros (the likelihood alone would send it to -Inf). Ignored when X_zi is NULL.

control

A named list of numerical knobs: max_iter, tol, n_threads (inner-solve controls, see tulpa_laplace()), and outer_maxit (iteration budget for the maximization over Sigma, default 500; applies to the Nelder-Mead simplex used from two parameters up, since the one-parameter case is bracketed by Brent). Exhausting the budget warns. outer_reltol sets that maximization's convergence tolerance (default 1e-10 for the gradient-driven methods, 1e-8 for the simplex, which cannot resolve as finely); it is converted to L-BFGS-B's factr on the bounded path, so one request means the same thing whichever method runs. sigma_init supplies the starting random-effect SD – a scalar, or one per coefficient across all blocks – replacing the method-of-moments guess taken from a pilot fit at Sigma = I. Worth setting when the true scale is far from 1, where that pilot starts the search on a flat stretch, and when a run should be reproducible from its inputs rather than from a pilot fit. It is diagonal: it sets each coefficient's scale and leaves any correlation to be fitted. Two further knobs tune marginal = TRUE and are inert without it: marginal_step (the stencil step in theta space, default 1e-3) and marginal_richardson (default FALSE; evaluate the stencil at step and step / 2 and extrapolate, turning the O(step^2) truncation error into O(step^4) at twice the solves – worth it only when the inner solver's own noise sits well below the truncation error, i.e. a tight tol).

Details

Blocks, coordinates and the default hyperprior are exactly those of tulpa_re_cov_nested() – a correlated (1 + x | g) term is a full ⁠Sigma = L L'⁠ in log-Cholesky coordinates, an uncorrelated (1 + x || g) term is diagonal in log-SD coordinates, and a scalar (1 | g) term is the degenerate one-coefficient block. Both functions call the same outer objective and the same optimizer, so tulpa_eb()$theta_hat and tulpa_re_cov_nested()$theta_hat are the same estimate on the same data – which requires hyperprior to default the same way on both: "flat", the zero function, matching the nested-Laplace convention on every other scale axis in the engine (icar / rw1 / rw2 / ar1's tau / iid all lack a hyperprior on their scale too; see vignette("priors")). Set hyperprior = "pc_lkj" for the weakly-informative PC + LKJ prior instead (see re_cov_pc_lkj_prior()) – the regularizer that, at small G, keeps a block off the sigma = 0 boundary this maximizer would otherwise reach.

The reported fixed-effect covariance is the conditional one at theta_hat (solve(H_beta)). It does not include the hyperparameter uncertainty that tulpa_re_cov_nested() integrates over, so EB intervals are narrower – increasingly so as the number of groups falls. Use the nested integrator when the variance components themselves, or calibrated fixed-effect intervals, are the target; use EB when the point estimate is, or as a fast starting fit.

Value

A tulpa_fit with:

References

Casella (1985). An introduction to empirical Bayes data analysis. The American Statistician 39(2):83-87. Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392.

See Also

tulpa_re_cov_nested() to integrate over Sigma rather than fix it; tulpa_laplace() for the inner solve.

Examples


set.seed(1)
G <- 30L; per <- 10L; n <- G * per
grp <- rep(seq_len(G), each = per); x <- rnorm(n)
b <- rnorm(G, 0, 0.8)
y <- rpois(n, exp(0.3 + 0.5 * x + b[grp]))
re_term <- list(idx = grp, n_groups = G, n_coefs = 1L)
fit <- tulpa_eb(y, NULL, cbind(1, x), re_term, family = "poisson")
fit$map$sigma          # empirical-Bayes RE standard deviation


Fit a latent-variable model via EM + Laplace approximation

Description

Generic EM engine. Model packages provide callbacks for the E-step (latent variable posterior) and the M-step encoding (how to assemble submodel data from weights). Each M-step submodel block is fit independently via tulpa_laplace(); the engine reads family and offset per block so heterogeneous-family mixtures (e.g. a binomial zero submodel + poisson positive submodel for a hurdle model) work without engine changes.

Usage

tulpa_em_laplace(
  e_step,
  m_step_encode,
  spatial = NULL,
  re_list = list(),
  max_iter = 50L,
  tol = 1e-04,
  damping = 0.3,
  correction = c("auto", "mi", "gibbs", "none"),
  n_imputations = 20L,
  n_gibbs = 10L,
  draw_z = NULL,
  m_step_extra = NULL,
  beta_prior = NULL,
  verbose = TRUE,
  ...
)

Arguments

e_step

Callback: ⁠function(fits, ...) -> list(weights = numeric, ...)⁠. Called once per EM iteration with the current per-submodel tulpa_laplace() fits. Must return a list whose weights element is the latent-variable posterior used by the next M-step.

m_step_encode

Callback: ⁠function(weights, ...) -> list of blocks⁠. Each block is itself a list with the following fields:

  • y (numeric, required) – response.

  • X (matrix, required) – fixed-effects design with nrow(X) == length(y).

  • family (character scalar, required) – one of "binomial", "poisson", "gaussian", "negbin" / "neg_binomial_2", "gamma", "beta". Forwarded to tulpa_laplace() for that block.

  • n_trials (numeric or NULL, optional; absent or NULL defaults to 1) – binomial trial counts.

  • offset (numeric or NULL, optional) – observation-level offset, length-matched to y when non-NULL.

  • phi (numeric scalar, optional) – dispersion forwarded to tulpa_laplace() (used by negbin, gamma).

  • weights (numeric, optional; length length(y)) – per-observation likelihood weight, scaling that row's log-density, score and Fisher information. This is the channel a soft (fractional) latent label travels on; see the section below.

  • re_list, spatial (optional) – forwarded as-is.

spatial, re_list

Not consumed at the driver level – latent structure is per-submodel, set as a spatial / re_list field on each block m_step_encode() returns. Supplying either here is an error (rather than a silent no-op).

max_iter

Maximum EM iterations.

tol

Convergence tolerance on max relative parameter change.

damping

EM damping factor in ⁠[0, 1)⁠. With damping = d, the E-step weights are smoothed between iterations as (1 - d) * new + d * prev (d = 0 is no damping); the M-step then refits on the smoothed weights, so the parameter update is damped indirectly through the weights rather than by mixing successive parameter vectors.

correction

Post-EM correction. "none" returns the EM point estimate only. "mi" draws n_imputations independent hard z from the converged posterior weights P(z|y, theta_hat), refits each block on the hard z, and pools via rubins_pool(). "gibbs" runs a warm-started z|theta -> theta|z Markov chain of length n_gibbs starting from the EM fits, also pooled via rubins_pool(). "auto" resolves to "none".

n_imputations

Number of MI draws (default 20L). Used when correction = "mi".

n_gibbs

Length of the Gibbs chain (default 10L). Used when correction = "gibbs".

draw_z

Optional function ⁠function(weights) -> hard_z⁠ that turns the E-step's continuous weights into a hard latent draw. Used only by correction %in% c("mi", "gibbs"). The default treats weights as a numeric vector of Bernoulli probabilities and draws per-observation. Multi-class / matrix-valued latent structures must supply their own callback.

m_step_extra

Optional ⁠function(fits, weights, ...) -> fits⁠. Fired once per M-step in every phase (EM iterations, MI draws, Gibbs steps). Receives the freshly assembled list of tulpa_laplace() results (fits), the continuous E-step weights P(z|y, theta) (NOT the hard z draw used by MI/Gibbs to encode the block), and any extra arguments forwarded through .... Returns a list with the same length and names as the input, possibly with mutated dispersion / shape / precision fields (e.g. fits[[k]]$phi). Use this to update non-eta parameters that fall out of the Laplace M-step (NB overdispersion, Gamma shape, Beta precision, Gaussian sigma). When NULL (default), behavior is unchanged.

beta_prior

Optional Gaussian prior on the fixed effects, applied to every block fit via tulpa_laplace() (i.e. blocks without a prior field). NULL (default) keeps the weak built-in prior. Otherwise a list with sd (required) and optional mean; see tulpa_laplace(). The same prior flows into the MI / Gibbs correction refits, so penalized corrections come for free. A block may override the default by setting its own beta_prior field in m_step_encode (e.g. different priors for the occupancy and detection submodels). Use scalar mean / sd here when blocks differ in width; per-coefficient vectors belong on the block.

verbose

Print per-iteration progress.

...

Forwarded to e_step, m_step_encode, and m_step_extra.

Details

This is an engine block, not a front door: model packages call it programmatically, so its tuning knobs (max_iter, tol, damping) sit in the signature rather than in a control list.

Value

A list with:

Soft latent labels go in weights, not in y

The M-step maximizes the expected complete-data log-likelihood. For a Bernoulli latent z_i carrying E-step posterior weight w_i that is

Q = \sum_i [\, w_i \log p_i + (1 - w_i) \log (1 - p_i) \,],

a weighted Bernoulli log-likelihood, which carries no binomial coefficient. Encode it as two rows per unit – y = 1 at weight w_i and y = 0 at weight 1 - w_i:

list(y       = rep(c(1, 0), each = n),
     X       = rbind(X, X),
     weights = c(w, 1 - w),
     family  = "binomial")

A fractional y on a binomial block is refused, and is not the same objective: it asks for the exact binomial density at a non-integer response, whose normalizer lchoose(n, y) is not zero there and depends on w. On the weighted encoding the block's log_marginal is the Laplace marginal of Q, an EM objective that increases across iterations.

Examples


# Zero-inflated Poisson via EM: the E-step scores the posterior probability
# that each zero is non-structural. Those are soft labels, so the occupancy
# arm is the weighted Bernoulli above -- two rows per unit -- and the
# abundance arm weights each count by the same w.
set.seed(1)
n <- 200
z <- rbinom(n, 1, 0.7)
y <- rpois(n, 4) * z
X <- cbind(1, rnorm(n))

e_step <- function(fits, ...) {
  if (!length(fits)) return(list(weights = pmax(as.numeric(y > 0), 0.5)))
  psi <- plogis(drop(X %*% fits$occ$mode))
  lam <- exp(drop(X %*% fits$abund$mode))
  w <- ifelse(y > 0, 1, psi * exp(-lam) / (psi * exp(-lam) + (1 - psi)))
  list(weights = w)
}
m_step_encode <- function(weights, ...) {
  list(
    occ   = list(y = rep(c(1, 0), each = n), X = rbind(X, X),
                 weights = c(weights, 1 - weights), family = "binomial"),
    abund = list(y = y, X = X, family = "poisson", weights = weights)
  )
}

res <- tulpa_em_laplace(e_step, m_step_encode, verbose = FALSE)
res$converged
plogis(res$fits$occ$mode[1])    # P(non-structural), truth 0.7
exp(res$fits$abund$mode[1])     # abundance mean, truth 4


Generic Monte-Carlo EM driver

Description

Same M-step plumbing as tulpa_em_laplace(): every iteration calls m_step_encode(weights, ...) to assemble per-submodel blocks and fits each block via tulpa_laplace(). The difference is the E-step: instead of computing closed-form weights, an e_step_sample callback returns n_mc weight draws per iteration. Each draw is run through the M-step independently and the resulting parameter estimates are pooled via rubins_pool().

Convergence criterion is the max relative change in pooled M-step parameter estimates between iterations. To increase Monte-Carlo accuracy as iterations progress (Booth-Hobert ascent-based MCEM), set n_mc_growth > 1.

This is an engine block, not a front door: model packages call it programmatically, so its tuning knobs (max_iter, tol, n_mc) sit in the signature rather than in a control list.

Usage

tulpa_em_mc(
  e_step_sample,
  m_step_encode,
  n_mc = 10L,
  n_mc_growth = 1,
  n_mc_max = 200L,
  max_iter = 30L,
  tol = 0.001,
  verbose = TRUE,
  ...
)

Arguments

e_step_sample

Function ⁠function(fits, n_mc, ...) -> list⁠. Must return a list of length n_mc, each element a weights object of the same shape m_step_encode consumes (typically a numeric vector of length n_obs, or a matrix ⁠n_obs x K⁠ for K-class latent variables). On the first iteration fits is list() – the callback should return draws from the prior.

m_step_encode

Function ⁠function(weights, ...) -> list of blocks⁠. Identical to the contract used by tulpa_em_laplace(): each block is a list with required fields y, X, family, and optional n_trials, offset, phi, re_list, spatial, weights. See ?tulpa_em_laplace for the full spec.

n_mc

Initial number of Monte-Carlo draws per iteration (default 10L).

n_mc_growth

Multiplicative growth of n_mc per iteration (default 1.0 = constant). Set ⁠> 1⁠ for ascent-based MCEM that ramps up MC accuracy near convergence.

n_mc_max

Cap on n_mc (default 200L).

max_iter

Maximum EM iterations (default 30L).

tol

Convergence tolerance on max relative change in pooled parameter estimates (default 1e-3). Looser than tulpa_em_laplace default because Monte-Carlo noise floors the achievable precision.

verbose

Print per-iteration progress (default TRUE).

...

Forwarded to e_step_sample and m_step_encode.

Value

A list with:

Tier

Inherits the tier of the inner M-step (Laplace => Tier 2). The Monte-Carlo E-step itself is exact in the limit n_mc -> infinity, so as a full pipeline MCEM is asymptotically Tier 1 if e_step_sample is exact.

References

Wei, G. C. G., & Tanner, M. A. (1990). A Monte Carlo implementation of the EM algorithm and the poor man's data augmentation algorithms. Journal of the American Statistical Association, 85(411), 699-704.

Booth, J. G., & Hobert, J. P. (1999). Maximizing generalized linear mixed model likelihoods with an automated Monte Carlo EM algorithm. JRSS B, 61(1), 265-285.

See Also

tulpa_em_laplace() for the closed-form-weights variant, rubins_pool() for the pooling rule.

Examples

## Not run: 
# Monte-Carlo EM from two callbacks: e_step_sample() draws the latent
# variables and m_step_encode() encodes the complete-data design for the inner
# Laplace M-step (model packages such as tulpaObs supply these). See
# ?tulpa_em_laplace for the deterministic-E-step analogue.
fit <- tulpa_em_mc(e_step_sample, m_step_encode)

## End(Not run)

Expectation-Propagation fit for a GLM

Description

Fits a generalized linear model with a Gaussian coefficient prior by Expectation Propagation: the posterior is approximated by a Gaussian whose per-observation sites match the moments of the tilted distribution (via Gauss-Hermite quadrature). EP is exact for a Gaussian likelihood and typically more accurate than Laplace on skewed likelihoods, since it matches marginal moments rather than the mode curvature.

Usage

tulpa_ep(
  formula,
  data,
  family = "binomial",
  phi = 1,
  phi2 = NULL,
  n_trials = NULL,
  beta_prior = .tulpa_default_beta_prior("ep"),
  control = list()
)

Arguments

formula

Model formula.

data

A data frame.

family

Character family name (see family_names()).

phi

Dispersion / precision passed to the family (held fixed).

phi2

Optional second dispersion (Student-t degrees of freedom for family = "t"; default 4 when NULL).

n_trials

Binomial denominators (length nrow(data)), or NULL (= 1).

beta_prior

Fixed-effect prior as list(mean, sd): a mean-zero (mean = 0) Gaussian on every coefficient with SD sd (default the engine default, prior_normal(0, 2.5)). EP's site parameterisation assumes a mean-zero coefficient prior, so a non-zero mean errors – use a sampler (mode = "mala") for a shifted prior.

control

List: max_sweeps (default 50), tol (default 1e-6), damping (default 0.8), n_quad (Gauss-Hermite nodes, default 20), n_draws (default 2000), seed.

Value

A tulpa_fit (subclass tulpa_ep) with coefficients (posterior mean), vcov, draws, log_marginal (the EP approximation), converged.

References

Minka (2001). Expectation Propagation for approximate Bayesian inference. UAI. Rasmussen & Williams (2006). Gaussian Processes for Machine Learning, Algorithm 3.5.

See Also

tulpa() (Laplace / sampler tiers), pathfinder() (VI).

Examples


set.seed(1)
d <- data.frame(x = rnorm(200))
d$y <- rbinom(200, 1, plogis(-0.3 + 0.8 * d$x))
fit <- tulpa_ep(y ~ x, data = d, family = "binomial")
coef(fit)


Construct a minimal tulpa_family for simulation

Description

Lightweight constructor for a tulpa_family object that exposes the contract required by prior_predict() and tulpa_simulate(). Model packages (tulpaRatio, tulpaObs) register richer families that also link to C++ likelihoods; this helper is for tests and simple custom families that only need simulation.

Usage

tulpa_family(
  name,
  simulate_fn,
  process_names = "y",
  extra_params = list(),
  link_inv = NULL
)

Arguments

name

Family name (character, length 1).

simulate_fn

⁠function(eta, params, n_obs, ...)⁠ returning a numeric vector of length n_obs (single-process) or a list of such vectors keyed by process_names (multi-process). eta is a list of linear predictors, one per process.

process_names

Character vector. Defaults to "y" (single-process).

extra_params

Named list of tulpa_prior objects for likelihood- specific scalar parameters (e.g., dispersion phi). Drawn at each prior-predictive iteration. Defaults to empty.

link_inv

List of inverse-link functions per process; defaults to identity for every process. tulpa passes the raw linear predictor to simulate_fn, so most families implement the link inside simulate_fn (e.g., mu = exp(eta) for Poisson). The link_inv slot exists for families that prefer to keep the inverse-link separate.

Value

A tulpa_family object.

Examples

fam <- tulpa_family(
  name = "poisson",
  simulate_fn = function(eta, params, n_obs, ...) {
    rpois(n_obs, exp(eta[[1]]))
  }
)


Formula parsing for tulpa models

Description

Parses mixed-model formulas by walking the formula's abstract syntax tree. R formulas are already parse trees – we do structural recursion to find random effect terms (| nodes) and separate them from fixed effects.

Value

The formula helpers documented in this family return parsed-formula structures (lists describing the fixed effects, random-effect terms, and latent blocks); see each function's own help page.


Fit a Gaussian linear model via tulpa's generic engine

Description

Proof-of-concept function demonstrating the tulpa generic interface. Fits y ~ Normal(X * beta, sigma) with HMC sampling.

Usage

tulpa_gaussian(
  formula,
  data,
  beta_prior = .tulpa_default_beta_prior("gaussian"),
  control = list()
)

Arguments

formula

A formula (e.g., y ~ x1 + x2)

data

A data frame

beta_prior

Fixed-effect prior as list(mean, sd): a mean-zero (mean = 0) Gaussian on every coefficient with SD sd (default the engine default, prior_normal(0, 2.5)).

control

List of numerical / sampler knobs: iter (total iterations, default 2000), warmup (default 1000), step_size (HMC step size, default 0.05), n_leapfrog (default 10), seed (NULL draws from the session RNG).

Value

A list with draws matrix, posterior means, and metadata


Fit via Polya-Gamma Gibbs sampler

Description

Public API for PG Gibbs sampling. Used by model packages for binomial and negative binomial GLMMs.

Usage

tulpa_gibbs(
  y,
  n_trials,
  X,
  group,
  n_groups,
  family = "binomial",
  beta_prior = .tulpa_default_beta_prior("gibbs"),
  prior_sigma_scale = 2.5,
  spatial = NULL,
  temporal = NULL,
  control = list()
)

Arguments

y

Response vector

n_trials

Trial sizes (binomial)

X

Design matrix

group

Integer vector of group indices (1-based)

n_groups

Number of groups

family

Character: "binomial" or "neg_binomial_2"

beta_prior

Fixed-effect prior as list(mean, sd): a mean-zero (mean = 0) Gaussian on every coefficient with SD sd (default the engine default, prior_normal(0, 2.5)). The Polya-Gamma sampler uses a mean-zero prior, so a non-zero mean errors.

prior_sigma_scale

Prior scale for RE sigma (statistical; default 2.5).

spatial

Optional spatial spec. When supplied the fit routes to the matching spatial Polya-Gamma Gibbs sampler via dispatch_gibbs_spatial(); group/n_groups are the iid random-effect block carried alongside the field. The full areal + continuous family is available for family = "binomial"; family = "neg_binomial_2" is backed by the areal ICAR negbin sampler only. Supported types:

  • areal – "icar", "bym2", "rsr": a list with type, adjacency and a 1-based spatial_idx per observation (e.g. list(type = "icar", adjacency = W, spatial_idx = unit)). "rsr" reuses spatial$rsr_projection if present, else builds the unit-level projector from the design.

  • continuous – "gp"/"nngp" (a validated spatial_gp() spec) and "multiscale_gp" (a validated spatial_multiscale() spec). These samplers carry no observation->location map, so they require one observation per unique location in coordinate order.

temporal

Optional temporal spec: a validated temporal_multiscale() object. Routes to the multiscale temporal Polya-Gamma sampler via dispatch_gibbs_temporal() (binomial only; RW1 trend + cyclic seasonal + AR1/IID short-term). Cannot be combined with spatial.

control

A named list of numerical / tuning knobs (statistical arguments stay in the signature above): n_iter (default 2000), warmup (default 1000), thin (default 1, applied on every route including the spatial and temporal ones; the run keeps ceiling((n_iter - warmup) / thin) draws), seed (NULL draws from the session RNG; the Polya-Gamma kernels use R's RNG, so a seed makes the fit reproducible), verbose (default FALSE), n_threads (default 1).

Details

For family = "neg_binomial_2" the Polya-Gamma weights are drawn at the exact real shape PG(y + r, eta) and the dispersion r is updated by a random-walk Metropolis-Hastings step on log(r) whose stationary support is bounded to r in ⁠[0.1, 500]⁠; data favouring a dispersion outside that range pile up at the boundary.

Every sampler that centres a latent effect – the negative-binomial kernels, and the binomial kernels carrying a spatial or temporal field – adds the removed level to the first coefficient, which leaves eta unchanged only when the first column of X is an all-ones intercept. Those routes error on a design without one.

Value

List with beta draws, RE draws, sigma_re draws (plus the spatial field draws when spatial is supplied)

Examples

set.seed(1)
G <- 20L; npg <- 15L; n <- G * npg
grp <- rep(seq_len(G), each = npg)
X <- cbind(1, rnorm(n))
b <- rnorm(G, 0, 0.6)
y <- rbinom(n, 1, plogis(X %*% c(-0.2, 0.5) + b[grp]))

fit <- tulpa_gibbs(y, rep(1L, n), X, grp, G, family = "binomial",
                   control = list(n_iter = 500L, warmup = 250L))
colMeans(fit$beta)


Outer hyperparameter-grid integration with a user-supplied inner fit

Description

Generic driver for nested-Laplace-style outer integration over a small hyperparameter block. The user supplies per-axis specs (values + optional log-prior + log-scale / bounds / refinable metadata) and an inner_fit callback that, at every hyperparameter cell, returns the inner log marginal and optionally the fixed-effect posterior mode + marginal covariance. The driver builds the Cartesian outer grid, normalises the log-marginals to integration weights, reports per-axis posterior moments and weighted quantiles, and (when the inner fit supplies them) law-of-total-covariance fixed-effect posterior + mixture draws.

This factors the per-family outer-grid plumbing in tulpa_nested_laplace() / tulpa_nested_laplace_joint() / tulpa_re_cov_nested() into one callback-driven entry point: downstream consumers (occupancy / N-mixture / cover hurdle families in tulpaObs, custom user families) drop in their own per-cell inner fit and get the outer integration for free.

Usage

tulpa_hyper_grid(
  hyper_specs,
  inner_fit,
  combine = c("law_of_total_cov", "weighted_mean_only", "none"),
  n_draws = 2000L,
  seed = NULL,
  beta_names = NULL,
  control = list()
)

Arguments

hyper_specs

A list of axis specs. Each spec is either a hyper_axis_spec() object or a plain list with the same fields (the driver auto-wraps); see hyper_axis_spec(). The outer grid is the Cartesian product of the per-axis grids; the joint log-prior is the sum of per-axis log_prior contributions (axes with log_prior = NULL are flat / improper).

inner_fit

⁠function(hypers)⁠ returning a list with:

  • log_marginal – scalar; the inner-solve log marginal at this cell. Non-finite values are mapped to -Inf (the cell gets zero weight).

  • beta_mean – numeric vector of fixed-effect posterior mean at the cell. Required when combine != "none".

  • beta_cov⁠p x p⁠ numeric matrix of fixed-effect marginal covariance at the cell. Required when combine = "law_of_total_cov". hypers is a named numeric vector with the current cell's axis values (names match ⁠vapply(hyper_specs, ⁠[[⁠, character(1), "name")⁠). Errors thrown by inner_fit are caught and treated as a failed cell (log_marginal = -Inf, no beta contribution).

combine

How to pool per-cell fixed-effect posteriors into a single posterior over the betas. One of:

  • "law_of_total_cov" (default) – compute E[Cov(beta | theta)] + Cov(E[beta | theta]) from the per-cell ⁠(beta_mean, beta_cov)⁠; synthesise n_draws posterior draws by mixture sampling. Requires beta_mean and beta_cov per cell.

  • "weighted_mean_only" – pool only the per-cell beta_mean into the weighted posterior mean. beta_cov is ignored; no draws.

  • "none" – do not assemble a fixed-effect posterior. Only the hyperparameter posterior is returned. beta_mean / beta_cov from inner_fit are ignored if supplied.

n_draws

Number of posterior draws of the fixed effects to synthesise from the cell mixture (default 2000). Used only when combine = "law_of_total_cov". 0 disables draw synthesis (the law-of-total-cov mean and covariance are still returned).

seed

Optional integer seed for the draw synthesis.

beta_names

Optional character vector naming the fixed-effect coordinates. When NULL (default) the names are taken from the first successful cell's beta_mean (or beta1, beta2, ... if it is unnamed).

control

Optional list of refinement / tuning knobs:

  • adaptive_grid (FALSE) – run the boundary / interior refinement pass on every axis whose spec has refinable = TRUE. New cells are appended along the refining axis paired with the boundary modal cell's other-axis values, carrying a marginal-scale calibration so they contribute on the right scale.

  • adaptive_grid_edge_thresh (0.02) – per-axis trigger threshold.

  • adaptive_grid_max_passes (1L) – cap on refinement passes.

  • var_of_means_consistency (FALSE) – run a post-integration consistency pass: for refinable axes whose joint-grid var-of-means undershoots the Laplace-at-mode SD by more than tolerance, append Laplace-guided slice points at ⁠theta_mean +/- {0.7, 1.5} * theta_sd⁠ pinned at the modal cell. One kernel call per axis.

  • var_of_means_tolerance (0.7) – consistency-pass trigger ratio.

Value

A list of class c("tulpa_hyper_grid", "tulpa_fit", "list") with:

See Also

hyper_axis_spec() for the axis-spec constructor; tulpa_nested_laplace_joint() for the family-specific outer-grid driver that this helper generalises.

Examples

## Not run: 
# Integrate an inner fit over a hyperparameter grid: inner_fit(theta) returns
# a per-cell fit and hyper_specs names the axes. tulpa_nested_laplace() is the
# packaged driver built on this.
res <- tulpa_hyper_grid(hyper_specs, inner_fit)

## End(Not run)

Select the symplectic integrator for HMC and NUTS

Description

Get or set the symplectic splitting integrator that the exact-MCMC tier (HMC / NUTS) uses to build its trajectory proposals. The integrator is backed by the SIMP library, which supplies leapfrog and the higher-order Yoshida members from one triple-jump composition.

Usage

tulpa_integrator(name, mts_substeps = 4L)

Arguments

name

Integrator name: "leapfrog" (default), "minerror2", "adaptive2", "adaptive3", "mts", "yoshida4", "yoshida6", or "yoshida8". Omit to query the current selection without changing it.

mts_substeps

Number of inner prior-force substeps per trajectory step for the "mts" integrator (default 4). Ignored by the other integrators.

Details

The default, "leapfrog", reproduces tulpa's historical trajectory step exactly.

"minerror2" is a two-stage order-two scheme whose coefficient is tuned to cancel the leading energy error on a Gaussian target. Since mass adaptation drives a posterior toward an isotropic Gaussian, it conserves energy well near the adapted optimum and adapts to larger step sizes, at two gradient evaluations per step. It is the recommended choice when leapfrog's step size is limited by energy error on a near-Gaussian posterior.

"yoshida4" is an order-4 scheme that also samples reliably (three gradient evaluations per step). "yoshida6" and "yoshida8" are available but experimental for NUTS: high-order composition integrators have a sharp step-size stability threshold that the dual-averaging adaptation pushes against, so "yoshida6" needs a high adapt_delta (0.95 or more) and "yoshida8" often fails to adapt. For sampling, prefer "leapfrog", "minerror2", or "yoshida4".

"adaptive2" and "adaptive3" are step-adapted versions of the two- and three-stage schemes. Rather than fixing the coefficient in advance, each NUTS chain resolves it at the end of warmup for its own operating point: the coefficient that minimizes the worst-case energy error over the band of dimensionless steps the chain actually takes, read off from the adapted mass matrix and the local posterior curvature. Where "minerror2" is optimal only in the small-step limit, "adaptive2" tracks the chain's realised step band; "adaptive3" spends a third gradient per step to hold a small error over a wider band. Both run a fixed placeholder of the same stage family during warmup, so the dual-averaged step size carries over. Step-adaptation applies to NUTS (the default sampler); the fixed-trajectory HMC path uses the placeholder.

"mts" is a multiple-time-stepping (RESPA) integrator. It splits the force into a stiff but cheap prior part – the Gaussian latent structure – and a smooth but expensive likelihood part. Each NUTS trajectory step takes mts_substeps inner leapfrog substeps against the prior force while evaluating the likelihood gradient only once, so the outer step can be larger without the stiff prior forcing it small. It pays one full gradient per step (as leapfrog does) plus mts_substeps cheap prior gradients, and helps most when the latent field is stiff relative to a comparatively flat likelihood. Like the other schemes it applies to NUTS.

The choice is process-global (like the gradient mode): set it once before fitting. It is read on the main thread at the start of sampling. Because it is process-global, a caller that changes it owns restoring it – and an error between the two calls would leave the process on the other integrator. with_tulpa_integrator() does both, restoring on error as well as on success.

Value

If name is omitted, the current integrator name. If name is given, the previous name is returned invisibly.

Examples

tulpa_integrator()            # current integrator
old <- tulpa_integrator("yoshida4")
tulpa_integrator(old)         # restore


Recognize an inline varying-coefficient bar

Description

Test whether a one-sided formula carries a single varying-coefficient grouping bar of the form ~ 1 + w || node (independent fields, ||) or ~ 1 + w | node (correlated fields, |). This is the same grammar spatial() and the inline temporal() field constructor accept, and the grammar tulpa_bar_field_specs() expands. A downstream package can use it to branch on "is this term a spatial / temporal varying-coefficient bar?" before calling tulpa_bar_field_specs().

Usage

tulpa_is_spatial_bar(x)

Arguments

x

A one-sided formula (e.g. ~ 1 + w || node) or the bar language object itself (quote(1 + w || node)).

Value

A single logical: TRUE when x is (or wraps) a | / || bar, FALSE otherwise (a plain formula such as ~ 1 + w, a non-bar term, or a non-formula / non-language input).

See Also

tulpa_bar_field_specs() for expanding the bar into per-column field specs.

Examples

tulpa_is_spatial_bar(~ 1 + w || cell)   # TRUE
tulpa_is_spatial_bar(~ 1 + w | cell)    # TRUE (correlated)
tulpa_is_spatial_bar(~ 1 + w)           # FALSE (no bar)


K-fold cross-validation for a tulpa fit

Description

Splits the data into K folds, refits the model on each K - 1 training partition (via the fit's stored tulpa() call), and accumulates the held-out fold's pointwise log predictive density \log \frac{1}{S}\sum_s p(y_i \mid \eta_i^{(s)}) over the training posterior draws. The summed elpd_kfold is directly comparable to the elpd_loo from tulpa_criteria() – the exact refit counterpart to the PSIS-LOO approximation, for when the Pareto k-hat gate flags LOO as unreliable.

Fixed-effect / GLMM fits only: subsetting the observations breaks a spatial or temporal field, so those fits are rejected (use PSIS-LOO via tulpa_criteria()). Held-out random-effect groups contribute at their prior mean (population-level held-out prediction), matching predict().

Usage

tulpa_kfold(object, data, K = 10L, folds = NULL, n_trials = NULL, seed = NULL)

Arguments

object

A tulpa_fit fitted through tulpa() (must carry ⁠$call⁠).

data

The data frame the model was fit to.

K

Number of folds (default 10).

folds

Optional integer vector of fold ids, length nrow(data); a random balanced partition is drawn when NULL.

n_trials

Optional binomial denominators (length nrow(data)); defaults to the trials stored on the fit, else 1 (Bernoulli). Each fold's refit receives the training rows' trials, and the held-out density is scored at the test rows' trials.

seed

Optional seed for the random partition.

Value

A list with elpd_kfold (summed held-out elpd), se_elpd_kfold (its standard error), pointwise (per-observation held-out elpd), folds, and K.

See Also

tulpa_criteria() for PSIS-LOO / WAIC on a single fit.

Examples


set.seed(1)
d <- data.frame(x = rnorm(120))
d$y <- rpois(120, exp(0.4 + 0.6 * d$x))
fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace")
cv  <- tulpa_kfold(fit, data = d, K = 5, seed = 1)
cv$elpd_kfold


Fit a model via Laplace approximation

Description

General-purpose Laplace approximation for latent Gaussian models. Finds the mode of the latent field (beta + random effects) and returns the Laplace-approximated marginal likelihood.

This is the public API for model packages (tulpaGlmm, tulpaObs, etc.) to call tulpa's Laplace engine. As the low-level engine entry point (not a front-door fitter), it keeps its numerical controls (max_iter, tol, n_threads, return_hessian) inline in the signature rather than in a control list, so callers assembling many solves pass them positionally.

Usage

tulpa_laplace(
  y,
  n_trials,
  X,
  re_list = list(),
  family = "binomial",
  phi = 1,
  phi2 = NULL,
  spatial = NULL,
  weights = NULL,
  offset = NULL,
  max_iter = 100L,
  tol = 1e-06,
  n_threads = 1L,
  return_hessian = TRUE,
  beta_prior = NULL,
  return_re_cov = FALSE,
  X_zi = NULL,
  zi_prior_sd = 2.5,
  return_joint_hessian = FALSE,
  compute_skew = FALSE,
  skew_idx = NULL,
  debias = NULL
)

Arguments

y

Response vector (integer for binomial/poisson/negbin, numeric for gaussian)

n_trials

Trial sizes (integer vector, used for binomial only)

X

Fixed-effects design matrix

re_list

List of RE specifications. Each element is a list with:

  • idx: integer vector of group indices (1-based)

  • n_groups: number of groups

  • n_coefs: coefficients per group (1 = intercept-only, >1 = random slopes)

  • sigma: per-coefficient RE standard deviation(s), a diagonal covariance (uncorrelated, lme4 (x || g)). A scalar is recycled to n_coefs.

  • Z: slope design matrix (n_obs x n_coefs) when n_coefs > 1; NULL means intercept-only.

  • L / cov: optional ⁠n_coefs x n_coefs⁠ covariance for a correlated term (lme4 (1 + x | g)) – supply either a lower-triangular Cholesky factor L (covariance = ⁠L L'⁠) or the covariance matrix cov. When present these take precedence over sigma; the off-diagonal enters both the joint Hessian (mode finding) and the marginal fixed-effect SE.

family

Character: "binomial", "poisson", "neg_binomial_2", "gaussian"

phi

Dispersion parameter. For gaussian / lognormal this is the residual VARIANCE (matching the R-side family registry and tulpa()); the SD-parameterized compiled kernels receive sqrt(phi) internally. For neg_binomial_2 the size, beta the precision, t the scale.

phi2

Optional second dispersion: the Student-t degrees of freedom (family = "t"; default 4 when NULL). Non-spatial path only.

spatial

Optional spatial specification (tulpa_spatial object)

weights

Optional observation weights (numeric vector, length length(y)). Scales each observation's log-density, score and Fisher curvature by the same w_i, on the spatial route as well as the non-spatial one, so the mode and the marginal precision H_beta describe one weighted model. NULL (default) uses 1.

offset

Optional observation-level offset on the linear predictor (numeric vector, length length(y)). NULL (default) uses 0.

max_iter

Maximum Newton iterations (default 100)

tol

Convergence tolerance (default 1e-6)

n_threads

Number of threads (default 1)

return_hessian

Logical: return the fixed-effect Hessian block? (default TRUE)

beta_prior

Optional Gaussian prior on the fixed effects. NULL (default) keeps the weak built-in prior beta ~ N(0, 100^2). Otherwise a list with element sd (prior standard deviation, required) and optional mean (prior mean, default 0). Each may be a scalar (applied to every coefficient) or a length-ncol(X) vector. Adds sum((beta - mean)^2 / (2 * sd^2)) to the negative log-posterior, so the mode is the penalized (MAP) estimate. A coefficient's sd may be +Inf, which sets its precision to 0 (no penalty on that coefficient). Not supported on the spatial path.

return_re_cov

If TRUE, additionally return per-group marginal posterior covariance blocks Cov(u_g | y, Sigma) – one ⁠n_coefs x n_coefs⁠ matrix per (RE term, group), with the fixed effects and other groups marginalized out (each block is a diagonal block of the full inverse Hessian, not the inverse of a diagonal block). Used by the EM M-step for a full random-effect covariance. Non-spatial multi-RE path only.

X_zi

Optional zero-inflation design matrix (length(y) rows). When supplied the latent fixed-effect block becomes ⁠[beta_count | beta_zi]⁠ and the family's compiled zero-inflated kernel is used. Non-spatial path only.

zi_prior_sd

Prior SD on the zero-inflation coefficients, beta_zi ~ N(0, zi_prior_sd^2) (default 2.5, matching the samplers' ModelData::zi_prior_sd). It is what keeps the logit identified when a level contributes no zeros, where the likelihood alone drives beta_zi to -Inf. +Inf removes the penalty. Ignored when X_zi is NULL.

return_joint_hessian

If TRUE, additionally return H_joint: the full joint posterior precision of the latent field ⁠[beta | random effects]⁠ at the mode, as a symmetric sparse matrix. This is the matrix the Laplace approximation takes the determinant of, so it is what an exact derivative of the log-marginal has to differentiate through; H_beta is only its fixed-effect Schur complement. Costs one extra copy of the Hessian, so it is off by default. Non-spatial multi-RE path only.

compute_skew

If TRUE, additionally return the inner-Laplace reliability material at skew_idx: inner_skew (gamma_3, Rue Martino & Chopin 2009's cubic term), and the importance curve inner_is_z / inner_is_log_joint the inner Pareto-k-hat is fitted from. Costs one linear solve plus a fixed batch of objective evaluations per probed index. Non-spatial path only.

skew_idx

1-based latent indices to probe (in the ⁠[beta | random effects]⁠ layout of mode). NULL with compute_skew = TRUE probes every latent index.

debias

Subspace debias: a list with idx (1-based latent indices to correct by Metropolis along the Gaussian-conditional-mean surface through the mode) and optional n_iter / warmup / thin. The result then carries debias_draws (⁠n_kept x length(idx)⁠, the sampled x_S - mode_S), debias_sigma_ss (the inner Laplace's own marginal covariance of x_S), debias_accept and debias_idx. NULL (default) or an empty idx leaves the solve bit-for-bit as it was and consumes no random number. Non-spatial path only.

Value

A list with:

Examples

set.seed(1)
n <- 200L
X <- cbind(1, rnorm(n))
eta <- X %*% c(-0.3, 0.8)
y <- rbinom(n, 1, plogis(eta))
fit <- tulpa_laplace(y, rep(1L, n), X, family = "binomial")
fit$mode          # posterior mode of the fixed effects

Fit a beta-regression model via Laplace, estimating the precision

Description

Thin wrapper around tulpa_laplace() for family = "beta". The Laplace engine treats phi as fixed per fit (same contract as gamma and neg_binomial_2); this wrapper does an outer 1-D optimisation of the Laplace-approximated log-marginal over phi, then refits at the optimum to return betas and Hessian.

The mean-precision parameterisation is y ~ Beta(mu * phi, (1 - mu) * phi) with default logit link; y must be strictly in ⁠(0, 1)⁠.

Usage

tulpa_laplace_beta(
  y,
  X,
  re_list = list(),
  spatial = NULL,
  weights = NULL,
  offset = NULL,
  max_iter = 100L,
  tol = 1e-06,
  n_threads = 1L,
  beta_prior = NULL,
  phi_init = NULL,
  phi_bounds = c(0.1, 10000),
  outer_tol = 1e-04,
  mode = c("laplace", "nuts"),
  control = list()
)

Arguments

y

Response in ⁠(0, 1)⁠.

X

Fixed-effects design matrix.

re_list, spatial, weights, offset, max_iter, tol, n_threads, beta_prior

Passed to tulpa_laplace() verbatim. beta_prior places a Gaussian penalty on the fixed effects (a list with sd, optional mean; see tulpa_laplace()). It is included in the Laplace log-marginal that the precision phi is optimised against, so the penalised model is fit consistently across the outer phi search. Not supported with spatial (the spatial solver carries its own prior).

phi_init

Optional starting value for the precision. If NULL, a method-of-moments warm start is used.

phi_bounds

Numeric length-2 vector with lower/upper bounds on phi for the outer optimisation. Default c(0.1, 1e4).

outer_tol

Tolerance for the outer optimisation. Default 1e-4.

mode

Inference method (the method is an argument, not a parallel verb): "laplace" (default) is the Laplace + Brent-over-phi point fit documented here; "nuts" delegates to tulpa_nuts_beta(), which samples phi jointly with the coefficients via NUTS. In "nuts" mode the Laplace-only arguments (re_list, spatial, weights, offset, phi_init, phi_bounds, outer_tol) are not used, and NUTS knobs are passed via control (see tulpa_nuts_beta()).

control

Passed to tulpa_nuts_beta() when mode = "nuts" (ignored for mode = "laplace").

Value

For mode = "laplace", the list returned by tulpa_laplace() at the optimum, augmented with phi (the optimised precision) and phi_log_marginal (the optimisation trace). For mode = "nuts", the draws object returned by tulpa_nuts_beta().

Examples

set.seed(1)
n <- 200L
X <- cbind(1, rnorm(n))
mu <- plogis(X %*% c(0.2, 0.7)); phi <- 8
y <- rbeta(n, mu * phi, (1 - mu) * phi)
fit <- tulpa_laplace_beta(y, X)
fit$mode

Latent Factor Specification for Unmeasured Confounders

Description

Specify latent factors to capture shared unmeasured confounders between model processes. Latent factors are particularly useful when you suspect that both processes are driven by common unmeasured variables.

Value

The constructor documented in this family (latent_factor()) returns a tulpa_latent specification object consumed by ratio / multi-arm model packages built on tulpa (e.g. tulpaRatio), where the shared factor enters two or more linear predictors and cancels in the derived ratio. It is a multi-arm construct: the single-response tulpa() front door does not read it (for an in-formula latent Gaussian block on a single response, use latent(tgmrf(...))).


Streaming pointwise log-likelihood

Description

Wrap a pointwise log-likelihood for tulpa_criteria() without materializing the whole ⁠[n_draws x n_obs]⁠ matrix. A plain matrix is wrapped directly; a block generator (a function of an integer column vector returning the ⁠[n_draws x length(cols)]⁠ submatrix) lets the criteria accumulators stream over observation blocks, so an EVA-scale ⁠[200 x 1.16M]⁠ log-likelihood is consumed a few thousand columns at a time.

Usage

tulpa_loglik(x, n_obs = NULL, n_draws = NULL)

Arguments

x

Either a numeric ⁠[n_draws x n_obs]⁠ matrix, an existing tulpa_loglik, or a function f(cols) returning the ⁠[n_draws x length(cols)]⁠ submatrix for the integer column indices cols.

n_obs, n_draws

Required when x is a generator function; the column and row counts of the implied matrix.

Value

A tulpa_loglik object: a list with get(cols), n_obs, n_draws, and materialized.

See Also

tulpa_criteria()


Multinomial (nominal K-class) logistic regression via Laplace

Description

Fits a baseline-category multinomial logit model: for a K-level unordered response, classes ⁠1..K-1⁠ each get their own linear predictor and class K is the baseline. The coupled multinomial likelihood is solved by a Newton step to the penalized mode (a Gaussian ridge prior on the coefficients) and summarized by a Laplace approximation, reusing the native multinomial kernel.

Usage

tulpa_multinomial(
  formula,
  data,
  beta_prior = .tulpa_default_beta_prior("multinomial"),
  control = list()
)

Arguments

formula

Model formula; the response must be a factor (or coercible to one) with >= 3 levels. The baseline is the last level.

data

A data frame.

beta_prior

Fixed-effect prior as list(mean, sd): a mean-zero (mean = 0) Gaussian ridge on every coefficient with scalar SD sd (default the engine default, prior_normal(0, 2.5)). A finite SD keeps the mode finite under separation.

control

List of numerical knobs: max_iter (default 100), tol (default 1e-8), n_draws (posterior draws, default 2000), seed.

Value

A tulpa_fit (subclass tulpa_multinomial) with coef (named class:term), vcov, draws, log_marginal, classes, baseline, and the standard generic-method support.

See Also

tulpa() for single-process GLMMs.

Examples


set.seed(1)
n <- 300L; x <- rnorm(n)
eta <- cbind(0.5 + 1.0 * x, -0.3 - 0.8 * x)          # classes 1, 2 vs baseline 3
P <- cbind(exp(eta), 1); P <- P / rowSums(P)
y <- factor(apply(P, 1, function(pr) sample.int(3L, 1L, prob = pr)))
fit <- tulpa_multinomial(y ~ x, data = data.frame(y = y, x = x))
coef(fit)


Nested Laplace approximation for latent Gaussian models

Description

Generic outer-grid nested Laplace driver. Builds a grid over the hyperparameters of a single latent prior block (spatial or temporal), runs an inner Laplace at each grid point with warm-starting, and integrates over the grid to give proper hyperparameter marginals.

Supported priors:

Usage

tulpa_nested_laplace(
  y,
  n_trials,
  X,
  prior = NULL,
  spec = NULL,
  data = NULL,
  re_idx = NULL,
  n_re_groups = 0L,
  sigma_re = 1,
  family = "binomial",
  phi = 1,
  likelihood = NULL,
  control = list()
)

Arguments

y

Response vector.

n_trials

Trial sizes (binomial). Pass 1L-vector otherwise.

X

Fixed-effects design matrix.

prior

A list describing the latent prior block. Required field type one of {"icar", "bym2", "car_proper", "rw1", "rw2", "ar1"}. Type-specific fields:

  • icar: spatial_idx, n_spatial_units, adj_row_ptr, adj_col_idx, n_neighbors (CSR adjacency, 0-based); optional tau_grid.

Any block accepts an optional svc_weight, one weight per observation, which makes it a varying coefficient: observation i contributes svc_weight[i] * f_i instead of f_i, where f_i is the block's field at that row (z[spatial_idx[i]] for an areal block, ⁠(A u)_i⁠ for an SPDE one). Absent, the field enters unweighted.

  • bym2: same adjacency; scale_factor; optional sigma_grid, rho_grid.

  • car_proper: same adjacency; optional tau_grid, rho_grid, rho_bounds = c(lower, upper) (defaults to (0, 1)).

  • rw1/rw2: temporal_idx (1-based), n_times; optional tau_grid, cyclic (default FALSE).

  • ar1: temporal_idx, n_times; optional tau_grid, rho_grid.

A default grid axis is a starting axis, not a hard ceiling: for icar (tau_grid) and bym2 (sigma_grid) a posterior mode that rails a boundary node (pareto_k_regime = "collapsed_edge") triggers one mode-Hessian recenter-and-refit, reported through outer_grid_placement / outer_grid_recenter_declined – see tulpa_nested_laplace_joint()'s return docs. An axis the caller pinned is never moved; mark a grid your own code defaulted with auto_grid() to keep the recenter live on it.

spec

Optional tulpa_temporal or tulpa_spatial spec object (output of temporal_rw1(), temporal_rw2(), temporal_ar1(), spatial_car(), spatial_bym2(), etc.). When supplied alongside data, the prior list is built automatically via prior_from_spec() – pass either prior or spec, not both.

data

Data frame used to validate spec and resolve time/group/site indices. Required when spec is supplied.

re_idx

Optional 1-based RE group index per obs (defaults to no RE).

n_re_groups

RE group count (default 0).

sigma_re

RE standard deviation (default 1).

family

"binomial", "poisson", "neg_binomial_2", etc.

phi

Dispersion (negbin/gamma).

likelihood

Optional model-supplied likelihood, replacing the built-in family. Pass an external pointer to a tulpa::NestedLikelihood (built in a model package's own C++ from a LikelihoodSpec); the inner Laplace solve then reads the per-observation score, Fisher weight, and log-likelihood from that spec instead of family, so family/phi are ignored. Used by model packages to fit a custom response without adding a family to tulpa – for example tulpaObs threads its marginalized single-season occupancy likelihood (a scaled Bernoulli, with the latent occupancy state integrated out) through this. Multi-block prior only. Default NULL (use family).

control

Optional list of perf/numerical tuning knobs (statistical arguments stay top-level), following the control convention of tulpa(). Recognised elements (defaults in parentheses):

  • max_iter (50L), tol (1e-6) – inner Newton iteration budget and tolerance.

  • n_threads (1L) – inner-loop OpenMP threads.

  • x_init (NULL) – warm-start for the first grid point's inner solve.

  • keep_grid_hessians (FALSE) – when TRUE, retain per-grid-point fixed-effects marginal Hessian H_\beta and mode \hat{\beta} on the return list as ⁠$grid_hessians⁠ (list of dense p\times p matrices) and ⁠$grid_modes⁠ (list of length-p vectors). Used downstream by simplified-Laplace (SLA) callers to assemble skew-aware marginals – see the cumulant pooling in rubins_pool().

  • diagnose_k (TRUE), k_samples (200L) – compute the outer Pareto-\hat{k} accuracy diagnostic (⁠$pareto_k⁠) by importance sampling the hyperparameter posterior against the Gaussian proposal fitted to the grid, drawing k_samples extra inner-marginal evaluations. Computed for a single-block, single positive-scale-axis grid; left NA (with the grid's quadrature ESS as the fallback diagnostic) for multi-block, multi-axis, or bounded-parameter grids. See tulpa_psis().

  • diagnose_skew (TRUE), skew_idx (NULL) – compute the inner-Laplace skewness diagnostic (⁠$inner_skew⁠, gamma_3, Rue Martino & Chopin 2009 Sec 3.2.3) at the fitted MAP grid cell: one extra Newton solve, scoring the p fixed-effects latent indices by default (pass skew_idx, 1-based latent indices, to score additional ones, e.g. specific spatial units – the full latent field is not scored by default since it costs one linear solve per index). This is the complementary layer to diagnose_k: the outer diagnostic scores the hyperparameter-grid integration around a FIXED inner Laplace, this scores whether that inner Gaussian approximation is itself a good fit to the latent-field conditional posterior. See diagnostics() for the combined whole-fit verdict.

  • within_cell ("box_uniform") – the WITHIN-CELL construction the reported per-axis hyperparameter intervals are read with. The outer grid's weights say how much mass each cell holds; they do not say how it is spread inside the cell, and a quantile needs both. "box_uniform" puts the cumulative FULL mass at each cell EDGE and interpolates between edges; "chord" puts the cumulative MID-mass at each cell coordinate and interpolates between coordinates – the same masses over the same boxes with the knots moved half a cell, which measures as a whole order of convergence (2.00 against 1.04 on a fixture with a closed-form posterior). THE DEFAULT IS "box_uniform" since 0.0.188, decided on FIXED-TRUTH coverage at the placement the engine ships, with auto_recenter = "resolve" as the default. Summed |coverage - nominal| over nominal 0.95 / 0.80 / 0.50, chord against box-uniform: 0.2900 / 0.1233 on the pre-registered fixed-truth instrument, 0.2004 / 0.0361 over 4680 truth-swept fits of the same fixture, and 0.2467 / 0.1572 over nine (config, axis) rows spanning seven families, at 0.69 to 1.08x the width. The conditional-coverage swing that held the default back reads 0.110 at the shipped placement against 0.415 on the coarse pinned grid it was measured on, and at nominal 0.50 it is the same on both reads. outer_grid_h_over_sd is how wide a cell is on each axis (with outer_grid_resolution_declined naming why an axis carries no ratio, and outer_grid_railed_axes naming any axis whose nodes do not contain its own posterior mode), and theta_within_cell is what each axis was actually read with. Only a "density" support admits it – a CCD design, a locally refined grid and a posterior sample are not cell partitions that tile – and an axis it declines on reports "chord" with a reason rather than erroring. Nothing else moves: point estimates, moments, draws and weights are untouched, and "chord" restores the previous report exactly.

  • skew_correct (TRUE) – consume the inner-Laplace expansion instead of only grading it: report Cornish-Fisher marginal quantiles at each coefficient's own gamma_3, about the centre gamma_1 + gamma_3 / 2 that Rue, Martino & Chopin (2009) eq. (22) implies, from summary() / confint() wherever the combined inner band says the leading-order expansion is in its regime, and the Gaussian quantiles everywhere else. It is post-processing on the reported quantiles: draws, modes and weights are untouched, so a fit run with it off is bit for bit the fit it was before. A coefficient whose location term could not be formed declines rather than reading it as zero. The band that bounded the relocation itself (centre_unreliable) is off (Inf): scored over seven fixtures with an exact reference, every finite cutoff declines the coefficients the correction helps most, because a large centre carrying a small gamma_3 is uniformly weak correlation rather than an expansion out of its regime. ⁠$skew_correction⁠ records the per-coefficient gamma_3, gamma_1 and the centre they form, the band, the inner importance k-hat, the combined band, the eligibility and the reason behind it; the skew_applied attribute on summary() / confint() records what was actually used at the requested level. RMC fit a skew normal here instead; the series correction is the same-order alternative, and unlike a skew normal its skewness does not saturate inside the band it is applied on.

    MEASURED. Against exact quadrature quantiles of rare-event binomial-logit posteriors it cuts total absolute endpoint error 69.2%, improving both endpoints in every case. Scored over the WHOLE marginal – paired CRPS against the exact posterior in a 400-replicate prior-predictive experiment – it reads -0.01643 against the uncorrected Laplace at t = -1.89, essentially all of the -0.01662 the exact posterior itself achieves, and its PIT re-enters the simultaneous SBC band. Applied about the Laplace mode instead of about gamma_1 + gamma_3 / 2 the same reshaping scored +0.00775 at t = +3.54, a net loss; the location term is what supplies that centre.

    IT IS ON BY DEFAULT, so summary() / confint() on a nested-Laplace fit report the corrected quantiles wherever the combined inner band admits the coefficient; skew_correct = FALSE restores the uncorrected report exactly. Scored against the mixture read a correction-off fit gives, the flip is t = -1.895 on the rare-event intercept and -3.765 / -3.201 on the small-group Bernoulli design. Across twelve model classes read off one solve per seed, pooled 95% coverage moves 0.9510 -> 0.9542 at a standard error of 0.0070, with every class inside the acceptance the shipped gates use. A fit the correction cannot help – a coupled one, whose location term is unreachable – reports what it reported before, to the bit.

  • subspace_debias (FALSE) – correct only the latent directions the inner-layer diagnostics flagged, by exact Metropolis, and leave the rest at their Gaussian conditional. TRUE takes every default; a list overrides band (the inner-reliability floor a coordinate is selected at, default "ok"), idx (pin the corrected set explicitly, skipping the selector), closure / closure_max (grow the set by strongly coupled precision-graph neighbours – declined on this backend, which retains no joint precision), the sampler budget n_iter / warmup / thin, and n_draws. The selector reads the per-index bands diagnose_skew already attached, so it costs no extra solve; the correction itself re-runs the settled grid once with the sampler on, and the fit then reports ⁠$draws⁠ – the per-cell Metropolis sample for the selected coordinates, the rest from the Gaussian conditional given them – instead of the Gaussian-mixture moments. An EMPTY selection leaves the fit bit-for-bit identical to the plain path. ⁠$subspace_debias⁠ records what was selected, the bands it was read from, and the per-cell acceptance rate. Requesting it turns keep_grid_hessians on, since the recombination reads exactly those per-cell pieces.

  • cila (FALSE) – corrected integrated Laplace, the second inner-layer debias (after Lai, Margossian and Sheldon, arXiv:2605.20345). Where subspace_debias selects coordinates and runs exact Metropolis on them, this selects nothing: at every outer cell it draws n_points points from the whole inner Gaussian, weights each by the exact joint density it came from, and reports the weighted particles. TRUE takes the defaults; a list overrides n_points (1024L), variant ("qmc", a Sobol net; "is" for iid draws, "rqmc" for the net under n_shift random shifts), n_shift (8L), n_draws and seed. Below 512 points a cell's particle set is too coarse to be a marginal at all and the request is refused. The corrected per-cell masses become the fit's own weights / log_marginal and weights_source reports "cila"; the pre-correction pair is kept as ⁠$cila$laplace⁠. A cell whose inner solve factorized sparsely draws through the CHOLMOD factor's own triangular and permutation solves; an LDL' factor has no square root to draw with and is declined with "sparse_factor_not_ll".

  • auto_recenter (TRUE) – outer-grid placement policy. TRUE re-centres the movable default axes on the posterior mode and refits when the grid either RAILS (an axis's own marginal is maximal at one of its own endpoints) or does not RESOLVE its posterior (an axis's node spacing exceeds 2 posterior SDs in its own coordinate). Both tests read the weights the fit already stored, so a grid that brackets and resolves its mode costs nothing beyond them; when the pass does fire it is a second full grid solve plus a finite-difference mode/Hessian stencil. A recentred axis is ⁠mode +/- 2.5 sd⁠ over 5 nodes, a cell width of 1.25 posterior SDs by construction, against a census median of 3.9 on the fixed spans.

    Measured over 200 fixed-truth seeds on each of six configurations (icar chain / icar lattice / rw1 / bym2 / iid / nngp), the default moves mean |coverage - nominal| from 0.043 to 0.030 at the 95% level, 0.171 to 0.084 at 80% and 0.243 to 0.129 at 50% against the rail-only policy, at 0.63 times the 95% interval width and 0.76 times the median bias, for 1.71 times the wall clock.

    Three other values. "rail" is the rail test alone, which is what TRUE meant before the sizing measurement settled the default. FALSE integrates over the grid exactly as given, whatever it is, and records outer_grid_recenter_declined = "auto_recenter_disabled". "always" re-centres every movable default axis whatever the fit did, at 2.04 times the wall clock; it agrees with the default seed for seed on five of the six measured configurations and differs on the one whose default axes already resolve their posterior, where it takes 50% coverage to 0.135 against a nominal 0.5.

    Which families carry movable axes is .NL_REGISTRY_AXIS_FIELD: icar, rw1, rw2, iid, bym2, nngp, hsgp and spde. car_proper, ar1 and hsgp_mo each carry a correlation axis with no guessable coordinate, and mcar / miid / tgmrf hold their axes in one matrix field; a fit of any of them records which through outer_grid_recenter_declined rather than passing in silence. The per-axis policy names are the standalone registry path only – tulpa_nested_laplace_joint() and fit_st_nested() refuse them rather than accept them and ignore them.

  • max_grid_cells (2048L) – cell-count ceiling on a multi-block outer grid, refused with an error above it. Each cell is one inner Newton solve, so the default catches a per-block grid that multiplied out to a run nobody asked for; a deliberate converged tensor reference grid (4 axes at 7 levels is 2401 cells) raises it here.

Value

A list with:

References

Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392.

Examples


set.seed(1)
S <- 30L                                   # spatial units arranged in a chain
nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L)))
nn <- lengths(nb)
field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4))))   # smooth spatial field
idx <- rep(seq_len(S), each = 6L); n <- length(idx); x <- rnorm(n)
y <- rbinom(n, 1L, plogis(-0.2 + 0.6 * x + field[idx]))
prior <- list(type = "icar", n_spatial_units = S, spatial_idx = idx,
              adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L,
              n_neighbors = nn, tau_grid = c(0.5, 1, 2, 4, 8))
fit <- tulpa_nested_laplace(y, rep(1L, n), cbind(1, x), prior = prior,
                            family = "binomial")
fit$theta_mean        # marginalized ICAR precision


Joint multi-likelihood nested Laplace approximation

Description

Outer-grid nested Laplace driver for joint models – multiple response arms sharing one latent prior block, parameterized as a per-arm field amplitude (sigma) on a unit-precision latent.

Supported priors:

Other backends (NNGP, HSGP, RW1/2, AR1) follow the same interface and land under Phase 3.

Usage

tulpa_nested_laplace_joint(
  responses,
  prior,
  copy = NULL,
  phi_grid = NULL,
  prior_sigma = NULL,
  prior_alpha = NULL,
  prior_phi = NULL,
  cell_coupling = "separable",
  control = list()
)

Arguments

responses

A named list of arm specs (length >= 1). Each arm:

  • y – numeric ⁠[N_arm]⁠ response.

  • n_trials – integer ⁠[N_arm]⁠ (use rep(1L, N_arm) for non-binomial).

  • X – numeric matrix ⁠[N_arm x p_arm]⁠ fixed-effects design.

  • spatial_idx – integer ⁠[N_arm]⁠, 1-based map obs -> spatial unit.

  • re_idx – optional numeric ⁠[N_arm]⁠ 1-based RE group index; defaults to rep(0, N_arm) (no RE).

  • n_re_groups – optional integer (default 0L).

  • sigma_re – optional numeric (default 1); ignored when n_re_groups == 0.

  • family – one of "binomial", "gaussian", "poisson", "neg_binomial_2", "beta", "lognormal", "gamma", "inverse_gaussian". For "lognormal", y is on the natural scale and the linear predictor ⁠eta = E[log y]⁠ (identity link on the log scale); the -log(y) Jacobian is included in the kernel's log_lik.

  • phi – numeric dispersion (gaussian/lognormal residual SD, negbin size, beta precision); default 1.

  • field_coef – optional per-arm field coefficient controlling this arm's multiplier on the shared latent field's amplitude. One of: * numeric scalar (default 1) – constant multiplier. 0 means the arm carries NO field at all (the per-row scatter ⁠eta += field_coef * sigma * z⁠ is skipped for that arm). * character of length 1 – names an outer-grid hyperparam axis (currently "alpha"); the coefficient varies across the grid. * list(name = , grid = ) – embedded axis declaration, equivalent to declaring the axis and naming it on this arm. At most one arm may declare a hyperparam-driven axis (the cover hurdle and occu_cover both need only one). Shared axes across multiple arms are deferred. A single-block copy coefficient is declared here, on the arm – not through a separate copy argument.

prior

A list describing the shared latent prior block. Required field type. Backend-specific fields:

  • bym2: n_spatial_units, adj_row_ptr, adj_col_idx, n_neighbors, scale_factor (default 1); optional sigma_grid (donor-arm field amplitude, default 5 log-spaced values in ⁠[0.1, 3]⁠), rho_grid (default c(0.2, 0.5, 0.8, 0.95, 0.99, 0.999)).

  • icar: n_spatial_units, adj_row_ptr, adj_col_idx, n_neighbors; optional sigma_grid (default 5 log-spaced values in ⁠[0.1, 3]⁠).

  • car_proper: same as icar plus rho_car_grid (default c(0.5, 0.8, 0.95, 0.99)).

sigma_grid's default is a starting axis, not a hard ceiling: when the fitted field-SD posterior mode rails the top node (pareto_k_regime = "collapsed_edge", see below), the driver re-centres the axis on a mode-Hessian and refits (up to two attempts, the second adding a light default PC(U=3, alpha=0.01) prior on sigma unless prior_sigma was pinned – see there), so a sparse or strongly-identified species is not silently truncated at 3.0. This engages whether or not control$diagnose_k computed the full outer Pareto-k diagnostic: the mode-Hessian is reused from the diagnostic when it ran, or computed on its own (one extra batched finite-difference solve, only when the grid actually collapsed) when it did not – so diagnose_k = FALSE, the default, does not leave a railed axis stuck. A sigma_grid the caller PINNED always wins: auto-recenter engages when the field is left NULL, when it is marked with auto_grid() (how a wrapper package declares an axis it defaulted rather than one the user chose), or when its nodes are exactly the engine's own default axis. Declines gracefully (keeps the fixed-grid fit) when another axis in the same grid has unguessable support (car_proper's rho_car); whichever way it declines, outer_grid_recenter_declined says which (see below).

copy

Multi-block copy specification (multi-block prior only). For a single-block fit there is no copy argument: declare the copy coefficient on the arm via responses[[X]]$field_coef = list(name = "alpha", grid = G). On the multi-block path copy is an unnamed list of specs – list(list(arm, block, alpha_grid),...) – coupling N distinct shared latent fields, each onto its own arm with its own \alpha axis, integrated over the product outer grid. Each spec must name a distinct block. The copy block may be any of icar / bym2 / car_proper / rw1 / rw2 / ar1 / iid; blocks with their own per-arm scaling (lf, hsgp_mo) or a precomputed precision (tgmrf) do not take a copy. A copy block's own sigma_grid (the donor field amplitude, same default ceiling as the single-block prior$sigma_grid above) auto-recenters on collapsed_edge the same way, one block per attempt.

phi_grid

Optional list specifying per-arm dispersion axes on the outer grid. Accepts either a named list (keys = arm names) or a positional list of length n_arms. Each entry is one of:

  • NULL or scalar – no axis for that arm; the kernel uses the parse-time scalar responses[[k]]$phi.

  • numeric vector of length > 1 – adds a new outer-grid axis ⁠phi_<arm>⁠ taking those values; the kernel rewrites ⁠arms[k].phi⁠ at each grid point before the inner Newton solve.

Family-specific interpretation of arm$phi (the parse-time scalar and the grid values):

  • gaussian – residual SD (variance is phi^2). Use phi_grid to estimate the residual SD as a hyperparameter instead of pinning it pre-fit.

  • lognormal – residual SD on the log scale; identical kernel parameterization as gaussian plus the -log(y) Jacobian.

  • neg_binomial_2 – dispersion (variance is mu + mu^2/phi).

  • beta – precision (variance is mu(1-mu)/(1+phi)).

  • gamma, inverse_gaussian – shape / dispersion.

  • binomial, poisson – ignored.

Each ⁠phi_<arm>⁠ axis is appended to the Cartesian product and varies slowest (within-spatial warm starts hold). The axis appears as a regular hyperparameter in theta_grid, theta_mean, and theta_sd, and participates in adaptive-grid refinement.

prior_sigma, prior_alpha

Optional regularizing hyperpriors on the donor field amplitude \sigma and on the copy coefficient \alpha. Each is NULL (flat, default) or a list of the form list(family, params):

  • list("pc.prec", c(U, alpha)) – Penalized Complexity prior, calibrated by P(theta > U) = alpha. Closed-form density lambda * exp(-lambda * theta) with lambda = -log(alpha)/U. Drop-in for the weakly-identified small-n_pos regime . Pick U at the upper end of plausible values so the prior shrinks the tail without biasing the modal cell when the data identifies it: default-friendly choice on \sigma is c(U = 1.0, alpha = 0.01) (donor amplitude); on the dimensionless copy coefficient \alpha the recommended choice is c(U = 8.0, alpha = 0.01). Too small a U over-shrinks the copy coefficient past the modal cell and, through the alpha * sigma copy axis, inflates the coupled donor amplitude sigma – e.g. on a fixture with truth \alpha = 1, c(U = 2.0, alpha = 0.01) pulls the \alpha posterior below 1 and lifts sigma above its truth.

  • list("half_normal", scale) – half-normal with scale scale > 0. Sharper tail decay than PC; use when stronger regularization is desired and the truth is well inside the prior. The contribution is added to log_marginal cell-by-cell at the kernel-call boundary, so refinement passes (adaptive grid, var-of-means consistency) see the regularized posterior. When the data identifies the parameter (e.g. n_pos >= ~200) the prior is essentially harmless – the lever is tail-shrinkage at small n_pos. prior_alpha only applies when copy is active; prior_sigma applies on any sigma-named axis.

prior_sigma also interacts with the auto-recenter above: its second attempt engages the engine's own weakly-informative PC(U = 3, alpha = 0.01) prior, and a prior_sigma the caller PINNED suppresses that (the caller's prior stands). Pinning is decided by provenance, not presence: a spec marked with auto_grid(), or one equal by value to the engine's own default, is a default and does not suppress the escalation. When it does, the fit carries outer_grid_prior_declined = "prior_pinned", so a second attempt that changed only the grid geometry is legible rather than looking like the full escalation.

prior_phi

Optional regularizing hyperprior on the per-arm dispersion axes declared through phi_grid (e.g. a Beta precision on a cover arm, a negbin dispersion, a Gaussian residual SD). Same families as prior_sigmaNULL (flat over the phi grid, default), list("pc.prec", c(U, alpha)), or list("half_normal", scale). A single spec re-weights every ⁠phi_<arm>⁠ axis on the grid, the way prior_sigma re-weights any sigma-named axis; with no phi_grid it is a no-op. Without it the phi grid carries an implicit flat prior over its bounds. The PC scale is the dispersion's own units (a precision for beta, a size for neg_binomial_2), so pick U at the upper end of plausible values.

cell_coupling

Character scalar naming a per-cell coupled likelihood registered against tulpa's process-global registry (see src/cell_coupling_registry.h). Defaults to "separable", the arm- separable per-obs sum every existing joint fit uses. Consumer packages (e.g. tulpaObs) compile a tulpa::CellCouplingSpec subclass in their own ⁠src/⁠ and register it from ⁠R_init_<pkg>⁠ via the tulpa_register_cell_coupling C callable; the R driver validates the name against the registry and the inner Newton routes the per-cell contribution through evaluate_cell() when the spec couples at least one arm.

control

Optional list of perf/numerical tuning knobs (statistical arguments stay top-level), following the control convention of tulpa(). Recognised elements (defaults in parentheses):

  • max_iter (50L), tol (1e-6) – inner Newton iteration budget and tolerance.

  • n_threads (1L) – inner-loop OpenMP threads (per-observation scatter, compute_eta, log-likelihood reduction). For typical joint workloads (N in the hundreds to a few thousand) inner parallelism is overhead-dominated; prefer n_threads_outer, which stacks better on many-core hardware. Capped at the physical performance-core count by default (see n_threads_scatter), as the inner per-observation loops oversubscribe a hybrid CPU's efficiency cores past that point. A fit is reproducible bit for bit at a GIVEN n_threads: the per-observation sum cuts its range into that many contiguous chunks and adds the chunk sums in chunk order, so nothing about the answer is left to the OpenMP runtime. Chunking imposes its own association, so two different n_threads agree only to floating-point tolerance (measured at 6e-14 on log_marginal over four families).

  • n_threads_scatter (performance-core count) – cap on the inner per-observation threads. Overrides the default performance-core cap on n_threads; raise it to use all logical cores or lower it to leave headroom. No effect where the core topology cannot be resolved (off Windows), where n_threads is used as requested.

  • n_threads_outer (1L) – outer-grid OpenMP threads. When ⁠> 1⁠, a pilot Laplace at the centre cell warm-starts the remaining cells, each dispatched across n_threads_outer threads with its own CHOLMOD solver and NewtonScratch (inner OpenMP auto-disabled). 1L is serial, chained warm-starts – bitwise identical to the pre-speedup driver. Recommended on multi-core workstations: parallel::detectCores() - 1L.

  • tile_warm (TRUE) – when n_threads_outer > 1 and a copy block is present, group outer cells into tiles sharing every axis except the copy coefficient alpha, solve one warm Tier-2 per tile from the centre pilot, and warm-start the rest from their tile pilot. Falls back to the single-tier path when no copy block / single tile / n_threads_outer <= 1L. FALSE recovers the pre-tiling behaviour (e.g. regression testing).

  • prune (FALSE), prune_tol (1e-3) – opt-in cheap-pass screening. When prune = TRUE, the driver sweeps the outer lattice running a short inner Newton per cell, each warm-started from the previous screened cell's quasi-mode (lattice-adjacent), computes a screening Laplace log-marginal, softmax-normalises, and skips the full inner Newton on cells whose normalised weight is ⁠< prune_tol⁠. The neighbour-warm-start sweep keeps every cheap mode near its cell's true mode, so the cheap ranking is faithful to the full-solve ranking even when the inner latent mode moves substantially across the grid. Pruned cells get log_marginal = -Inf, n_iter = 0, and inherit the pilot mode; the pilot cell is never pruned. A safety gate falls back to the full grid (with a warning) if the cheap-screen argmax disagrees with the full-solve argmax or the kept posterior collapses onto a cell the screen badly mis-estimated, so a silently-wrong pruned posterior is impossible. Stacks with n_threads_outer. prune_tol must be in ⁠[0, 1)⁠. Keep it conservative (⁠<= 1e-3⁠); pruning helps most when the grid has many low-mass tail cells. Default FALSE (the full grid is correct).

  • x_init (NULL) – warm-start for the first grid point's inner solve.

  • verbose (FALSE) – when TRUE, announce the engaged outer integrator for a multi-block prior in one line at selection time (see integration), and report each CCD decline reason.

  • store_Q (FALSE) – also return the per-grid joint precision Q (lower triangle, CSC) as Q_csc_p_per_grid, Q_csc_i_per_grid, Q_csc_x_per_grid, Q_csc_n, letting callers compute INLA-style total-variance posterior moments (Var-of-means + Mean-of-Var) on inner latent coordinates such as fixed-effect betas.

  • keep_grid_hessians (TRUE) – retain the per-grid-point fixed-effect mode and marginal precision as ⁠$grid_modes⁠ / ⁠$grid_hessians⁠, which is what lets summary.tulpa_fit(), confint.tulpa_fit() and vcov.tulpa_fit() report the grid-marginalized fixed-effect covariance instead of NA. Memory is O(n_fixed^2) per cell. When the retention is not available the reason is recorded on ⁠$grid_fixed_declined⁠.

  • adaptive_grid (FALSE), adaptive_grid_edge_thresh (0.02), adaptive_grid_max_passes (1L) – when adaptive_grid = TRUE, a mode-tracked 1D refinement pass triggers on any axis whose marginal boundary weight exceeds adaptive_grid_edge_thresh. New points are appended on that axis (interior densification + outward log-spaced extension) paired with the boundary cell's modal other-axis values, each carrying a calibration term so it contributes on the marginal scale – O(n_new_points) kernel solves, not the full cartesian product. The edge score is ⁠max(marginal_weight_at_boundary, exp(max_log_marginal_at _boundary - max_log_marginal_overall))⁠, catching both boundary pile-up and integrand truncation; 0.02 is ~4 log units of decay. adaptive_grid_max_passes caps the passes (one usually suffices). Fixes posterior CI under-coverage when truth sits near a grid edge.

  • var_of_means_consistency (TRUE) – run a post-integration consistency pass on the variance of the per-arm posterior means and attach var_of_means_consistency_info.

  • inner_factorization ("auto") – which factorization the dense inner Newton applies to the Hessian it assembled: "auto" by the latent dimension, "sparse" for CHOLMOD, "dense" for the dense Cholesky. Independent of force_sparse, which selects the assembly path.

  • force_sparse (FALSE) – linear-algebra backend for the inner joint solve. TRUE / FALSE select the sparse or dense path outright, regardless of the dense/sparse heuristic. "auto" selects by the latent dimension the fit will actually build, taking the sparse path above n_x > 1000 and the dense one at or below it, where the sparse symbolic-analysis and indirection overhead outweighs the fill-in saving. A model whose latent dimension cannot be determined resolves to dense.

  • integration ("auto") – outer-grid node layout for a multi-block prior. A central composite design (CCD) integrates the hyperparameter posterior on ⁠1 + 2d + 2^d⁠ nodes oriented by the Cholesky of the posterior covariance at the joint hyperparameter mode – far fewer inner solves than the d-dimensional tensor product (25 vs 81 at d = 4). "auto" (the default) uses the CCD only at ⁠>= 4⁠ transformable axes, where the tensor product's k^d blow-up bites hardest, and keeps the cheaper, more ridge-robust tensor grid at ⁠<= 3⁠ axes; "ccd" lowers the CCD threshold to ⁠>= 3⁠ axes; "grid" always forces the full tensor product. "grid_adaptive" is the low-dimensional companion to the CCD: it seeds a coarse subsample of the SAME tensor lattice (latent block axes and phi axes together), floods outward from the posterior mode on the fine lattice, and evaluates only the cells within a log-density cutoff of the peak – a strict, uniform-weight subset of the dense tensor, so its posterior matches the dense grid to that cutoff at fewer inner solves when the hyperparameter posterior concentrates (a sharply-identified field SD / precision). It declines back to the dense tensor on a diffuse posterior (kept region would rival the tensor) or a degenerate lattice, so it never costs accuracy; tune it with adaptive_grid_cutoff / adaptive_grid_stride / adaptive_grid_max_frac. The CCD auto-falls back to the tensor grid for an axis whose support is not safely transformable (a CAR_proper rho_car or a non-BYM2 rho), or a flat / ridged / degenerate outer mode-find or Hessian; an active phi_grid rides as a tensor axis crossed on top of the CCD. Single-block joint priors always use the tensor grid. The CCD mode-find runs cheap warm-started inner solves and does not write to the checkpoint file. Under verbose = TRUE the engaged integrator is announced in one line at selection time (e.g. ⁠outer integration: CCD (4 latent axes, 25 nodes)⁠ or ⁠tensor grid (72 cells)⁠, or ⁠CCD declined -> tensor grid⁠), so the auto switch to the CCD at ⁠>= 4⁠ axes is never silent. The resolved integrator is also returned on the joint result as ⁠$integration⁠, alongside ⁠$integration_requested⁠ and ⁠$integration_declined⁠ (see Value), so a fallback is on the fit itself and not only in a verbose message.

  • local_ccd (NULL) – local CCD refinement of a multi-block tensor grid. TRUE (defaults) or a list(max_cells =, f0 =, skew_max =) refines a few high-weight, mutually non-adjacent interior cells, replacing each with a small curvature-aware CCD node cloud so a coarse base grid resolves the sharply-peaked directions without the k^d tensor blow-up. The local curvature is a diagonal finite difference of the outer log-marginal over the cell's own grid neighbours (no mode-find; only the off-centre nodes are new solves), warm-started from the cell's inner mode; each refined cell's sub-nodes carry partition-of-unity design weights so the total integration weight is conserved (no double-count). max_cells (8L) caps the refined cells; f0 (1.1) is the CCD radius. The design scale is shrunk per cell so the cloud fits the cell's Voronoi box (the local-Gaussian mass beyond it belongs to the neighbouring cells). A cell keeps its cloud only while the nodes' own log-marginals stay within skew_max (the gamma3_ok band, 0.5) of the quadratic the cloud was placed from, measured as a standardized cubic magnitude; above it the cell is put back as its own mass atom, which on a skewed outer target is measurably closer than the design. Engages only on the tensor path (the curvature stencil needs axis neighbours), at ⁠>= 4⁠ transformable latent axes, with no active phi_grid; otherwise it is a no-op. The applied refinement is summarised on the result as ⁠$local_ccd_info⁠. Also driven automatically by k_refine = "ccd".

  • adaptive_grid_cutoff (10), adaptive_grid_stride (2L), adaptive_grid_max_frac (0.75), adaptive_grid_min_cells (48) – tuning for integration = "grid_adaptive". adaptive_grid_cutoff is the log-density keep / expand radius from the peak (larger keeps more cells, closer to the dense tensor); adaptive_grid_stride the coarse-seed subsample stride per axis; adaptive_grid_max_frac the kept-fraction ceiling past which the builder declines back to the dense tensor; adaptive_grid_min_cells the smallest dense tensor worth the adaptive machinery – below it the builder declines BEFORE any inner solve (on a small tensor the coarse seed is already most of the grid, so there is no tail to skip). Ignored by the other integrators. The kept-cell / dense / solve counts are returned as ⁠$adaptive_grid_info⁠.

  • inner_refresh (1L) – inner-Newton Cholesky factor reuse interval (Shamanskii / chord method). For a non-quadratic positive arm (e.g. a beta cover arm) the latent Hessian changes every inner iteration, so the default re-factorizes the sparse Cholesky on each step – the dominant per-grid-cell cost. inner_refresh = m > 1 re-factorizes only every m-th inner step and reuses the cached factor in between (refreshing early whenever a reused solve fails). The gradient is exact on every step and each step is line-search safeguarded, so the converged mode is unchanged and the final mode-pass Hessian (log_det, SEs) is always fresh; only the path to the mode uses a stale curvature, which may cost a few extra inner iterations. Applies to the sparse joint path with the default control$hessian = "lm" curvature; the dense small-n_x path re-factorizes a cheap Hessian and ignores it. 2L-4L is a good range for a slow beta arm.

  • k_quality ("report") – the reliability intent for the outer Pareto-\hat{k}, a single statement of how reliable the fit should be. "report" (default) computes the diagnostic and reports the achieved band. "ok" / "good" additionally name a TARGET band (the \hat{k} confidently usable, resp. good) and raise the default k_samples (to 800L / 2000L, unless you set it) so the bootstrap CI can resolve it. "none" disables the diagnostic. The fit carries an honest verdict – k_quality_requested, k_quality_reached, k_quality_best, k_quality_reason, k_quality_rounds – and never silently downgrades: if the requested band is not confidently met it reports the band actually reached and why. For "ok" / "good", when the first fit does not reach the band the engine escalates by REFINING THE INTEGRATION GRID, driven by the bad \hat{k} (see k_refine): each round widens / densifies the grid where the posterior mass escapes its current bounds and re-diagnoses, up to k_max_rounds times. This is the actual fix for a grid-width deficiency; k_samples is the separate knob that sharpens the \hat{k} ESTIMATE and is not escalated here.

  • k_refine ("grid") – the integration-refinement rung for k_quality "ok" / "good". "grid" (default) re-fits with adaptive grid refinement (adaptive_grid) each escalation round, driven by the bad \hat{k}, so a too-coarse / too-narrow grid is widened / densified where the importance weight concentrates until the band is reached or the budget is spent. "ccd" instead refines a few high-weight, mutually non-adjacent interior cells with local curvature-aware CCD node clouds (see local_ccd), the right rung when the grid is too coarse to resolve a sharply-peaked direction rather than too narrow; it forces a tensor base grid (the curvature stencil needs axis neighbours) and engages only on the multi-block path at >= 4 transformable latent axes. "none" disables refinement: the band is reported but not chased.

  • k_max_rounds (2L) – the grid-refinement round budget for k_quality "ok" / "good": the maximum number of refine-and-re-fit rounds after the first fit. Each round allows one more refinement pass than the last. 0L disables escalation (single-shot, the band is reported but not chased).

  • diagnose_k (TRUE), k_samples (500L) – compute the outer Pareto-\hat{k} accuracy diagnostic by importance-sampling the joint hyperparameter posterior against the proposal the integrator fits (mixed per-axis transforms: log for positive scales, logit for the BYM2 mixing weight, identity for the copy coefficient \alpha). k_samples is the number of importance draws, each one an extra inner joint solve, and is the diagnostic's precision knob: a tighter k-hat needs MORE actual tail ratios, so increase k_samples (not k_bootstrap). The draws are RNG-restored so the fit's modes / draws are unchanged. A fit carrying an axis whose support is not safely known (CAR_proper's rho_car) declines to the quadrature-ESS fallback (pareto_k = NA). FALSE skips the diagnostic. "by_arm" additionally computes a k-hat restricted to each arm's hyperparameter axes (the other arms held at their posterior mean), reported in pareto_k_by_arm, to localise which arm drives a tail-heavy joint k; the joint k itself is unchanged. Per-arm k is defined for the multi-block layout with two or more arms and declines for the single-block shared-field layout. The legacy k_samples name is accepted as an alias for k_samples.

  • k_threads (NULL) – outer-thread width for the diagnostic's importance batch. The k_samples re-solves are independent and run after the grid (every core free), each solved single-threaded once the batch saturates the pool, so widening it is a bit-identical wall-clock speedup (the k-hat is unchanged). NULL follows the fit's own thread grant – the larger of n_threads_outer and the inner n_threads – so a serial fit keeps a serial diagnostic while a threaded fit gets a free parallel one. "auto" uses the physical performance-core count (capped at 2 under R CMD check); an integer pins the width (1L forces serial). Always capped at k_samples.

  • k_bootstrap (1000L) – bootstrap replicates for the outer Pareto-\hat{k} uncertainty. The k-hat is a single fixed number for a fit + proposal; its sampling uncertainty GIVEN the proposal is estimated by resampling the diagnostic's raw importance log-ratios with replacement and re-fitting the GPD tail k_bootstrap times (no new inner solves). Reports pareto_k_se_boot (bootstrap SE), pareto_k_ci_low / pareto_k_ci_high (the 2.5\ closed-form GPD-shape MLE asymptotic SE (1 + k)/\sqrt{M}, a cross-check), and pareto_k_band_confident (TRUE iff the bootstrap CI lies within one reliability band). The bootstrap measures how UNSTABLE the current tail estimate is; it cannot create tail information. Increase k_samples, not k_bootstrap, to obtain more tail information. 0L skips it (point k-hat only). The per-arm k carries the same fields.

  • k_tail_points (NULL) – number of upper-tail order statistics for the GPD fit. NULL uses the automatic PSIS rule \lceil\min(0.2 N, 3\sqrt{N})\rceil. An explicit value is an EXPERT tail-threshold control, capped at the 20\ so the fit stays an extreme tail; it is NOT a precision knob (a request that drags body ratios into the tail lowers variance but biases the k-hat). The used and requested counts are reported in pareto_k_tail_points / pareto_k_tail_points_requested.

  • k_conf_bands (NULL) – the reliability-band boundaries the bootstrap CI is tested against for pareto_k_band_confident. NULL (default) uses the sample-size-dependent boundaries c(0.5, \min(1 - 1/\log_{10} S, 0.7)) at the realised draw count S (Vehtari et al. 2024): the good cut is 0.5 and the usable cut tightens below 0.7 for small S (about 0.565 at S = 200, reaching 0.7 only past S \approx 2154). Supply a strictly-increasing numeric vector to fix the boundaries instead, e.g. c(0.5, 0.7) for the size-independent good / ok / unreliable split.

  • diagnose_skew (TRUE), skew_idx (NULL) – compute the inner-Laplace skewness diagnostic (⁠$inner_skew⁠, gamma_3, Rue Martino & Chopin 2009 Sec 3.2.3) at the fitted MAP grid cell: one extra Newton solve via the same kernel_fn the outer diagnostic reuses, scoring every arm's fixed-effects coefficients by default (pass skew_idx, 1-based indices in the joint ⁠[arm1_beta | arm1_re | arm2_beta | ... | blocks]⁠ latent layout – see ⁠$arm_layout⁠ – to probe additional indices). This is the complementary layer to diagnose_k: that scores the outer hyperparameter-grid integration around a FIXED inner Laplace, this scores whether that inner Gaussian approximation is itself a good fit. A genuinely coupled arm (cell_coupling != "separable" on that arm, e.g. tulpaObs's occu_cover) has no per-obs likelihood for the separable formula to score; it is scored instead by the contraction of the cell third-derivative tensor, which differences the cross-arm Hessian the coupling spec already returns. A fit that can carry neither reports NaN, not a silently wrong 0, and ⁠$inner_skew_declined⁠ says WHY – "coupled_arm" marks arms the inner layer could not score at all, distinct from a diagnostic that was simply switched off – while ⁠$inner_skew_arms_declined⁠ names the arms with no oracle, including on a partially scored fit. See diagnostics() for the combined verdict. Wired for both the single-block backends (icar/bym2/car_proper) and the multi-block path (a per-group RE, a trend field, or an arm-specific field block).

  • within_cell ("box_uniform") – the WITHIN-CELL construction the reported per-axis hyperparameter intervals are read with. The outer grid's weights say how much mass each cell holds; they do not say how it is spread inside the cell, and a quantile needs both. "box_uniform" puts the cumulative FULL mass at each cell EDGE and interpolates between edges; "chord" puts the cumulative MID-mass at each cell coordinate and interpolates between coordinates – the same masses over the same boxes with the knots moved half a cell, which measures as a whole order of convergence (2.00 against 1.04 on a fixture with a closed-form posterior). THE DEFAULT IS "box_uniform" since 0.0.188, decided on FIXED-TRUTH coverage at the placement the engine ships, with auto_recenter = "resolve" as the default. Summed |coverage - nominal| over nominal 0.95 / 0.80 / 0.50, chord against box-uniform: 0.2900 / 0.1233 on the pre-registered fixed-truth instrument, 0.2004 / 0.0361 over 4680 truth-swept fits of the same fixture, and 0.2467 / 0.1572 over nine (config, axis) rows spanning seven families, at 0.69 to 1.08x the width. The conditional-coverage swing that held the default back reads 0.110 at the shipped placement against 0.415 on the coarse pinned grid it was measured on, and at nominal 0.50 it is the same on both reads. outer_grid_h_over_sd is how wide a cell is on each axis, and theta_within_cell is what each axis was actually read with. Only a "density" support admits it – a CCD design, a locally refined grid and a posterior sample are not cell partitions that tile – and an axis it declines on reports "chord" with a reason rather than erroring. Nothing else moves: point estimates, moments, draws and weights are untouched, and "chord" restores the previous report exactly.

  • skew_correct (TRUE) – consume the inner-Laplace expansion instead of only grading it: report Cornish-Fisher marginal quantiles at each coefficient's own gamma_3, about the centre gamma_1 + gamma_3 / 2, from summary() / confint() wherever the combined inner band (gamma_3 and the inner importance k-hat) says the leading-order expansion is in its regime, and the Gaussian quantiles everywhere else. It is post-processing on the reported quantiles: draws, modes and weights are untouched, so a fit run with it off is bit for bit the fit it was before. The band that bounded the relocation itself (centre_unreliable) is off (Inf): every finite cutoff was measured to decline the coefficients the correction helps most. ⁠$skew_correction⁠ records the per-coefficient gamma_3, gamma_1 and the centre they form, the band, the k-hat, the combined band, the eligibility and the reason behind it; the skew_applied attribute on summary() / confint() records what was actually used at the requested level. A FULLY COUPLED fit declines it: the location term's contraction against a covariance block is not reachable from the cell third-derivative oracle, and an absent gamma_1 is never read as zero. Such a fit therefore reports what it reported before the default moved, to the bit – a declined coefficient keeps the grid-mixture read. See tulpa_nested_laplace() for the measurement behind the default.

  • subspace_debias (FALSE) – correct only the latent directions the inner-layer diagnostics flagged, by exact Metropolis along the Gaussian-conditional-mean surface through each cell's mode, and leave the rest at their Gaussian conditional. Settings and semantics are the ones documented on tulpa_nested_laplace(). On a fully coupled fit gamma_3 is NaN for every arm, so the selector rests on the derivative-free inner Pareto-k-hat; where that also bands the coordinate reliable, idx pins the set explicitly. An EMPTY selection leaves the fit bit-for-bit identical to the plain path. A fit whose inner solve took the s2z rank-1 or the PSD eigen-clamp path carries no usable factor to build the surface from and is left uncorrected, the same two paths diagnose_skew declines on.

  • cila (FALSE) – corrected integrated Laplace, the second inner-layer debias (after Lai, Margossian and Sheldon, arXiv:2605.20345). Where subspace_debias selects coordinates and runs exact Metropolis on them, this selects nothing: at every outer cell it draws n_points points from the whole inner Gaussian, weights each by the exact joint density it came from, and reports the weighted particles. The cell marginals and the latent posterior both converge to the exact ones as the effort grows, so n_points is the only dial. TRUE takes the defaults; a list overrides n_points (1024L), variant ("qmc", a Sobol net; "is" for iid draws, "rqmc" for the net under n_shift random shifts), n_shift (8L), n_draws (the reported draw count) and seed (the auxiliary stream, engine-owned so requesting the correction leaves every other posterior draw unchanged). Below 512 points a cell's particle set is too coarse to be a marginal at all and the request is refused. The correction reports DRAWS, so every coefficient-facing method reads them instead of the grid's Gaussian mixture, and the corrected per-cell masses become the fit's own weights / log_marginal with weights_source reporting "cila"; the pre-correction pair is kept as ⁠$cila$laplace⁠ and ⁠$cila$retained_mass⁠ is the original share of the cells that produced a usable particle set. ⁠$cila⁠ also carries the variant actually run and the PSIS grade (pareto_k, rel_ess) of the correction's own weights. A cell whose inner solve factorized SPARSELY draws through the CHOLMOD factor's own triangular and permutation solves; an LDL' factor carries no square root to draw with and is declined with "sparse_factor_not_ll".

  • auto_recenter (TRUE) – re-centre a default outer grid axis on its posterior mode and refit when the fit rails against a boundary node. FALSE integrates over the grid exactly as given, whatever it is, and records outer_grid_recenter_declined = "auto_recenter_disabled". The joint rescues trigger on the whole grid's collapsed-edge regime rather than on a per-axis rail, so the per-axis policy names tulpa_nested_laplace() takes ("rail", "resolve", "always") are refused here with an error rather than accepted and ignored.

  • max_grid_cells (2048L) – cell-count ceiling on a multi-block tensor outer grid, refused with an error above it. Each cell is one inner Newton solve, so the default catches per-block grids that multiplied out to a run nobody asked for; a deliberate converged tensor reference grid (4 axes at 7 levels is 2401 cells) raises it here, which integration = "ccd" cannot serve since a CCD is a different integration design.

  • checkpoint (NULL) – grid-cell checkpoint/resume. Set list(path = "fit.ckpt", resume = TRUE) to make a killed or interrupted fit resumable: each completed outer-grid cell is appended to path, and a later call with the same responses + grid + control loads the finished cells and solves only the rest. EVA-scale joint fits run for hours, so a wrapper teardown, reboot, or OOM near the end otherwise loses the whole run. resume = TRUE (the default when a path is given) continues from an existing file; resume = FALSE removes it first and starts over. Adaptive-grid refinement cells are checkpointed under their own coordinate keys, so resume covers them too. A file written for different data or solver settings is rejected (fingerprint mismatch) rather than resumed onto a stale result.

Value

A list of class c("tulpa_nested_laplace_joint", "tulpa_nested_laplace", "list") with:

(sigma, alpha) parameterization

Each arm's linear predictor reads

\eta_{arm} = X_{arm} \beta_{arm} + \sigma_{arm} \cdot z_s,

where z_s is a unit-precision latent (ICAR(tau=1) for ICAR/BYM2, or the BYM2 mix; CAR_proper uses the structure of D - \rho_{car} W). All arms share the donor amplitude \sigma from the outer-grid sigma_grid axis. The copy arm's amplitude is \sigma_{arm} = \alpha \cdot \sigma, where \alpha is a direct outer-grid axis taken from copy$alpha_grid. The Cartesian product is over ⁠(sigma, [rho/rho_car,] alpha[, phi])⁠. Direct \alpha as an outer-grid axis (rather than a post-hoc ratio \alpha = \sigma_{pos} / \sigma_{occ}) avoids plug-in bias on the weakly-identified ratio at small n_pos and lets a regularizing hyperprior land on \alpha directly.

References

Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392.

See Also

tulpa_nested_laplace() for the single-arm engine.

Examples


set.seed(1)
S <- 25L                                   # shared spatial units (chain graph)
nb <- lapply(seq_len(S), function(s) setdiff(c(s - 1L, s + 1L), c(0L, S + 1L)))
nn <- lengths(nb)
field <- as.numeric(scale(cumsum(rnorm(S, 0, 0.4))))
mk_arm <- function(m, fam) {
  si <- sample(S, m, replace = TRUE); x <- rnorm(m)
  lin <- 0.2 + 0.5 * x + 0.8 * field[si]
  y <- if (fam == "binomial") rbinom(m, 1L, plogis(lin)) else lin + rnorm(m, 0, 0.5)
  list(y = as.numeric(y), n_trials = rep(1L, m), X = cbind(1, x),
       spatial_idx = si, family = fam, phi = if (fam == "gaussian") 0.5 else 1)
}
prior <- list(type = "icar", n_spatial_units = S,
              adj_row_ptr = c(0L, cumsum(nn)), adj_col_idx = unlist(nb) - 1L,
              n_neighbors = nn, sigma_grid = c(0.3, 0.7, 1.4))
fit <- tulpa_nested_laplace_joint(
  responses = list(occ = mk_arm(200L, "binomial"), pos = mk_arm(200L, "gaussian")),
  prior = prior)
fit$theta_mean        # shared field amplitude, integrated across both arms


Fit a beta-regression model via NUTS (joint sampling of beta + log_phi)

Description

Bayesian counterpart to tulpa_laplace_beta(). Routes the Beta GLM through tulpa's full NUTS backend via the generic LikelihoodSpec interface, sampling the precision phi jointly with the regression coefficients on the log scale. NUTS performs exact marginalisation over phi, replacing the Brent outer-opt in tulpa_laplace_beta() and the deterministic nested-Laplace grid that would otherwise integrate over the Beta dispersion.

Mean-precision parameterisation, default logit link: y_i ~ Beta(mu_i * phi, (1 - mu_i) * phi) with mu_i = 1 / (1 + exp(-eta_i)) and eta_i = X_i %*% beta.

Usage

tulpa_nuts_beta(
  y,
  X,
  beta_prior = .tulpa_default_beta_prior("beta_nuts"),
  log_phi_prior_sd = 3,
  log_phi_init = 0,
  control = list()
)

Arguments

y

Response vector, strictly in ⁠(0, 1)⁠.

X

Fixed-effects design matrix.

beta_prior

Fixed-effect prior as list(mean, sd): a mean-zero (mean = 0) Gaussian on each coefficient with SD sd (default the engine default, prior_normal(0, 2.5)).

log_phi_prior_sd

Prior SD on log(phi) (log_phi ~ N(0, log_phi_prior_sd)). Default 3 (very weak; covers phi from ~0.001 to ~1000 within +-2 SD).

log_phi_init

Starting value for log(phi). Default 0 (i.e. phi = 1); a method-of-moments warm start can speed warmup in highly concentrated regimes.

control

A named list of numerical / sampler knobs (statistical arguments stay in the signature): n_iter (default 2000), n_warmup (default 1000), max_treedepth (default 10), adapt_delta (default 0.8), seed (NULL draws from the session RNG), verbose (default FALSE).

Value

A list with:

See Also

tulpa_laplace_beta() for the Laplace + Brent point estimate; tulpa_laplace() for the underlying Laplace engine.

Examples

set.seed(1)
n <- 150L
X <- cbind(1, rnorm(n))
mu <- plogis(X %*% c(0.2, 0.7)); phi <- 8
y <- rbeta(n, mu * phi, (1 - mu) * phi)

fit <- tulpa_nuts_beta(y, X, control = list(n_iter = 500L, n_warmup = 250L))
colMeans(fit$draws)


Sample an SPDE GLM via NUTS, optionally jointly over Matern hypers

Description

Bayesian counterpart to fit_spde() / laplace_spde_at(). Routes the SPDE-augmented GLM through tulpa's full NUTS backend via the generic LikelihoodSpec interface. Two modes:

Usage

tulpa_nuts_spde(
  y,
  X,
  spatial,
  family = c("gaussian", "poisson", "binomial", "gamma", "neg_binomial_2", "beta"),
  n_trials = NULL,
  joint = FALSE,
  range = NULL,
  sigma = NULL,
  prior_range = NULL,
  prior_sigma = NULL,
  log_kappa_init = NULL,
  log_tau_init = NULL,
  beta_prior = .tulpa_default_beta_prior("spde_nuts"),
  log_phi_prior_sd = 3,
  log_phi_init = 0,
  control = list()
)

Arguments

y

Response vector. Family-specific:

  • gaussian: any real

  • poisson, neg_binomial_2: non-negative integers

  • binomial: non-negative integers in ⁠[0, n_trials]⁠

  • gamma: strictly positive reals

  • beta: strictly in ⁠(0, 1)⁠

X

Fixed-effects design matrix.

spatial

A tulpa_spatial object from spatial_spde() / spatial_spde_custom() – supplies the FEM matrices (C0, G1) and projection (A) plus the smoothness nu.

family

One of "gaussian", "poisson", "binomial", "gamma", "neg_binomial_2", "beta".

n_trials

Integer vector for family = "binomial" (else ignored).

joint

Logical. FALSE (default) conditions on fixed ⁠(range, sigma)⁠. TRUE activates joint sampling of ⁠(log_kappa, log_tau)⁠ with the PC prior from prior_range, prior_sigma.

range, sigma

Fixed-hyper mode only. Matern range and marginal SD on the field. Default to the SPDE prior medians (spatial$prior_range[1], spatial$prior_sigma[1]).

prior_range, prior_sigma

Joint mode only. PC prior anchors as c(value, alpha) pairs:

  • prior_range = c(r0, a_r) encodes P(range < r0) = a_r

  • prior_sigma = c(s0, a_s) encodes P(sigma > s0) = a_s Default to the spec's anchors (spatial$prior_range, spatial$prior_sigma) when those are length-2 PC anchors.

log_kappa_init, log_tau_init

Joint mode only. Initial values for the hyper slots. Default to the value implied by the PC anchor's ⁠(r0, s0)⁠ pair via ⁠kappa = sqrt(8 nu) / r0⁠, ⁠tau = 1 / (sqrt(4 pi) * kappa * s0)⁠.

beta_prior

Fixed-effect prior as list(mean, sd): a mean-zero (mean = 0) Gaussian on each coefficient with SD sd (default the engine default, prior_normal(0, 2.5)).

log_phi_prior_sd

Prior SD on log(phi). Role of phi is family-specific:

  • gaussian: phi is the residual SD (sampled jointly)

  • gamma: phi is the Gamma shape (sampled jointly)

  • neg_binomial_2: phi is the NB size r (sampled jointly)

  • beta: phi is the Beta precision (sampled jointly)

  • poisson, binomial: log_phi is held tight and ignored downstream.

log_phi_init

Starting value for log(phi).

control

A named list of numerical / sampler knobs (statistical arguments stay in the signature): n_iter (default 2000), n_warmup (default 1000), max_treedepth (default 10), adapt_delta (default 0.8), seed (NULL draws from the session RNG), verbose (default FALSE), noncenter (fixed-hyper only, default TRUE: sample the mesh field in a non-centered ⁠v = L^{-T} z⁠ parameterisation – the same target density, a priori isotropised; ignored when joint = TRUE), and mass_matrix (NUTS metric: "auto" (default) picks a dense metric at ⁠<= 200⁠ parameters and diagonal above, capturing the fixed-effect / field cross-curvature that corrects the intercept marginal; "diag", "dense", "block_diag" force the choice).

Value

A list with draws (matrix ⁠n_samples x n_params⁠), means, phi_summary (where applicable), accept_prob, divergent, treedepth, epsilon, joint_hypers, plus the supplied spatial spec. In joint = TRUE mode additionally: w_draws (transformed z -> w per draw), range_draws, sigma_draws, kappa_draws, tau_draws, and range_summary, sigma_summary (mean/median/5%/95% quantiles).

See Also

fit_spde() for the Laplace counterpart and the nested-Laplace path over (range, sigma).

Examples

## Not run: 
# SPDE-field NUTS. `spatial` is an SPDE spec built from a mesh, e.g.
# spatial_spde(~ x + y, df). See fit_spde() for the Laplace analogue.
fit <- tulpa_nuts_spde(y, X, spatial = spatial_spde(~ x + y, df))

## End(Not run)

Ordinal (ordered K-class) cumulative-logit regression via Laplace

Description

Fits a proportional-odds cumulative-logit model for an ordered factor response: ⁠P(y <= j | x) = plogis(c_j - x'beta)⁠ with ordered cutpoints ⁠c_1 < ... < c_{K-1}⁠. The penalized mode (a ridge prior on the coefficients and cutpoints) is found by L-BFGS and summarized by a Laplace approximation. There is no separate intercept – the cutpoints carry the baseline levels.

Usage

tulpa_ordinal(
  formula,
  data,
  link = c("logit", "probit"),
  beta_prior = .tulpa_default_beta_prior("ordinal"),
  cut_prior_sd = 10,
  control = list()
)

Arguments

formula

Model formula; the response must be an ordered (or coercible) factor with >= 3 levels. An intercept in formula is dropped.

data

A data frame.

link

Cumulative link: "logit" (proportional odds, default) or "probit".

beta_prior

Fixed-effect prior as list(mean, sd): a mean-zero (mean = 0) Gaussian ridge on every coefficient with scalar SD sd (default the engine default, prior_normal(0, 2.5)).

cut_prior_sd

SD of the mean-zero Gaussian ridge prior on the cutpoint parameters (default 10).

control

List of numerical knobs: max_iter (default 200), n_draws (default 2000), seed.

Value

A tulpa_fit (subclass tulpa_ordinal) with coef (covariate effects), cutpoints, vcov, draws, log_marginal, levels.

See Also

tulpa_multinomial() for the nominal (unordered) case.

Examples


set.seed(1)
n <- 400L; x <- rnorm(n)
cuts <- c(-1, 0.5, 2); eta <- 0.8 * x
Fm <- plogis(outer(-eta, cuts, "+")); P <- cbind(Fm, 1) - cbind(0, Fm)
y <- ordered(apply(P, 1, function(pr) sample.int(4L, 1L, prob = pr)))
fit <- tulpa_ordinal(y ~ x, data = data.frame(y = y, x = x))
fit$coefficients; fit$cutpoints


Parse a mixed-model formula

Description

Decomposes a formula into fixed effects and random effects by walking the formula's abstract syntax tree. This is structural recursion with pattern matching on bar terms – no string manipulation.

Usage

tulpa_parse_formula(formula)

Arguments

formula

A formula object (e.g., y ~ x + (1 | group))

Value

A list with:

Examples

pf <- tulpa_parse_formula(y ~ x1 + x2 + (1 | group) + (x1 || site))
pf$response        # "y"
pf$fixed_formula   # y ~ x1 + x2


Probability integral transform from a predictive CDF

Description

The generic, family-agnostic half of a PIT residual check: the model package supplies the posterior-predictive CDF evaluated at each observation (a ⁠[n_draws x n_obs]⁠ matrix, or a draw-averaged ⁠[n_obs]⁠ vector), and this returns the PIT value per observation. For a discrete or mixed response (a hurdle has a point mass at zero) supply the left limit cdf_lower (P(Y < y)); the randomized PIT then draws one uniform per observation and interpolates ⁠F(y^-) + U (F(y) - F(y^-))⁠, which is uniform under a correct model. With cdf_lower = NULL the response is treated as continuous and the PIT is the draw-averaged CDF.

Usage

tulpa_pit(cdf, cdf_lower = NULL, jitter = TRUE)

Arguments

cdf

Posterior-predictive CDF at the observed value, P(Y <= y). A ⁠[n_draws x n_obs]⁠ matrix (averaged over draws here) or an ⁠[n_obs]⁠ vector.

cdf_lower

Optional left-limit CDF P(Y < y), same shape as cdf, for the randomized PIT of a discrete / mixed response.

jitter

If TRUE (default) and cdf_lower is NULL, add a tiny uniform jitter to break ties from a discretized CDF; ignored when cdf_lower is supplied (the interpolation already randomizes).

Value

Numeric vector of length n_obs of PIT values in ⁠[0, 1]⁠.

See Also

tulpa_criteria()


Posterior draws from a nested-Laplace fit

Description

Draw from the outer-grid mixture posterior of a nested-Laplace fit – the engine analogue of inla.posterior.sample(). Each draw picks an outer-grid cell k ~ Categorical(weights) and then samples that cell's inner Gaussian, so the draws are i.i.d. samples from ⁠sum_k w_k N(m_k, V_k)⁠.

Sampling the mixture is the faithful primitive for marginalizing nonlinear derived quantities (e.g. plogis(eta_2) - plogis(eta_1), expected-cover products p * mu): compute the derived quantity per draw, then summarize. Collapsing the grid to a single moment-matched Gaussian biases skewed or multimodal-over-grid posteriors.

Usage

tulpa_posterior_draws(fit, idx = NULL, n = 1000, ...)

Arguments

fit

A nested-Laplace fit (tulpa_nested_laplace() or tulpa_nested_laplace_joint()).

idx

Optional integer vector of 1-based indices into whatever a draw covers for this backend (see above); NULL (default) returns all of them.

n

Number of posterior draws (default 1000).

...

Unused; for S3 compatibility.

Value

A numeric matrix ⁠[n x length(idx)]⁠, one row per draw. Carries attr(., "draws_kind") = "iid" (consistent with the draws-provenance gate), attr(., "cells") – the outer-grid cell index each row was drawn from – and attr(., "scope"), which of the two representations above the columns are.

What a draw covers

It depends on which representation the backend retained, and the returned matrix says so in its scope attribute.

tulpa_nested_laplace_joint

the FULL latent vector – per-arm fixed effects, per-arm random effects, then the latent field(s) – because the joint fit retains each cell's sparse precision over that vector (control$store_Q = TRUE). scope is "latent".

tulpa_nested_laplace (single-block)

the FIXED-EFFECT block. This backend inverts each cell's precision into the marginal fixed-effect block and releases the precision itself, so the latent field is not part of the retained per-cell Gaussian and cannot be sampled from the fit. scope is "fixed".

See Also

tulpa_nested_laplace(), tulpa_nested_laplace_joint(), posterior_sample()


Posterior draws from a joint nested-Laplace fit

Description

The tulpa_posterior_draws() method for a joint nested-Laplace fit (tulpa_nested_laplace_joint()). Each draw picks an outer-grid cell k ~ Categorical(weights) and then samples the inner latent vector from the constrained Gaussian N(m_k, V_k) at that cell, where m_k is the cell's inner mode and V_k is the inner-Laplace covariance with the ICAR / BYM2 sum-to-zero field constraint imposed (conditioning by kriging). The draws are i.i.d. samples from ⁠sum_k weights_k * N(m_k, V_k)⁠.

Usage

## S3 method for class 'tulpa_nested_laplace_joint'
tulpa_posterior_draws(fit, idx = NULL, n = 1000, ...)

Arguments

fit

A tulpa_nested_laplace_joint fit (single-block or multi-block). The fit must have been produced with control$store_Q = TRUE so the per-grid sparse precision ⁠Q_csc_*_per_grid⁠ is available.

idx

Optional integer vector of 1-based latent indices to return. NULL (default) returns the full latent vector. The latent vector stacks per-arm fixed effects, per-arm random effects, then the latent field(s); use fit$arm_layout (beta_start, re_start, phi_start / theta_start / field_starts, all 0-based) to map a sub-block to indices.

n

Number of posterior draws (default 1000).

...

Unused; for S3 compatibility.

Details

The constrained draw at cell k uses the sparse Cholesky of the stored precision Q_k and the conditioning-by-kriging correction

z_c = z - Q_k^{-1} A^\top (A Q_k^{-1} A^\top)^{-1} A z,

where z ~ N(0, Q_k^{-1}) and A stacks the field sum-to-zero rows (one for an ICAR / CAR field; two – structured and unstructured – for a BYM2 field; one per spatial block in a multi-block fit). The returned draw is m_k + z_c, restricted to idx. Because m_k already satisfies the constraint (the inner solve centres the field), the mean is left unchanged and only the covariance is constrained, so the per-cell marginal matches the inner-Laplace constrained covariance exactly.

Cells with zero outer-grid weight (e.g. pruned cells) or no stored Q are dropped and the remaining weights renormalized. A degenerate single-cell grid (quadrature ESS 1) returns draws from that cell's N(m_1, V_1).

Value

A numeric matrix ⁠[n x length(idx)]⁠ of latent draws, one row per draw, columns named ⁠x<idx>⁠. Carries attr(., "draws_kind") = "iid" (consistent with the draws-provenance gate), attr(., "cells") – the outer-grid cell index each row was drawn from – and attr(., "scope") = "latent".

See Also

tulpa_posterior_draws(), tulpa_nested_laplace_joint(), posterior_sample()


Power-scaling prior / likelihood sensitivity

Description

Local power-scaling sensitivity (Kallioinen et al. 2024): how much each fixed-effect posterior moves when the prior or the likelihood is raised to a power alpha near 1. The existing draws are importance-reweighted by exp((alpha - 1) * log_component) (PSIS-smoothed via tulpa_psis(); no refits), and the sensitivity is the gradient of the cumulative Jensen-Shannon distance between the base and power-scaled posteriors with respect to log2(alpha).

Values above threshold flag sensitivity. High on both the prior and likelihood components indicates potential prior-data conflict; high prior with low likelihood indicates a strong prior / weak likelihood.

When the fit recorded per-draw hyperparameter log-prior values at draw-synthesis time (⁠$hyper_log_prior_draws⁠, stored by the nested-Laplace mixture paths such as tulpa_re_cov_nested() and the tulpa() random-slope redirect), a hyperparameter column reports the power-scaling sensitivity of the hyperparameter prior by the same reweighting; NA otherwise.

Usage

tulpa_powerscale_sensitivity(
  fit,
  data,
  prior = NULL,
  lower_alpha = 0.99,
  upper_alpha = 1.01,
  threshold = 0.05
)

Arguments

fit

A tulpa_fit fitted through tulpa() (fixed-effect / GLMM; spatial / temporal-field fits are rejected).

data

The data frame the model was fit to.

prior

The Gaussian fixed-effect prior the fit used, as list(mean =, sd =) (scalars recycled). Required for the prior component; omit to compute the likelihood component only.

lower_alpha, upper_alpha

Power-scaling grid endpoints for the gradient (defaults 0.99 / 1.01, as in priorsense).

threshold

Sensitivity flag threshold (default 0.05).

Value

A data frame with one row per fixed-effect parameter and columns variable, prior, hyperparameter, likelihood, diagnosis.

References

Kallioinen, Paananen, Buerkner & Vehtari (2024). Detecting and diagnosing prior and likelihood sensitivity with power-scaling. Statistics and Computing 34:57. Nguyen & Vreeken (2015). Non-parametric Jensen-Shannon divergence. ECML PKDD.

See Also

tulpa_psis(), tulpa_criteria().

Examples


set.seed(1)
d <- data.frame(x = rnorm(150))
d$y <- rpois(150, exp(0.5 + 0.7 * d$x))
fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace",
             beta_prior = list(mean = 0, sd = 5))
tulpa_powerscale_sensitivity(fit, data = d, prior = list(mean = 0, sd = 5))


Prior specification for tulpa models

Description

Specify priors for model parameters. Supports both PC (penalized complexity) priors for variance components and standard distributions for other parameters.

Usage

tulpa_priors(
  beta = NULL,
  sigma = NULL,
  phi = NULL,
  rho_temporal = NULL,
  rho_spatial = NULL
)

Arguments

beta

Prior for fixed effects. Default: prior_normal(0, 2.5).

sigma

Prior for random effect SDs. Default: PC prior with P(sigma > 1) = 0.01.

phi

Prior for overdispersion parameter. Default: PC prior with P(phi > 10) = 0.01.

rho_temporal

Prior for temporal autocorrelation. Default: prior_beta(2, 2) centered at 0.5.

rho_spatial

Prior for spatial proportion (BYM2). Default: prior_beta(1, 1) (uniform).

Details

PC priors (Simpson et al., 2017) provide principled regularization that:

For other parameters, standard distributions are available via helper functions: prior_normal(), prior_half_normal(), prior_half_cauchy(), prior_gamma(), prior_beta(), prior_exponential().

Value

A tulpa_priors object

References

Simpson, D., Rue, H., Riebler, A., Martins, T. G., & Sorbye, S. H. (2017). Penalising model component complexity: A principled, practical approach to constructing priors. Statistical Science, 32(1), 1-28.

Examples

# Default priors
tulpa_priors()

# Custom fixed effect prior
tulpa_priors(beta = prior_normal(0, 1))

# Tighter random effect prior
tulpa_priors(sigma = prior_pc(U = 0.5, alpha = 0.01))

# Half-Cauchy for random effect SD
tulpa_priors(sigma = prior_half_cauchy(2.5))

# Informative prior for temporal correlation
tulpa_priors(rho_temporal = prior_beta(5, 2))  # Prior mode at ~0.8


Profile the inner Laplace solve by phase

Description

Times the sparse joint Laplace solver one phase at a time – scatter (the Hessian and gradient assembly), factorize (numeric Cholesky), eta, line search, and the rest – and returns the breakdown as a data frame. The accumulator aggregates across the parallel outer-grid worker threads, so the reported times cover the whole fit rather than only the calling thread.

Usage

tulpa_profile(expr, sort = TRUE)

Arguments

expr

An expression that runs a fit (for example a call to tulpa_nested_laplace_joint()). Evaluated once, after the profile counters are reset.

sort

Logical; order rows by descending time. Default TRUE.

Details

Use it to settle where a per-cell solve spends its time, e.g. whether a slow joint occu_cover() fit is bound by the assembly scatter or the Cholesky factorize:

  p <- tulpa_profile(
    tulpa_nested_laplace_joint(..., control = list(integration = "ccd"))
  )
  print(p)            # rows ordered by time; scatter vs factorize at top
  fit <- attr(p, "value")

Value

A data frame with one row per phase and columns phase, seconds, calls, ms_per_call (mean wall time per phase call), and share (fraction of total timed seconds). The fit result is attached as the "value" attribute.

Examples


set.seed(1)
n <- 200L; X <- cbind(1, rnorm(n))
y <- rbinom(n, 1, plogis(X %*% c(0, 0.5)))
tulpa_profile(tulpa_laplace(y, rep(1L, n), X, family = "binomial"))


Pareto-smoothed importance sampling

Description

Smooths a set of importance ratios by replacing the largest weights with the order statistics of a generalized-Pareto fit to their upper tail, and returns the Pareto shape diagnostic pareto_k together with the smoothed (normalized) log weights and their importance-sampling effective sample size. pareto_k estimates the number of finite moments of the raw weight distribution: ⁠< 0.5⁠ is good (finite variance). The usable upper boundary is sample-size dependent, min(1 - 1/log10(S), 0.7) for S draws (Vehtari et al. 2024): about 0.565 at S = 200, reaching the 0.7 cap only past S ~ 2154. Above it the proposal cannot be reliably corrected to the target.

Usage

tulpa_psis(log_ratios, tail_points = NULL)

Arguments

log_ratios

Numeric vector of (unnormalized) log importance ratios ⁠log p_target(x) - log q_proposal(x)⁠ evaluated at draws x ~ q.

tail_points

Number of upper-tail order statistics for the generalized-Pareto fit, or NULL (default) for the automatic PSIS rule ceil(min(0.2 * S, 3 * sqrt(S))). An explicit value is an expert tail-threshold control, capped at floor(0.2 * S) so the fit stays an extreme tail; it is NOT a precision knob (raise the draw count for a tighter shape estimate).

Value

A list with pareto_k (the tail shape, NA if the sample is too small to fit), is_ess (importance-sampling effective sample size, 1 / sum(w^2) on the normalized smoothed weights), log_weights (the normalized smoothed log weights), tail_len (the tail size used), and tail_smoothed (FALSE when the tail kept its raw log ratios because the generalized-Pareto fit was not attempted or returned a shape / scale the quantile function is undefined at; pareto_k then reports the attempted fit and does not describe the returned weights).

References

Vehtari, Simpson, Gelman, Yao & Gabry (2024). Pareto smoothed importance sampling. JMLR 25(72):1-58.

See Also

diagnostics() for the fit-level diagnostic front door.

Examples

set.seed(1)
# Well-behaved importance ratios: k-hat is small.
ps <- tulpa_psis(rnorm(2000))
ps$pareto_k
ps$is_ess

Adaptive Gauss-Hermite refinement of a grouped random-effect covariance

Description

Refines a generalized linear mixed model's fixed effects and random-effect covariance by replacing the per-group Laplace integral with n_quad-point adaptive Gauss-Hermite quadrature (AGHQ). At n_quad = 1 this is the joint Laplace (glmer nAGQ = 1); higher n_quad reduces the small-cluster attenuation of the variance components for binary / count data. Unlike agq_fit() (intercept-only RE, built-in binomial/poisson/gaussian likelihoods), this engine is callback-driven: the caller supplies the per-group conditional likelihood, so a custom marginal (e.g. a latent-state-integrated occupancy / detection likelihood, or the latent-abundance-integrated N-mixture marginal) refines through the same quadrature.

This is an engine block, not a front door: model packages call it programmatically, so its tuning knobs (max_iter, n_quad, keep) sit in the signature rather than in a control list.

The engine is structure-agnostic. It integrates the per-group marginal

M_g = \int \exp\{\ell_g(b_g)\}\, N(b_g; 0, \Sigma)\, db_g,

where b_g is the group's random-effect vector (dimension \sum_m c_m over the RE terms) and \ell_g(b_g) is the group's conditional log-likelihood when its linear predictors are perturbed by b_g. How b_g enters the likelihood – through one linear predictor, or through several coupled arms at different observation granularities (e.g. a per-site abundance arm and a per-visit detection arm sharing a species grouping) – lives entirely in the callback. The engine only needs, per group, the value / gradient / Hessian of \ell_g in b_g (for the mode) and \ell_g at the quadrature nodes (for the sum). The fixed parameters theta and the log-Cholesky coordinates of \Sigma are optimized jointly on \sum_g \log M_g; standard errors come from the exact-marginal Hessian.

Two callback forms select the structure (supply exactly one):

Scope: one shared grouping factor across all RE terms (the per-group integral factorizes). The total RE dimension per group should be small (the quadrature grid is n_quad^dim).

Usage

tulpa_re_aghq(
  theta0,
  re_terms,
  Sigma0,
  make_site = NULL,
  make_group = NULL,
  oracle = NULL,
  n_obs = NULL,
  keep = NULL,
  n_quad = 9L,
  lkj_eta = 1,
  theta_prior_sd = Inf,
  sigma_prior = NULL,
  gradient = c("fd", "analytic"),
  max_iter = 200L
)

Arguments

theta0

Initial fixed-parameter vector. The engine optimizes these jointly with the RE covariance; the callback interprets them.

re_terms

A list of RE term specs (or one spec), each defining a covariance block: n_coefs (block dimension c_m), optional correlated (default TRUE for c_m > 1; FALSE gives a diagonal block), and n_groups (shared across terms). For the make_site path each term also carries idx (1-based group index, length n_obs) and, for a slope block, Z (the ⁠n_obs x n_coefs⁠ design). For the make_group path the per-observation idx / Z are optional – the callback owns them – and the term needs only n_coefs / correlated / n_groups.

Sigma0

List of initial per-term covariance matrices (the EM estimate).

make_site

⁠function(theta)⁠ for the single-arm separable case, returning a list with: eta_re (length n_obs, the RE-arm fixed predictor), ⁠deriv = function(rows, eta)⁠ returning list(logL, d1, d2) (per-row marginal log-likelihood and its first/second derivatives w.r.t. the RE-arm predictor eta, used for the per-group mode), and ⁠lmat = function(rows, ETA)⁠ returning a ⁠length(rows) x ncol(ETA)⁠ matrix of per-observation log-likelihoods over the quadrature node columns. Supply this or make_group, not both.

make_group

⁠function(theta)⁠ for the general / multi-arm case, returning a list with two per-group closures (let d = sum(n_coefs) be the group RE dimension):

  • grad_hess(g, b) – for group g at RE value b (length d), the list list(logL, grad, negH): the group conditional log-likelihood \ell_g(b), its gradient \partial \ell_g/\partial b (length d), and the data-only observed information -\partial^2 \ell_g/\partial b^2 (⁠d x d⁠; the engine adds the \Sigma^{-1} prior curvature).

  • node_ll(g, B) – for group g, a numeric vector of length nrow(B) giving \ell_g at each quadrature node (rows of the ⁠nrow x d⁠ matrix B are candidate b vectors). The callback owns all arm / design / clamping bookkeeping. Supply this or make_site, not both.

oracle

Optional prebuilt native (compiled) oracle, an external pointer to a REGroupOracle (constructed in a consumer package's src/ via LinkingTo: tulpa against ⁠<tulpa/aghq_oracle.h>⁠). When supplied the engine drives it directly, with no per-group / per-node round trip into R, and neither make_site nor make_group is needed; re_terms, theta0 and Sigma0 must still describe the same layout the oracle exposes. The integration core is identical to the R-closure path.

n_obs

Number of observations (length of each term's idx). Required for the make_site path; ignored for make_group.

keep

Optional logical/integer mask of observations to include (default all; make_site path only). Rows outside keep are dropped from every group.

n_quad

Quadrature nodes per RE dimension. Either a single integer (default 9; 1 = Laplace) broadcast to every covariance block, or an integer vector of length length(re_terms) giving a per-block node count. The tensor grid then uses n_quad[b] nodes along every dimension of block b, for ⁠prod_b n_quad[b]^(dim_b)⁠ total nodes; a scalar reproduces the uniform grid exactly. Per-block orders let a heterogeneous stack spend fewer nodes on cheap scalar nuisance blocks than on the correlated coefficient blocks (e.g. c(3, 3, 2, 2) on blocks of dimension ⁠2, 2, 1, 1⁠ gives 3^2 * 3^2 * 2 * 2 = 324 nodes rather than 3^6 = 729).

lkj_eta

LKJ shape for an optional correlation penalty on each correlated block (log-density ⁠(eta - 1) log det R⁠, maximized at independence). 1 disables it; ⁠> 1⁠ regularizes a weakly-identified correlation off the boundary without touching the marginal SDs. The marginal SDs are otherwise unpenalized (pure ML), so the refinement debiases them rather than shrinking them.

theta_prior_sd

Optional Gaussian ridge SD on the fixed parameters theta (a mean-zero N(0, theta_prior_sd^2) prior, added to the optimized objective and hence the marginal Hessian). Inf (default) is pure ML on theta; a large finite value (e.g. 100) is a weak ridge that stabilizes a weakly-identified fixed effect without materially shifting the estimate.

sigma_prior

Optional Penalized-Complexity prior on the marginal standard deviations of one or more RE covariance blocks, added to the objective (and hence the marginal Hessian). NULL (default) is pure ML on the covariances – the refinement debiases the SDs rather than shrinking them. Otherwise a c(U, alpha) pair (P(sigma_i > U) = alpha, the same convention as re_cov_pc_lkj_prior()) applied to every block, or a list ⁠list(blocks = <integer indices>, prior_sigma = c(U, alpha))⁠ applied to the named blocks only. Reuses the exact PC log-prior + Jacobian of re_cov_pc_lkj_prior(). A weakly-identified variance component (e.g. a scalar dispersion / zero-inflation random effect at few groups) can drift to the boundary and flatten the marginal Hessian; a weak PC prior adds curvature there (the ⁠+ log sigma⁠ Jacobian repels sigma -> 0, the ⁠- lambda sigma⁠ term caps inflation), keeping the joint optimum non-singular without materially shifting an identified fit.

gradient

How stats::optim gets the gradient of the AGHQ objective. "fd" (default) lets optim finite-difference the objective – correct at every n_quad and the only option for the R-closure (make_site / make_group) paths. "analytic" supplies the Fisher-identity gradient (posterior-weighted theta-score plus the Sigma moment-matching residual), which avoids the per-coordinate objective re-solve and so is far cheaper for the quadrature debias. It requires a prebuilt native oracle (the only one exposing the theta-score) and n_quad > 1: being the gradient of the true marginal it omits the node-placement terms (O of the AGHQ truncation), so it agrees with the objective only as n_quad grows.

max_iter

Optimizer iteration cap (default 200).

Value

A list with: theta (refined fixed parameters), Sigma_list (refined per-term covariance), blup / blup_var (per-term ⁠n_groups x n_coefs⁠ posterior mean / variance of the RE), group_ok (logical, length n_groups: FALSE where that group's mode search or precision factorization failed, which is what the NA rows of blup, blup_var, blup_cov_g and blup_cross_g mean – a caller conditions its per-group reads on this rather than on trapping the accompanying warning), blup_cross (per-term ⁠n_groups x n_theta x n_coefs⁠ array: the mode/theta cross-Hessian block Bf, ⁠-d^2 ell_g / d theta db⁠ at each group's mode, in the same negative-Hessian sign convention as the posterior precision underlying blup_var – so a joint draw of theta and a group's RE b_g uses b_g | theta ~ N(blup_g - Cinv_g %*% t(Bf_g) %*% (theta_draw - theta), Cinv_g) with Cinv_g the group's ⁠n_coefs x n_coefs⁠ posterior covariance block. NA throughout when blup_cross_available is FALSE: the cross-Hessian needs the oracle's analytic theta_score, which the R-closure bridge (make_site / make_group) does not supply – only a prebuilt native oracle carries it), blup_cov_g / blup_cross_g (per-group lists, length n_groups, of the FULL joint posterior covariance (⁠d x d⁠, d = every RE term's width combined) and mode/theta cross-Hessian (⁠n_theta x d⁠) across ALL RE terms sharing that group – the superset blup_var/blup_cross reduce to a per-term diagonal block of when a group carries more than one term, since a group's terms are found jointly and can carry real posterior covariance BETWEEN terms (e.g. an abundance-arm and a detection-arm term sharing one grouping factor); same NA-when-unavailable rule as blup_cross), theta_cov / theta_se (fixed-parameter covariance / SE from the marginal Hessian), re_par / re_par_cov / re_par_se (the RE-covariance coordinates the optimizer carried – log-Cholesky for a full block, log-SD for a diagonal one – with their block of the same inverse Hessian, so ⁠SE(log sigma)⁠ is available for a boundary test on a weakly-identified variance component), re_par_layout (per block: label, nc, full, the index range into re_par and the coord names, so a caller does not reconstruct the packing), joint_cov (the whole (n_theta + n_chol) inverse Hessian), log_marginal (the AGHQ marginal log-likelihood at the optimum, excluding any ridge), n_quad, lkj_eta, converged, and counts (stats::optim's own function / gradient evaluation counts, so a caller reporting how much work the fit took has a number to report rather than NA). RE terms that do not share one grouping factor are an input error and stop. Three conditions warn and return NULL (caller keeps its prior fit): a singular / non-finite optimum, an objective that is already undefined at the starting parameters (some group's solve fails there, so there is nothing to descend), and an optimum whose objective is the failure sentinel rather than an attained marginal likelihood – the last two report which groups failed.

References

Pinheiro & Bates (1995). Approximations to the log-likelihood function in the nonlinear mixed-effects model. Journal of Computational and Graphical Statistics 4(1):12-35. Lewandowski, Kurowicka & Joe (2009). Generating random correlation matrices based on vines and extended onion method. Journal of Multivariate Analysis 100(9):1989-2001.

Examples


# A per-row-separable binomial GLMM marginal supplied through `make_site`.
l1pe <- function(x) ifelse(x > 0, x + log1p(exp(-x)), log1p(exp(x)))
make_binom_site <- function(X, y, nt) function(theta) {
  eta_fixed <- as.numeric(X %*% theta)
  list(eta_re = eta_fixed,
       deriv = function(rows, eta) {
         p <- plogis(eta)
         list(logL = y[rows] * eta - nt[rows] * l1pe(eta),
              d1 = y[rows] - nt[rows] * p, d2 = -nt[rows] * p * (1 - p))
       },
       lmat = function(rows, ETA) y[rows] * ETA - nt[rows] * l1pe(ETA))
}
set.seed(1)
ng <- 30L; npg <- 8L; n <- ng * npg
g <- rep(seq_len(ng), each = npg); x <- rnorm(n)
X <- cbind(1, x); nt <- rep(3L, n); u <- rnorm(ng, 0, 0.9)
y <- rbinom(n, nt, plogis(0.3 + 0.7 * x + u[g]))
fit <- tulpa_re_aghq(theta0 = c(0, 0),
                     re_terms = list(list(idx = g, n_groups = ng, n_coefs = 1L)),
                     Sigma0 = list(matrix(0.25, 1, 1)),
                     make_site = make_binom_site(X, y, nt), n_obs = n, n_quad = 5L)
sqrt(fit$Sigma_list[[1]][1, 1])     # adaptive-GHQ RE standard deviation


Gibbs estimation of random-effect covariances (exact-target debias)

Description

For one or more random-effects terms (e.g. (1 + x | g), (1 + x || g), or several terms together), estimate the random-effect covariances Sigma by sampling the exact joint posterior p(beta, {b}, {Sigma} | y) rather than fixing each Sigma at the Laplace mode. This removes the Laplace / PQL "approximation" bias that shrinks variance components low for binary and low-count responses with small groups.

Usage

tulpa_re_cov_gibbs(
  y,
  n_trials = NULL,
  X,
  re_terms,
  family = "binomial",
  phi = 1,
  prior_df = NULL,
  prior_scale = NULL,
  beta_prior = .tulpa_default_beta_prior("re_cov_gibbs"),
  control = list()
)

Arguments

y, n_trials, X, family, phi

Passed to the likelihood and to tulpa_laplace() for the pilot solve. n_trials = NULL defaults to 1.

re_terms

Either a single random-effect term or a list of them. Each term is a list with idx (1-based group index per observation), n_groups, n_coefs (c), Z (the ⁠n_obs x c⁠ RE design; only required when c > 1), and correlated (TRUE for a full Sigma, FALSE for a diagonal one; defaults to TRUE). An optional label / group_var names the block. Any supplied L / cov / sigma is ignored – Sigma is what this function samples.

prior_df

Inverse-Wishart prior degrees of freedom. Applied to every correlated block (default n_coefs + 1, the minimal proper choice) and as the scalar inverse-gamma shape for every diagonal block (default 2). Must leave each block's prior proper – unlike tulpa_re_cov_nested() / tulpa_eb() (hyperprior = "flat" by default), the Sigma_m | b_m conjugate draw here needs a proper Inverse-Wishart to sample from, so an improper flat prior is not an option; the minimal-df default is the closest analogue this sampler can offer.

prior_scale

Inverse-Wishart prior scale matrix. Used for a block when its dimension matches (default diag(n_coefs)); otherwise the per-block default is used.

beta_prior

Gaussian fixed-effect prior as list(mean, sd) (default the engine default, prior_normal(0, 2.5)). Scalar mean / sd are recycled to ncol(X); a length-ncol(X) vector sets a per-coefficient prior.

control

A named list of numerical / tuning knobs (statistical arguments stay in the signature above). Recognized entries:

  • n_iter: recorded post-warmup sweeps (default 2000).

  • warmup: warmup (burn-in) sweeps, used for proposal-scale adaptation (default 1000).

  • thin: keep every thin-th recorded sweep (default 1).

  • seed: optional integer seed for reproducibility.

  • max_iter, tol, n_threads: pilot-solve controls (see tulpa_laplace()).

Details

Metropolis-within-Gibbs targeting p(beta, {b_m}, {Sigma_m} | y):

A single Laplace solve provides the starting values (beta, b) and the proposal shapes; the random-walk scales for the beta block and the per-term b blocks are adapted toward their target acceptance during burn-in (Robbins-Monro), then held fixed for the recorded sweeps. The covariance summary marginalizes the derived scale / correlation parameters over the posterior draws via the same machinery as tulpa_re_cov_nested().

For family = "gaussian" the response is already conditionally Gaussian, so there is no Laplace bias to remove; phi (the residual variance) is treated as known. The sampler still runs and is useful as a reference / for Sigma uncertainty.

Value

A list with:

References

Lewandowski, Kurowicka & Joe (2009). Generating random correlation matrices based on vines and extended onion method. Journal of Multivariate Analysis 100(9):1989-2001.

See Also

tulpa_re_cov_nested() for the grid-integration (summary-bias) fix; tulpa_laplace() for the pilot solve and per-group covariance blocks.

Examples


set.seed(1)
G <- 20L; per <- 12L; n <- G * per
grp <- rep(seq_len(G), each = per); x <- rnorm(n)
b <- cbind(rnorm(G, 0, 0.7), rnorm(G, 0, 0.5))     # random intercept + slope
eta <- -0.2 + 0.5 * x + b[grp, 1] + b[grp, 2] * x
y <- rbinom(n, 1L, plogis(eta))
re_term <- list(idx = grp, n_groups = G, n_coefs = 2L, Z = cbind(1, x),
                correlated = TRUE)
fit <- tulpa_re_cov_gibbs(y, rep(1L, n), cbind(1, x), re_term,
                          family = "binomial",
                          control = list(n_iter = 300L, warmup = 150L))
fit$Sigma_mean        # exact-debias RE covariance posterior mean


Nested-Laplace integration over random-effect covariances

Description

For one or more random-effects terms (e.g. (1 + x | g), (1 + x || g), or several terms together), integrate the Laplace marginal likelihood over the random-effect covariances Sigma instead of fixing them at point estimates. Reports weighted posterior summaries (mean, SD, median, 2.5\ Sigma and its derived scale (sigma_i) and correlation (rho_ij) parameters, marginalizing the joint posterior over a Sigma-grid.

This corrects the plug-in-MAP ("summary") bias: the mode of a skewed variance-component marginal is biased low relative to its median, so the headline summary should be the marginalized median, not the mode.

Usage

tulpa_re_cov_nested(
  y,
  n_trials = NULL,
  X,
  re_terms,
  family = "binomial",
  phi = 1,
  phi2 = NULL,
  prior_sigma = c(3, 0.05),
  eta = 2,
  hyperprior = c("flat", "pc_lkj"),
  log_prior_theta = NULL,
  beta_prior = NULL,
  offset = NULL,
  n_quad = 1L,
  X_zi = NULL,
  zi_prior_sd = 2.5,
  control = list()
)

Arguments

y, n_trials, X, family, phi

Passed to tulpa_laplace() for the inner solve. n_trials = NULL defaults to 1 (binary / single-trial).

re_terms

Either a single random-effect term or a list of them. Each term is a list with idx (1-based group index per observation), n_groups, n_coefs (c), Z (the ⁠n_obs x c⁠ RE design, e.g. cbind(1, x) for (1 + x | g); only required when c > 1), and correlated (TRUE for a full Sigma, FALSE for a diagonal one; defaults to TRUE). An optional label / group_var names the block in the output. Any L / cov / sigma field is ignored – Sigma is what this function integrates over.

phi2

Optional second dispersion, threaded into every inner tulpa_laplace() solve: the Student-t degrees of freedom (family = "t", default 4 when NULL) or the Tweedie variance power (family = "tweedie", required – a defaulted power would be a statistical decision the caller never made). A phi2 supplied for any other family errors rather than being ignored. It is conditioned on: the integration is over the random-effect covariances, not over phi2.

prior_sigma, eta

Hyperparameters of the PC + LKJ prior used when hyperprior = "pc_lkj" (see re_cov_pc_lkj_prior()): prior_sigma = c(U, alpha) with P(sigma_i > U) = alpha (default c(3, 0.05)) and LKJ shape eta (default 2). Ignored when hyperprior = "flat" or log_prior_theta is supplied.

hyperprior

"flat" (default) or "pc_lkj". "flat" integrates with log_prior_theta the zero function (flat in log(theta)), matching the nested-Laplace convention on every other scale axis in the engine. "pc_lkj" builds the PC + LKJ prior from prior_sigma / eta (the regularizer that keeps a variance component off the sigma = 0 boundary at small G). Ignored when log_prior_theta is supplied.

log_prior_theta

Optional ⁠function(theta)⁠ returning a scalar log prior density on the full stacked parameter vector, overriding hyperprior entirely. Default NULL, which defers to hyperprior.

beta_prior

Optional Gaussian prior on the fixed effects, threaded into every inner tulpa_laplace() solve (list(mean, sd)). NULL (default) keeps the weak built-in prior.

offset

Optional observation-level offset on the linear predictor (length length(y)), e.g. log(exposure) for a rate model. Not supported with n_quad > 1, which errors rather than dropping it.

n_quad

Quadrature order for the inner marginal. 1 (default) uses the joint-field Laplace inner solve (tulpa_laplace()). ⁠> 1⁠ refines the inner marginal with n_quad-point adaptive Gauss-Hermite quadrature (the tulpa_re_aghq() debias applied inside the Sigma integration), reducing the small-cluster variance attenuation for binary / low-count data. AGHQ requires a single shared grouping factor (the per-group integral must factorize); with crossed RE terms n_quad > 1 errors. When AGHQ is used the fixed effects are integrated, so the reported fixed-effect posterior is the marginal (ML-II) one rather than the joint-mode (PQL) estimate.

X_zi

Optional zero-inflation design matrix (length(y) rows), making the model a two-process mixture: each observation is a structural zero with probability ⁠plogis(X_zi beta_zi)⁠ and otherwise follows family. Paired with a zero-truncated family it is the hurdle model. The random effects enter the count predictor only, and the integration runs over the same covariance coordinates – the mixture changes the inner solve, not the parameters being integrated over. The ZI coefficients are reported alongside the count ones in coef() / vcov(), so the fixed block is ncol(X) + ncol(X_zi) wide. Needs n_quad = 1: the adaptive Gauss-Hermite inner marginal runs through a single-predictor oracle.

zi_prior_sd

Prior SD on beta_zi, keeping the logit identified where a level carries no zeros (the likelihood alone would send it to -Inf). Ignored when X_zi is NULL.

control

A named list of numerical / tuning knobs (statistical arguments stay in the signature above). Recognized entries:

  • integration: node layout, "ccd" (default, central-composite design, scales to larger total parameter count) or "grid" (full tensor product).

  • n_per_axis: points per parameter axis in the tensor grid (default 5); used only when integration = "grid".

  • span: half-width of the tensor grid in posterior standard deviations per whitened axis (default 3); grid only.

  • n_draws: posterior draws of the fixed effects synthesized from the node mixture (default 2000), exposed as draws for the generic tulpa_fit methods. The Sigma posterior is summarized directly from the integration nodes in posterior, independent of n_draws.

  • seed: optional integer seed for the fixed-effect draw synthesis.

  • diagnose_k: if TRUE (default), compute the outer Pareto k-hat accuracy diagnostic for the Gaussian proposal over the hyperparameters, returned as pareto_k.

  • k_samples: importance draws for the diagnose_k estimate (default 200).

  • max_iter, tol, n_threads: inner-solve controls (see tulpa_laplace()).

  • outer_maxit: iteration budget for the mode-finding step that centres the integration grid (default 500). Applies to the Nelder-Mead simplex used from two parameters up; the one-parameter case is bracketed by Brent. Exhausting the budget warns, since the nodes are then centred on wherever the optimizer stopped.

  • checkpoint: node checkpoint/resume spec list(path = , resume = ). Each completed CCD / grid node (one inner Laplace solve) is cached to path; a resume = TRUE run loads the finished nodes and re-solves only the rest. resume = FALSE starts fresh. A file written for different data, layout, or grid is rejected (fingerprint mismatch). Default NULL (off).

  • subspace_debias: subspace debias, FALSE by default. TRUE takes every default; a list overrides band (the inner-reliability floor a coordinate is selected at, default "ok"), idx (pin the corrected set explicitly, skipping the selector), probe (the latent indices scored, default the fixed effects), closure (FALSE, TRUE, or a partial-correlation threshold: grow the set by the precision-graph neighbours it is strongly coupled to), closure_max, and the sampler budget n_iter / warmup / thin. When the selected set is non-empty, each integration node reports the selected fixed-effect coordinates from a Metropolis sample of the exact conditional along the Gaussian-conditional-mean surface, and the rest from the Gaussian conditional given them; an EMPTY set leaves the fit bit-for-bit identical to the plain path. What was selected is recorded in subspace_debias on the returned fit.

Details

Each term is one covariance block. A correlated block ((1 + x | g)) is a full ⁠Sigma = L L'⁠ parameterized by its lower Cholesky factor in log-Cholesky coordinates (the log-diagonal and the strictly-lower entries of L, c(c+1)/2 values for a c-coefficient block), which keeps Sigma positive definite for every coordinate. An uncorrelated block ((1 + x || g)) is a diagonal Sigma parameterized by its c log standard deviations. A scalar (1 | g) term is the degenerate c = 1 block. Several blocks stack their parameters into one integration vector; a single-term model is the length-1 case.

Integration nodes live in the whitened stacked-parameter space, centred at the joint marginal-likelihood mode and rotated/scaled by the Cholesky of the mode's posterior covariance (solve(Hessian)), so points track the posterior ridge. Two node layouts are available via integration:

Each node k contributes integration weight proportional to Delta_k * exp(log_marginal(Sigma_k) + log_prior_theta(theta_k)), following the INLA convention ⁠int ~ sum_k Delta_k pi(theta_k)⁠.

The two layouts also decide how the reported median and 2.5\ of each derived quantity are read off the nodes. A tensor grid's uniform cells discretize the posterior density, so the cumulative node weights are a CDF and the summary is the weighted quantile. A CCD is a moment rule: its nodes sit where they reproduce the integrand's first two moments and carry no probability mass of their own, so the summary is moment-matched instead – the first two weighted moments on each quantity's own coordinate (log for a scale or a variance, atanh for a correlation, the identity for a covariance) define a Gaussian there whose quantiles are mapped back. Scale intervals are therefore positive and asymmetric, and correlation intervals stay inside ⁠(-1, 1)⁠. The mean and sd columns are the weighted moments under either layout.

By default (hyperprior = "flat") log_prior_theta is the zero function: flat in log(theta), the same convention the nested-Laplace spatial / temporal / RE-scale axes use (icar / rw1 / rw2 / ar1's tau / iid, none of which carry a hyperprior on their scale either – see vignette("priors")). Set hyperprior = "pc_lkj" to use the weakly-informative PC + LKJ hyperprior instead, built per block by re_cov_pc_lkj_prior() and summed over blocks (PC prior on each marginal SD via prior_sigma, LKJ prior on each correlated block's correlation matrix via eta), expressed in the same parameterization with the exact change-of-variables Jacobian. Supply a custom log_prior_theta function to override either default (then prior_sigma / eta / hyperprior are ignored); it must act on the full stacked parameter vector. tulpa_eb() shares this same objective and the same default, so tulpa_eb()$theta_hat and tulpa_re_cov_nested()$theta_hat stay the same estimate on the same data under either setting.

Value

A list with:

References

Rue, Martino & Chopin (2009). Approximate Bayesian inference for latent Gaussian models by using integrated nested Laplace approximations. JRSS-B 71(2):319-392. Lewandowski, Kurowicka & Joe (2009). Generating random correlation matrices based on vines and extended onion method. Journal of Multivariate Analysis 100(9):1989-2001.

See Also

tulpa_laplace() for the inner solve; tulpa_nested_laplace() for the analogous outer integration over spatial / temporal prior hyperparameters.

Examples


set.seed(1)
G <- 20L; per <- 12L; n <- G * per
grp <- rep(seq_len(G), each = per); x <- rnorm(n)
b <- cbind(rnorm(G, 0, 0.7), rnorm(G, 0, 0.5))     # random intercept + slope
eta <- -0.2 + 0.5 * x + b[grp, 1] + b[grp, 2] * x
y <- rbinom(n, 1L, plogis(eta))
re_term <- list(idx = grp, n_groups = G, n_coefs = 2L, Z = cbind(1, x),
                correlated = TRUE)
fit <- tulpa_re_cov_nested(y, rep(1L, n), cbind(1, x), re_term,
                           family = "binomial")
fit$Sigma_mean        # marginalized RE covariance


Selective refit of high-Pareto-k observations (reloo)

Description

PSIS-LOO with exact refits where the importance sampling is unreliable: observations whose Pareto k-hat exceeds k_threshold are re-scored by refitting the model without that observation (through the fit's stored tulpa() call, as in tulpa_kfold()) and evaluating the exact held-out log predictive density. All other observations keep their PSIS-LOO value, so the cost is one refit per flagged observation rather than per fold.

The same restrictions as tulpa_kfold() apply: fixed-effect / GLMM fits only (subsetting breaks a spatial / temporal field), and held-out random-effect groups contribute at their prior mean.

Usage

tulpa_reloo(
  object,
  data,
  k_threshold = .nl_diag("k_usable"),
  n_trials = NULL,
  ndraws = NULL
)

Arguments

object

A tulpa_fit fitted through tulpa() (must carry ⁠$call⁠).

data

The data frame the model was fit to.

k_threshold

Pareto k-hat above which an observation is refit (default 0.7, the standard PSIS reliability gate).

n_trials

Optional binomial denominators (length nrow(data)); defaults to the trials stored on the fit, else 1.

ndraws

Number of posterior draws used for the PSIS-LOO baseline (defaults to all stored draws, or 400 on the draw-free Laplace tier).

Value

A list with elpd_loo (corrected), se_elpd_loo, looic, pointwise (per-observation elpd, exact at the refit observations), reloo_idx (indices refit), pareto_k (the original k-hat values), and k_threshold.

See Also

tulpa_kfold() for the full refit-CV; tulpa_criteria() for PSIS-LOO / WAIC without refits.

Examples


set.seed(1)
d <- data.frame(x = rnorm(120))
d$y <- rpois(120, exp(0.4 + 0.6 * d$x))
fit <- tulpa(y ~ x, data = d, family = "poisson", mode = "laplace")
rl  <- tulpa_reloo(fit, data = d)
rl$elpd_loo


Fit a fixed-effect GLM with a model-agnostic sampler kernel

Description

Drives one of tulpa's ModelData sampler kernels – NUTS ("hmc"), elliptical slice sampling ("ess"), SGHMC ("sghmc"), SGLD ("sgld"), MCLMC ("mclmc"), sequential Monte Carlo ("smc"), or variational inference ("vi") – on a fixed-effect GLM. The model (design + per-observation likelihood) is built once through the same built-in-family scaffold the single-point Laplace fit uses, so no likelihood / link logic is duplicated.

Usage

tulpa_sample_glmm(
  y,
  n_trials,
  X,
  family,
  backend,
  phi = 1,
  phi2 = NULL,
  offset = NULL,
  fixed_names = NULL,
  re_spec = NULL,
  spatial_spec = NULL,
  temporal_spec = NULL,
  svc_spec = NULL,
  tvc_spec = NULL,
  zi_spec = NULL,
  sigma_re_scale = 2.5,
  sigma_beta = .tulpa_prior_sd("sample_glmm"),
  warm_start = NULL,
  control = list()
)

Arguments

y

Response vector.

n_trials

Binomial denominators (or NULL -> all 1).

X

Fixed-effect design matrix (nrow(X) == length(y)).

family

Character family name (see family_names()).

backend

One of "hmc", "ess", "sghmc", "sgld", "mclmc", "smc", "vi".

phi

Dispersion/precision passed to the family (held fixed). The kernel parameterization: for gaussian / lognormal this is the residual SD (the tulpa() front door passes sqrt(phi), its phi being the variance); for t the scale.

phi2

Optional second dispersion: the Student-t degrees of freedom (family = "t"; default 4 when NULL).

offset

Optional fixed additive term on the linear predictor (⁠eta = offset + X beta⁠), length length(y); NULL -> no offset.

fixed_names

Optional fixed-effect names for the draw columns.

re_spec

Optional random-effect spec: a list with idx (list of per-term 1-based group-index vectors), ngroups, ncoefs, correlated (per-term), and Z (per-term RE design or NULL). NULL -> no RE.

spatial_spec

Optional areal spatial spec: a list with type ("icar"/"bym2"), spatial_idx, n_spatial_units, adj_row_ptr, adj_col_idx, n_neighbors, and scale_factor (BYM2). NULL -> none.

temporal_spec

Optional temporal spec: a list with type ("rw1"/"rw2"/"ar1"), time_idx, n_times, n_groups, group_idx, and cyclic. NULL -> none.

sigma_re_scale

Half-Cauchy scale for the RE / BYM2 standard-deviation hyperprior (sampled jointly with the latent effects).

warm_start

Optional list(init, inv_metric_diag) seeding the NUTS kernel: init an ⁠n_chains x total_params⁠ matrix of initial positions, one row per chain, and inv_metric_diag a positive vector of length total_params used as the starting inverse mass (warmup adaptation still runs from it). Build it with .build_warm_start() against cpp_tulpa_glmm_layout() rather than by hand – the entries are positional and the layout owns the positions. Only the NUTS/HMC kernel takes one; any other backend errors rather than sampling from the default start.

control

List of kernel tuning knobs (n_iter, warmup, seed, sigma_beta, n_chains, max_treedepth, adapt_delta, epsilon, L, batch_size, alpha, n_particles, n_mcmc_steps, ess_threshold, vi_variant, vi_mc_samples, vi_max_iter, vi_max_grad_norm, n_draws, verbose, mass_matrix).

mass_matrix selects the NUTS/HMC metric: "diag" (the default), "dense", "block_diag", or "auto". Under "auto" the kernel reads the parameter layout and gives each correlated hyperparameter group its own small dense block – the BYM2 and GP ⁠(log sigma, phi)⁠ pairs, the multiscale-temporal variances, a correlated random-slope term's Cholesky coordinates – while an ICAR or latent-factor model small enough for the O(p^2) per-step cost takes a full dense metric. "block_diag" additionally blocks the temporal-GP and HSGP hyperparameter pairs, which "auto" leaves to the diagonal. Backends other than "hmc" carry no metric and reject a non-default value.

vi_max_iter and vi_mc_samples both bound a loop that has to run at least once – the optimisation loop whose ELBO history is the fit's only record, and the reparameterisation average every gradient divides by – so values below 1 are rejected. vi_max_grad_norm (default 10) is the gradient-norm clip applied before every Adam step.

epsilon pins the step size on the stochastic-gradient backends: "sghmc" runs its warmup step-size adapter only when no epsilon is supplied, and "sgld" runs its polynomial decay a * (b + t)^-gamma only then, so a supplied value is what the whole run samples at. On "mclmc" a non-positive epsilon selects the kernel's own adaptation. alpha is the SGHMC friction and L its leapfrog count; SGLD carries neither.

The SGHMC discretisation is calibrated for small epsilon^2 * lambda_max, and inflates every posterior SD above that; the acceptance statistic its adapter targets is computed from a log-posterior ratio the sampler never accepts or rejects on, so it does not measure that error. A sharply informative design wants an epsilon chosen by hand.

The elliptical-slice kernel takes four more, all prefixed ess_ and all inert on other backends. Note that ess_threshold above is SMC's resampling threshold and not one of them – the two unrelated senses of "ESS" are why these carry the prefix. ess_adapt_during_warmup (default FALSE) adapts the random-walk proposal SDs on the non-Gaussian parameters during warmup and ess_adapt_interval (default 50) is how many sweeps sit between those updates, so it acts only while adapting. ess_joint_sigma_re toggles the joint ⁠(log_sigma_re, re)⁠ rescaling move, which defaults to on whenever a random-effect term is present because the two are strongly anti-correlated under the centered parameterization and mix poorly when moved separately; forcing it off is how one demonstrates that. ess_joint_proposal_sd (default 0.1) is that move's step.

The elliptical-slice kernel draws from R's own RNG, so control$seed does not reach it: set.seed() before the call is what reproduces an ESS run. Every other backend carries control$seed into its own generator.

Value

A tulpa_fit with draws, means, param_names, the kernel's diagnostics, and (for "hmc") chain_id / n_chains so chain diagnostics apply.


Simulate data from a tulpa model

Description

Generic simulator that dispatches through a tulpa_family's simulate_fn. Given a model spec (formula + family + data) and a parameter vector or a fitted model, generate one or more synthetic response datasets.

Used internally by prior_predict() and exposed for posterior predictive checks, simulation-based calibration, and what-if analyses with fixed parameters.

Usage

tulpa_simulate(
  formula,
  family,
  data,
  theta = NULL,
  n_sims = 1L,
  priors = NULL,
  seed = NULL,
  ...
)

Arguments

formula

A model formula, or list of formulas keyed by process name.

family

A tulpa_family object (see tulpa_family()).

data

Data frame with covariates and grouping factors.

theta

One of:

  • A named list with beta (numeric or list per process), u (list of RE coefficient vectors, one per RE term per process), extras (named list of family-specific extras like phi, sigma_y).

  • A tulpa_fit object: posterior draws are sampled from ⁠$draws⁠.

  • NULL: equivalent to a single prior draw (shortcut).

n_sims

Number of simulated datasets. When theta is a fit, draws are subsampled (or recycled) to n_sims. Default 1.

priors

Used only if theta = NULL; default tulpa_priors().

seed

Optional integer seed.

...

Passed to family$simulate_fn.

Value

A tulpa_simulate object: list with y (length-n_sims list of simulated responses), theta (parameters used per sim), linpred, family, n_sims, n_obs.

Examples

fam <- tulpa_family(
  name = "gaussian",
  simulate_fn = function(eta, params, n_obs, ...) {
    rnorm(n_obs, eta[[1]], params$sigma_y)
  },
  extra_params = list(sigma_y = prior_half_normal(1))
)
df <- data.frame(y = rep(0, 20), x = rnorm(20))
theta <- list(
  beta = list(y = c(0.5, 1.0)),
  u = list(y = list()),
  extras = list(sigma_y = 0.5)
)
sim <- tulpa_simulate(y ~ x, fam, df, theta = theta, n_sims = 3, seed = 1)
length(sim$y)  # 3


Spatial structure specifications for tulpa

Description

Functions to specify spatial random effects for tulpa models. Spatial effects are shared between processes by default, which helps prevent bias from spatially-structured unmeasured confounders.

Value

The spatial constructors documented in this family (spatial_car(), spatial_bym2(), spatial_gp(), and the others) each return a tulpa_spatial (or related) specification object to pass to the spatial argument of tulpa().


Temporal structure specifications for tulpa

Description

Functions to specify temporal random effects for tulpa models. Temporal effects are shared between processes by default, which helps prevent bias from temporally-structured unmeasured confounders.

Value

The temporal constructors documented in this family (temporal_rw1(), temporal_rw2(), temporal_ar1(), and the others) each return a tulpa_temporal specification object to pass to the temporal argument of tulpa().


Fit a custom tgmrf latent block

Description

One front door for inference over a user-defined tgmrf() latent block's hyperparameter vector theta. mode selects the inference engine, all of which share the same Laplace body for ⁠(beta, z) | theta⁠:

The inference method is an argument, not a parallel verb.

Usage

tulpa_tgmrf(
  y,
  n_trials,
  X,
  block,
  family = "binomial",
  phi = 1,
  re_idx = NULL,
  n_re_groups = 0L,
  sigma_re = 1,
  mode = c("imh", "nuts", "vi", "nuts_joint"),
  control = list(),
  ...
)

Arguments

y, n_trials, X

Response, binomial trial counts (or NULL), and the fixed-effect design matrix.

block

A tgmrf() latent block.

family, phi

Observation family and its dispersion.

re_idx, n_re_groups, sigma_re

Optional scalar random-intercept structure.

mode

Inference engine: "imh", "nuts", "vi", or "nuts_joint".

control

A list of numerical / tuning knobs for the chosen mode (e.g. n_iter, warmup, thin, scale for "imh"; epsilon, max_depth, target_accept for the NUTS modes; n_draws, max_lbfgs, lbfgs_tol for "vi"; plus the shared pilot_axis_points, max_iter, tol, n_threads, verbose). An unknown knob for the chosen mode errors rather than being silently ignored.

...

Individual control knobs may also be passed directly by name; they are merged into control (a named argument here wins over the same name inside control).

Value

A tulpa_tgmrf / tulpa_fit object; ⁠$backend⁠ and ⁠$mode⁠ record the engine used.

See Also

tgmrf() for the block, tgmrf_cpp() for the compiled-block form.

Examples

## Not run: 
# `block` is a tgmrf latent block (from tgmrf() / tgmrf_cpp()); the inference
# method is an argument, not a parallel verb. See vignette("tgmrf").
fit <- tulpa_tgmrf(y, rep(1L, length(y)), X, block = blk, mode = "imh")

## End(Not run)

Posterior predictive checks for tulpa models

Description

Visual and numerical checks comparing observed data to posterior predictive distributions. Essential for assessing model fit.

Value

The functions documented in this family return a ggplot object (pp_check()) or a tulpa_prior_predict object (prior_predict()); see each function's own help page.


Empirical semivariogram of residuals

Description

Computes the empirical semivariogram in distance bins for visual assessment of remaining spatial structure in residuals.

Usage

tulpa_variogram(
  object,
  coords,
  n_bins = 15L,
  max_dist = NULL,
  resid_type = "pearson"
)

Arguments

object

A fitted model, or a numeric vector of residuals

coords

N x 2 coordinate matrix (required)

n_bins

Number of distance bins (default 15)

max_dist

Maximum distance (default: half the maximum pairwise distance)

resid_type

Residual type if extracting from model (default "pearson")

Value

A tulpa_variogram data.frame with columns dist, gamma, n_pairs


Extract temporally-varying coefficients from a fitted model

Description

Extract posterior distributions of temporally-varying coefficients (TVCs) from a fitted tulpa model with TVC specification.

Usage

tvc(object, terms = NULL, summary = FALSE, probs = c(0.025, 0.5, 0.975), ...)

## S3 method for class 'tulpa_fit'
tvc(object, terms = NULL, summary = FALSE, probs = c(0.025, 0.5, 0.975), ...)

Arguments

object

A tulpa_fit object fitted with tvc argument

terms

Which TVC terms to extract. If NULL (default), extracts all.

summary

Logical; if TRUE, return summary statistics instead of full posterior draws.

probs

Quantiles to compute if summary = TRUE.

...

Ignored

Value

A tulpa_tvc_posterior object containing:

See Also

temporal_tvc(), plot.tulpa_tvc_posterior()

Examples


set.seed(160)
n_t <- 10L; reps <- 5L
walk <- cumsum(rnorm(n_t, 0, 0.35)); walk <- walk - mean(walk)
year <- rep(seq_len(n_t), each = reps)
df <- data.frame(year = year, x = rnorm(length(year)))
df$count <- rpois(nrow(df), exp(0.3 + (0.5 + walk[year]) * df$x))

# The slope on `x` walks in time; TVC is exact-mode only.
fit <- tulpa(
  count ~ x,
  data = df,
  family = "poisson",
  temporal = temporal_tvc("year", terms = ~ x - 1, structure = "rw1"),
  mode = "exact",
  control = list(n_iter = 200L, n_warmup = 100L, seed = 1L)
)

tvc_post <- tvc(fit)
summary(tvc_post)
plot(tvc_post, "x")



Validate a tulpa_family

Description

Validate a tulpa_family

Usage

validate_family(family)

Arguments

family

Object to validate


Validate GP spatial specification against data

Description

Validate GP spatial specification against data

Usage

validate_gp(gp, data)

Arguments

gp

tulpa_gp or tulpa_multiscale object

data

Data frame

Value

Updated spatial object with computed neighbor structure


Internal validation helpers

Description

Small shared helpers used by ⁠validate_*()⁠ functions across the spatial / temporal / SVC / TVC specs. Centralised here to keep the per-spec validators thin and prevent drift between near-identical column-existence checks and coordinate preparation blocks.


Validate HSGP spatial structure

Description

Validate HSGP spatial structure

Usage

validate_hsgp(spatial, data)

Arguments

spatial

A tulpa_hsgp object

data

The data frame

Value

A validated tulpa_hsgp object with coords_matrix filled in


Validate latent factor specification against data

Description

Validate latent factor specification against data

Usage

validate_latent(latent, N)

Arguments

latent

A tulpa_latent object

N

Number of observations

Value

Validated latent specification with additional computed fields


Validate that a fit used the expected mode

Description

Ensures the fit object was created with the expected inference mode. Useful for enforcing mode requirements in downstream analysis.

Usage

validate_mode(fit, expected_mode, error = TRUE)

Arguments

fit

A tulpa_fit object

expected_mode

Mode that should have been used

error

If TRUE (default), error on mismatch. If FALSE, return logical.

Value

If error = FALSE, returns TRUE/FALSE. Otherwise errors on mismatch.


Validate a prior specification

Description

Validate a prior specification

Usage

validate_prior(prior, name)

Arguments

prior

A prior object

name

Parameter name for error messages


Validate RSR specification

Description

Validate RSR specification

Usage

validate_rsr(spatial, data, formula)

Arguments

spatial

tulpa_rsr object

data

Data frame

formula

Model formula (to extract design matrix)

Value

Updated spatial object with projection matrix


Validate spatial specification against data

Description

Validate spatial specification against data

Usage

validate_spatial(spatial, data)

Arguments

spatial

tulpa_spatial object

data

Data frame

Value

NULL (invisibly); errors if validation fails


Validate SVC specification against data and design matrix

Description

Validate SVC specification against data and design matrix

Usage

validate_svc(svc, data, X)

Arguments

svc

tulpa_svc object

data

Data frame

X

Design matrix (to resolve term names)

Value

Updated tulpa_svc object with computed neighbor structure


Validate temporal specification against data

Description

Validate temporal specification against data

Usage

validate_temporal(temporal, data)

Arguments

temporal

tulpa_temporal object

data

Data frame

Value

Updated tulpa_temporal object with indices computed


Validate temporal GP specification against data

Description

Validate temporal GP specification against data

Usage

validate_temporal_gp(temporal, data)

Arguments

temporal

tulpa_temporal_gp object

data

Data frame

Value

Updated object with computed time structure


Validate multi-scale temporal specification

Description

Validate multi-scale temporal specification

Usage

validate_temporal_multiscale(temporal, data)

Arguments

temporal

tulpa_temporal_multiscale object

data

Data frame

Value

Updated object with indices computed


Validate TVC specification against data and design matrix

Description

Validate TVC specification against data and design matrix

Usage

validate_tvc(tvc, data, X)

Arguments

tvc

tulpa_tvc object

data

Data frame

X

Design matrix (to resolve term names)

Value

Updated tulpa_tvc object with computed structure


Variance-covariance matrix of the fixed effects

Description

Variance-covariance matrix of the fixed effects

Usage

## S3 method for class 'tulpa_fit'
vcov(object, ...)

Arguments

object

A tulpa_fit object.

...

Ignored.

Value

Fixed-effect variance-covariance matrix (empirical for sampler tiers, H_beta^-1 for the Laplace tier).


Run an expression under a chosen symplectic integrator

Description

Evaluate expr with the integrator set to name, then restore whatever was selected before – on an error as well as on success. The integrator selection is process-global, so a bare tulpa_integrator() call leaves every later fit in the session on the new scheme, and an error between setting and restoring leaves it there permanently.

Usage

with_tulpa_integrator(name, expr, mts_substeps = 4L)

Arguments

name

Integrator name, as for tulpa_integrator().

expr

Expression to evaluate. Evaluated in the caller's environment.

mts_substeps

Inner prior-force substeps for "mts" (default 4).

Value

The value of expr.

See Also

tulpa_integrator()

Examples

with_tulpa_integrator("yoshida4", tulpa_integrator())
tulpa_integrator()   # unchanged


Elementwise log-likelihood of the zero-inflated mixture.

Description

Elementwise log-likelihood of the zero-inflated mixture.

Usage

zi_loglik(eta, logit_zi, y, family, n_trials = NULL, phi = 1, phi2 = NULL)

Negative Hessian of the zero-inflated mixture in eta space.

Description

Returns an ⁠n x 3⁠ matrix with columns count (-d2/d eta_count^2), zi (-d2/d logit_zi^2) and cross (-d2/d eta_count d logit_zi), i.e. the distinct entries of the symmetric 2 x 2 block per observation.

Usage

zi_neg_hessian(eta, logit_zi, y, family, n_trials = NULL, phi = 1, phi2 = NULL)

Details

This is the observed negative Hessian, exact wherever the base family registers obs_weight. At y > 0 the mixture is additively separable, so the count block is the base family's own curvature and the cross term is zero; the coupling lives entirely in the y = 0 branch, where both components can explain the zero.


Response-scale mean of the zero-inflated mixture.

Description

Response-scale mean of the zero-inflated mixture.

Usage

zi_response_mean(eta, logit_zi, family, n_trials = NULL, phi = 1)

One draw per element from the zero-inflated mixture.

Description

One draw per element from the zero-inflated mixture.

Usage

zi_sample(eta, logit_zi, family, n_trials = NULL, phi = 1, phi2 = NULL)

Score of the zero-inflated mixture in the two-process eta ordering.

Description

Returns an ⁠n x 2⁠ matrix with columns count (d loglik / d eta_count) and zi (d loglik / d logit_zi).

Usage

zi_score_eta(eta, logit_zi, y, family, n_trials = NULL, phi = 1, phi2 = NULL)

Response variance of the zero-inflated mixture.

Description

Law of total variance over the mixture indicator: with probability pi the response is a structural zero, otherwise it is a base-family draw.

Usage

zi_variance(eta, logit_zi, family, n_trials = NULL, phi = 1, phi2 = NULL)