Skip to content

Lower-level construction ​

The @latte macro is the primary way to define a model: you write the latent field and likelihood once, and Latte assembles the LatentGaussianModel and its hyperparameter prior for you. This page documents the secondary path — building an LatentGaussianModel by hand from its three components. Reach for it when @latte cannot express your model, for example when you want full control over the latent precision matrix and assemble it yourself.

Prefer @latte for most models

Most models are written with the @latte macro, which builds a LatentGaussianModel for you (see Defining models: the @latte macro and the tutorials). Construct one directly, as shown below, when you need full control over the latent prior.

A hand-built model has three parts:

  1. a hyperparameter prior, declared with @hyperparams;

  2. a latent field prior — a function of the hyperparameters returning a mean and a precision matrix, wrapped in FunctionLatentModel;

  3. an observation model linking observations to the latent field (see Observation Models).

Hyperparameters: @hyperparams ​

Hyperparameters control the latent prior (e.g. marginal variance, correlation) and the observation model (e.g. noise level). The @hyperparams macro declares them, each with a prior distribution, a transformation, and the space in which the prior is written.

julia
using Latte, Distributions

spec = @hyperparams begin
    (σ ~ Exponential(1.0), transform = log, space = natural)
    (ρ ~ Beta(2, 2),       transform = logit, space = natural)
end

Each ~ line introduces a free parameter; a plain name = value line fixes a parameter to a constant. Only free parameters appear in the parameter vector.

Natural and working space ​

Latte distinguishes two spaces:

  • Natural space is where parameters live for the user and for the model functions: a standard deviation σ > 0, a correlation ρ ∈ (0, 1).

  • Working space is the unconstrained space the inference engines optimise and explore in. The transform field is the bijector mapping natural → working (log for positive parameters, logit for the unit interval, identity for unbounded ones).

The two parameter wrappers, WorkingHyperparameters and NaturalHyperparameters, carry a vector together with the spec, and convert moves between them and to a NamedTuple:

julia
θ_w  = WorkingHyperparameters([0.5, -1.2], spec)  # working space (log σ, logit ρ)
θ_n  = convert(NaturalHyperparameters, θ_w)        # natural space (σ, ρ)
θ_nt = convert(NamedTuple, θ_n)                    # (ρ = logistic(-1.2), σ = exp(0.5))

Model functions receive parameters in natural space as keyword arguments, even though optimisation happens in working space.

Prior space and the Jacobian correction ​

The space field records where the prior is written. With space = natural, the prior is a density over the natural parameter, and Latte applies the change of variables to evaluate it in working space:

log⁡pworking(η)=log⁡pnatural(θ)+log⁡|dθdη|,

where η is the working value and θ=g(η) the corresponding natural value. The log-Jacobian term is supplied by logdetjac. With space = working, the prior is already a density over the working parameter and no correction is added. logpdf_prior evaluates the prior in working space — including this correction when the prior was specified in natural space.

julia
spec = @hyperparams begin
    (σ ~ Exponential(1.0), transform = log, space = natural)
end
# Equivalent to a prior on log(σ) in working space:
# p(log σ) = Exponential(1.0)(σ) · |dσ/d(log σ)| = Exponential(1.0)(σ) · σ

Vector-valued hyperparameters ​

A free hyperparameter may carry a continuous vector prior, so several components share one joint distribution — for example regression-style coefficients with a non-diagonal covariance:

julia
spec = @hyperparams begin
    (σ ~ Exponential(1.0), transform = log, space = natural)
    κ ~ MvNormal(zeros(2), [0.7 0.4; 0.4 1.3])   # one vector-valued hyperparameter
end

The flat parameter vector concatenates the entries in declaration order — here [σ; κ[1]; κ[2]], three coordinates — and the wrappers expose each entry under its name, as a scalar or a Vector:

julia
θ_n = NaturalHyperparameters([2.0, 0.1, -0.3], spec)
θ_n.σ                     # 2.0
θ_n.κ                     # [0.1, -0.3]
convert(NamedTuple, θ_n)  # (σ = 2.0, κ = [0.1, -0.3])

Model functions receive the vector entry as a Vector keyword argument, in natural space, exactly as written in the prior.

Two restrictions keep the flat layout well-defined:

  • Transforms on vector entries must be dimension-preserving and act elementwise: identity (the default, right for MvNormal) or elementwise(f) (e.g. elementwise(log) for a positive vector). Dimension-changing bijectors — simplex, Cholesky — are rejected at construction.

  • The prior must be vector-shaped (Distribution{Multivariate}); matrix-variate priors are not supported.

Inference engines treat each coordinate as one dimension of the θ-space, so marginals stay per-coordinate: a vector entry κ of length 2 contributes marginals named κ[1] and κ[2], and the by-name accessor hyperparameter_marginals(result, :κ) returns both. hyperparameter_groups maps :κ to its coordinate range. Note that grid exploration is exponential in the total θ dimension — a long vector hyperparameter calls for the CCD strategy (selected automatically for more than two coordinates).

In the @latte macro, mark a vector prior with @fixed to make it a hyperparameter — an unmarked MvNormal site classifies as a latent (fixed-effects) block.

Latte.@hyperparams Macro
julia
@hyperparams begin ... end

Convenient macro for creating HyperparameterSpec objects with clean, declarative syntax.

Syntax

Free parameters use the ~ operator:

julia
name ~ prior                                         # Identity transform, working space (default)
(name ~ prior, transform = transform_expr)           # With custom transform
(name ~ prior, transform = log, space = natural)     # With options

Fixed parameters use =:

julia
name = value

Builtin Transforms

The macro provides convenient shortcuts for common transformations:

  • log → elementwise(log) for positive parameters (σ, τ, κ, etc.)

  • logit → Bijectors.Logit(0.0, 1.0) for parameters in (0,1) (ρ, correlation, etc.)

  • identity → no transformation (default for parameters already unconstrained)

Call aliases are also supported: Log(), Logit(), Identity()

For maximum flexibility, you can also provide any custom bijector expression directly.

Options

  • transform: Bijector mapping natural → working space (default: identity)

  • space or prior_space: Space in which the prior is specified

    • :natural - Prior is on the natural (constrained) parameter space

    • :working - Prior is already on the working (unconstrained) space (default)

Important: When specifying options, wrap the entire clause in parentheses!

Examples

julia
using Distributions, Bijectors

# Example 1: PC priors (Penalizing Complexity priors)
# Exponential prior on σ, optimized on log(σ)
spec = @hyperparams begin
    (σ ~ Exponential(1.0), transform = log, space = natural)
    (ρ ~ Beta(2, 2), transform = logit, space = natural)
    μ = 0.0  # Fixed parameter
end

# Example 2: Using call aliases
spec = @hyperparams begin
    (σ ~ Exponential(1.0), transform = Log(), space = natural)
    (ρ ~ Beta(2, 2), transform = Logit(), space = natural)
end

# Example 3: Custom bijector expression
spec = @hyperparams begin
    (τ ~ Gamma(2, 1), transform = elementwise(x -> log(x + 1)), space = natural)
end

# Example 4: Identity transform (default) - prior already in working space
spec = @hyperparams begin
    μ ~ Normal(0, 10)      # No transformation needed
    σ ~ Gamma(2, 1)        # Prior specified directly in working space
end

# Example 5: Mixed free and fixed parameters
spec = @hyperparams begin
    (σ ~ Exponential(1.0), transform = log, space = natural)
    (ρ ~ Beta(2, 2), transform = logit, space = natural)
    μ = 0.0    # Fixed intercept
    n = 100    # Fixed sample size
end

# Example 6: Using prior_space keyword (synonym for space)
spec = @hyperparams begin
    (σ ~ Exponential(1.0), transform = log, prior_space = natural)
end

# Example 7: Vector-valued hyperparameter (joint prior over the components).
# Vector entries admit `identity` (default) or `elementwise(f)` transforms.
spec = @hyperparams begin
    (σ ~ Exponential(1.0), transform = log, space = natural)
    κ ~ MvNormal(zeros(2), [1.0 0.5; 0.5 2.0])
end

Comparison with Manual Construction

The macro provides a much more concise syntax compared to manual construction:

julia
# With macro (recommended)
spec = @hyperparams begin
    (σ ~ Exponential(1.0), transform = log, space = natural)
    (ρ ~ Beta(2, 2), transform = logit, space = natural)
    μ = 0.0
end

# Manual construction (verbose)
spec = HyperparameterSpec(
    free = (
        σ = Hyperparameter(Exponential(1.0), transform=elementwise(log), prior_space=:natural),
        ρ = Hyperparameter(Beta(2, 2), transform=Bijectors.Logit(0.0, 1.0), prior_space=:natural)
    ),
    fixed = (μ = 0.0,)
)

Notes

  • At least one free parameter (using ~) is required

  • When specifying options, use parentheses: (name ~ prior, option = value)

  • Options use unquoted symbols: space = natural, not space = :natural

  • Parameters cannot be specified multiple times

  • The space and prior_space keywords are synonyms - use whichever is more natural

See Also

source
Latte.HyperparameterSpec Type
julia
HyperparameterSpec{Free, Fixed}

Complete specification of hyperparameters with both free and fixed parameters.

Type Parameters

  • Free: Concrete NamedTuple type for free parameters

  • Fixed: Concrete NamedTuple type for fixed parameter values

Fields

  • free::Free: Free parameters to be estimated (NamedTuple of Hyperparameter objects)

  • fixed::Fixed: Fixed parameter values (NamedTuple of scalar values)

Example

julia
using Bijectors

spec = HyperparameterSpec(
    free = (
        σ = Hyperparameter(Exponential(1.0), transform=elementwise(log), prior_space=:natural),
        ρ = Hyperparameter(Beta(2, 2), transform=Bijectors.Logit(0.0, 1.0), prior_space=:natural)
    ),
    fixed = (μ = 0.0,)
)
source
Latte.Hyperparameter Type
julia
Hyperparameter{T, S}

A hyperparameter specification with an explicit transformation between working and natural spaces.

Type Parameters

  • T: The transformation type (Bijector or identity function)

  • S: The space in which the prior was originally specified (:natural or :working)

Fields

  • prior::Distribution: Prior distribution in working space (always stored in working space for numerical stability)

  • transform::T: Bijector mapping natural → working (constrained → unconstrained)

Transformation Convention

Follows Bijectors.jl/Turing.jl convention:

  • transform: natural (constrained) → working (unconstrained)

  • inverse(transform): working (unconstrained) → natural (constrained)

For example, for σ ∈ (0,∞):

  • Natural space: σ > 0

  • Working space: log(σ) ∈ ℝ

  • Transform: elementwise(log) maps σ → log(σ)

Spaces

  • Working space (η): Unconstrained space used for optimization/exploration

  • Natural space (θ): Natural parameter space used in user model functions

Example

julia
using Bijectors

# Used within a HyperparameterSpec, not standalone
spec = HyperparameterSpec(
    free = (
        σ = Hyperparameter(Exponential(1.0), elementwise(log), Val(:natural)),
        ρ = Hyperparameter(Beta(2, 2), Bijectors.Logit(0.0, 1.0), Val(:natural))
    ),
    fixed = (μ = 0.0,)
)
source
Latte.WorkingHyperparameters Type
julia
WorkingHyperparameters{T, Spec} <: AbstractVector{T}

Hyperparameters in working (unconstrained) space.

Behaves like a vector while preserving semantic meaning. Supports indexing, iteration, broadcasting, and other vector operations.

Type Parameters

  • T: Element type of the parameter vector

  • Spec: HyperparameterSpec type

Fields

  • θ::Vector{T}: Parameter values in working space

  • spec::Spec: Hyperparameter specification

Example

julia
spec = HyperparameterSpec(
    free = (σ = Hyperparameter(Exponential(1.0), transform=elementwise(log), prior_space=:natural),)
)
θ_w = WorkingHyperparameters([0.5], spec)

# Vector-like operations
θ_w[1]           # indexing
θ_w .+ 0.1       # broadcasting
sum(θ_w)         # reduction
source
Latte.NaturalHyperparameters Type
julia
NaturalHyperparameters{T, Spec} <: AbstractVector{T}

Hyperparameters in natural (constrained) space.

Behaves like a vector while preserving semantic meaning. Supports indexing, iteration, broadcasting, and other vector operations.

Type Parameters

  • T: Element type of the parameter vector

  • Spec: HyperparameterSpec type

Fields

  • θ::Vector{T}: Parameter values in natural space

  • spec::Spec: Hyperparameter specification

Example

julia
spec = HyperparameterSpec(
    free = (σ = Hyperparameter(Exponential(1.0), transform=elementwise(log), prior_space=:natural),)
)
θ_n = NaturalHyperparameters([2.0], spec)

# Vector-like operations
θ_n[1]           # indexing
θ_n .* 2         # broadcasting
maximum(θ_n)     # reduction
source
Latte.logpdf_prior Function
julia
logpdf_prior(θ::WorkingHyperparameters) -> Float64

Evaluate the log prior density in working space.

Arguments

  • θ::WorkingHyperparameters: Hyperparameters in working space

Returns

  • Float64: Log prior density in working space

Details

Since priors are stored in working space internally, this directly evaluates the prior at the working-space values.

Example

julia
spec = HyperparameterSpec(
    free = (σ = Hyperparameter(Exponential(1.0), transform=elementwise(log), prior_space=:natural),)
)
θ_w = WorkingHyperparameters([0.5], spec)
log_p = logpdf_prior(θ_w)  # Evaluates log p(η) in working space
source
julia
logpdf_prior(θ::NaturalHyperparameters) -> Float64

Evaluate the log prior density in natural space.

Arguments

  • θ::NaturalHyperparameters: Hyperparameters in natural space

Returns

  • Float64: Log prior density in natural space

Details

Converts to working space, evaluates the working-space prior, then adds the Jacobian correction to obtain the natural-space density.

Example

julia
spec = HyperparameterSpec(
    free = (σ = Hyperparameter(Exponential(1.0), transform=elementwise(log), prior_space=:natural),)
)
θ_n = NaturalHyperparameters([2.0], spec)
log_p = logpdf_prior(θ_n)  # Evaluates log π(θ) in natural space
source
julia
logpdf_prior(θ_natural::NamedTuple, spec::HyperparameterSpec) -> Float64

Evaluate the log prior density in natural space (legacy interface).

Arguments

  • θ_natural::NamedTuple: Free parameters in natural space

  • spec::HyperparameterSpec: Hyperparameter specification

Returns

  • Float64: Log prior density in natural space

Details

Legacy interface that constructs NaturalHyperparameters internally and dispatches to the modern implementation.

Example

julia
spec = HyperparameterSpec(
    free = (σ = Hyperparameter(Exponential(1.0), transform=elementwise(log), prior_space=:natural),)
)

θ_natural = (σ = 2.0,)  # σ in natural space
log_p = logpdf_prior(θ_natural, spec)  # Evaluates log π(σ)
source
Latte.logdetjac Function
julia
logdetjac(θ::WorkingHyperparameters) -> Float64

Compute the log absolute determinant of the Jacobian for the working → natural transformation.

This is the correction term to add when converting a density from working space to natural space:

julia
log p_natural(θ) = log p_working(η) + logdetjac(θ_working)

where η are working values, θ = g(η) are the corresponding natural values.

Example

julia
spec = HyperparameterSpec(
    free = (σ = Hyperparameter(Exponential(1.0), transform=elementwise(log), prior_space=:natural),)
)
θ_w = WorkingHyperparameters([0.5], spec)

# Have a density in working space, want natural space
log_density_working = some_function(θ_w)
log_density_natural = log_density_working + logdetjac(θ_w)
source
julia
logdetjac(θ::NaturalHyperparameters) -> Float64

Compute the log absolute determinant of the Jacobian for the natural → working transformation.

This is the correction term to add when converting a density from natural space to working space:

julia
log p_working(η) = log p_natural(θ) + logdetjac(θ_natural)

where θ are natural values, η = f(θ) are the corresponding working values.

Example

julia
spec = HyperparameterSpec(
    free = (σ = Hyperparameter(Exponential(1.0), transform=elementwise(log), prior_space=:natural),)
)
θ_n = NaturalHyperparameters([2.0], spec)

# Prior is in natural space, want working space density
log_prior_natural = logpdf(Exponential(1.0), θ_n.σ)
log_density_working = log_prior_natural + logdetjac(θ_n)
source

Building a model directly: LatentGaussianModel ​

LatentGaussianModel combines the three components. It factors the joint as p(θ)p(x∣θ)p(y∣x,θ) with p(x∣θ) Gaussian.

The latent field prior is a function of the (keyword) hyperparameters that returns a (mean, precision) tuple. Wrap it with FunctionLatentModel(f, n), passing the latent dimension n; a raw function is rejected with an error asking you to wrap it. The function receives every hyperparameter — free and fixed — by name in natural space, so accept kwargs... to ignore the ones it does not use.

julia
using Latte, GaussianMarkovRandomFields, Distributions, SparseArrays

spec = @hyperparams begin
    (σ ~ Exponential(1.0), transform = log, space = natural)
    (ρ ~ Beta(2, 2),       transform = logit, space = natural)
end

# Latent GMRF: an AR(1) precision built by hand
function ar1_latent(; σ, ρ, kwargs...)
    n = 100
    Q = spdiagm(
        -1 => -ρ * ones(n - 1),
         0 => (1 + ρ^2) * ones(n),
         1 => -ρ * ones(n - 1),
    )
    return (zeros(n), Q / σ^2)  # FunctionLatentModel expects (mean, precision)
end

obs_model = ExponentialFamily(Normal)

model = LatentGaussianModel(spec, FunctionLatentModel(ar1_latent, 100), obs_model)

Validation ​

The constructor checks that every hyperparameter required by the observation model is present in the spec, as a free or fixed parameter. A missing one is an error:

julia
# Errors: Normal requires a σ hyperparameter, but the spec only declares τ
spec = @hyperparams begin
    (τ ~ Gamma(2, 1), transform = log, space = natural)
end
obs_model = ExponentialFamily(Normal)
LatentGaussianModel(spec, FunctionLatentModel(latent_fn, 50), obs_model)  # ERROR: missing σ

Extra hyperparameters beyond those the observation model needs are allowed — they are typically consumed by the latent field prior function.

Once built, the model feeds the inference engines directly; pass it to inla (see the INLA engine) and work with the result using the result accessors.

Latte.LatentGaussianModel Type
julia
LatentGaussianModel{HP, F, O}

A latent Gaussian model specification with hyperparameter prior, latent field prior, and observation model.

Factors as p(θ) · p(x | θ) · p(y | x, θ) with p(x | θ) Gaussian. This is the shared structure that INLA, TMB, HMC-on-Laplace, and variational approximations all operate on.

The alias LGM is available for brevity.

Type Parameters

  • HP: Type of the hyperparameter specification (HyperparameterSpec)

  • F <: AbstractLatentPrior: Type of the latent prior — a Gaussian LatentModel (wrap a function with FunctionLatentModel), or a NonGaussianLatentPrior (e.g. AutoDiffLatentPrior) for nonlinear state-space priors

  • O <: ObservationModel: Type of the observation model

Fields

  • hyperparameter_spec::HP: Hyperparameter specification with transformations

  • latent_prior::F: The latent field prior as a LatentModel, receiving natural-space hyperparameters as keyword arguments

  • observation_model::O: Observation model linking observations to latent field

Example

julia
using SparseArrays

# Hyperparameter specification (free parameters via `~`)
hp_spec = @hyperparams begin
    (σ ~ Exponential(1.0), transform = log, space = natural)
end

# Latent field prior: a function of the (keyword) hyperparameters returning (mean, precision)
function latent_gmrf(; σ, kwargs...)
    n = 100
    Q = spdiagm(0 => fill(1 / σ^2, n))  # Simple white noise
    μ = zeros(n)
    return (μ, Q)
end

# Observation model
obs_model = ExponentialFamily(Normal)

# Create the latent Gaussian model — wrap the latent function with its dimension
model = LatentGaussianModel(hp_spec, FunctionLatentModel(latent_gmrf, 100), obs_model)
source
Latte.latent_gmrf Function
julia
latent_gmrf(model::LatentGaussianModel, θ_named)

Get the latent field GMRF for given hyperparameters θ_named.

source
julia
latent_gmrf(model::LatentGaussianModel, ws, θ_named)

Construct the latent GMRF through a persistent workspace ws. Reuses the workspace's Cholesky symbolic factorization; only the numeric values are refactorized. Use this form inside hot loops over hyperparameters.

source
Latte.log_joint_density Function
julia
log_joint_density(model::LatentGaussianModel, x, θ_w::WorkingHyperparameters, y)

Evaluate the joint log-density log π(x, θ, y) for the INLA model in working space.

This computes: log π(θ) + log π(x | θ) + log π(y | x, θ)

Arguments

  • model::LatentGaussianModel: The INLA model

  • x: Latent field values

  • θ_w: Hyperparameters in working space

  • y: Observations

This is the main implementation. Creates the latent GMRF and observation likelihood internally.

source
julia
log_joint_density(model::LatentGaussianModel, x, θ_n::NaturalHyperparameters, y)

Evaluate the joint log-density log π(x, θ, y) for the INLA model in natural space.

Converts to working space and adds Jacobian correction term.

Arguments

  • model::LatentGaussianModel: The INLA model

  • x: Latent field values

  • θ_n: Hyperparameters in natural space

  • y: Observations

source