Skip to content

Joint Functional Gaussians

FunctionalGaussian represents the joint Gaussian induced by applying a named collection of linear functionals to a Gaussian process. It owns the joint mean, the joint covariance (with per-block storage that preserves structured CovarianceMatrix subtypes), and the algebra of marginalising, conditioning, and likelihood evaluation.

It exists because writing one independent ~ MvNormal(...) per block in a probabilistic programming model would drop the cross-covariances induced by the shared underlying GP. FunctionalGaussian collects all functionals into a single joint object so those cross-covariances are preserved.

Construction

julia
using FunctionalGPs, AbstractGPs

f = GP(WendlandKernel(1, 3, 8 // 10))

X_obs  = collect(0.0:0.1:1.0)
X_pred = collect(0.0:0.05:1.0)

fg = FunctionalGaussian(f;
    y  = EvaluationFunctional(X_obs),
    dy = EvaluationFunctional(X_pred)  PartialDerivative((1,)),
    q  = VectorizedLebesgueIntegral([Interval(0.0, 1.0)]),
)

The same construction reads as math via the Notation submodule (see the Notation page): δ(X_obs), δ(X_pred) ∘ ∂(1), ∫([Interval(0, 1)]).

FunctionalGPs.FunctionalGaussian Type
julia
FunctionalGaussian(f::AbstractGP; name1 = ℒ1, name2 = ℒ2, ...)
FunctionalGaussian(f::AbstractGP, linfuncs::NamedTuple)

A finite-dimensional Gaussian induced by applying a collection of named linear functionals to a Gaussian process.

This is the package-native object that owns the joint Gaussian algebra over GP-derived random variables. Marginalising, conditioning, and likelihood evaluation are all operations on this object — never on the individual blocks in isolation, because doing so would drop cross-covariances induced by the shared underlying GP.

Fields

  • f: the underlying GP

  • linfuncs: NamedTuple of name => AbstractLinearFunctional

  • μ: joint mean vector (concatenated block means)

  • Σ: joint covariance as a BlockMatrix

  • ranges: NamedTuple of name => UnitRange{Int} giving the row/column range of each named block in μ/Σ

Example

julia
k = WendlandKernel(1, 3, 8 // 10)
f = GP(k)

L_y  = EvaluationFunctional(X)
L_dy = EvaluationFunctional(Xd)  PartialDerivative((1,))
fg = FunctionalGaussian(f; y = L_y, dy = L_dy)

mean(fg)              # joint mean (length 5)
cov(fg, :y, :dy)      # cross-covariance block
marginal(fg, :dy)     # MvNormal over the dy block
post = posterior(fg, (; y = y_obs); noise = (; y = σ²))
loglikelihood(fg, (; y = y_obs); noise = (; y = σ²))
source

Block accessors

Each named block has a row/column range inside the joint mean and covariance. Per-block accessors return the underlying objects unchanged, so structured CovarianceMatrix subtypes (Toeplitz, Khatri-Rao, sparse, ...) keep their fast getindex/matvec paths.

julia
keys(fg)              # (:y, :dy, :q)
length(fg)            # total dimension
block_range(fg, :dy)  # row/col range in the joint Σ
mean(fg)              # joint mean Vector
mean(fg, :dy)         # mean of one block (view)
cov(fg)               # joint Σ (BlockMatrix; per-block types retained)
cov(fg, :y)           # self-covariance of :y, e.g. StationaryKernelMatrix
cov(fg, :y, :dy)      # cross-covariance — same structure preservation
FunctionalGPs.block_range Function
julia
block_range(fg::FunctionalGaussian, name::Symbol) -> UnitRange{Int}

Return the index range of the named block in the joint mean/covariance.

source

Property access

fg.<block_name> returns a LazyMvNormal over that block: an AbstractMvNormal that does not eagerly factorise its covariance, so the structured per-block matrix flows through to cov(fg.y) unchanged.

julia
fg.y                 # LazyMvNormal{..., StationaryKernelMatrix}
mean(fg.y)
cov(fg.y)            # same object as cov(fg, :y) — no factorisation
var(fg.y)

Block-name access takes priority over the underlying struct fields. To access an internal field that is shadowed by a block name (e.g. a block named ), use getfield(fg, :μ).

propertynames(fg) returns the block names by default; pass true to also see the struct fields.

LazyMvNormal

FunctionalGPs.LazyMvNormal Type
julia
LazyMvNormal{T, M, S} <: AbstractMvNormal

Multivariate normal distribution that does not eagerly factorise its covariance. Stores the mean and the covariance matrix as-is, so structured covariance types (Toeplitz, Khatri-Rao, sparse, etc.) keep their fast getindex/matvec paths intact.

Returned by marginal(fg, name) and the fg.<name> property accessor on a FunctionalGaussian.

Cheap, structure-preserving

  • mean(d), cov(d), var(d), length(d) — no factorisation.

  • cov(d) returns the underlying matrix unchanged (e.g. StationaryKernelMatrix).

Heavier (materialises and factorises on each call)

  • logpdf(d, x), rand(d), Distributions.logdetcov(d), sqmahal(d, x) — fall back to MvNormal(d). For repeated likelihood evaluation, build the factorised form once via MvNormal(d) and reuse it.

Conversion

  • MvNormal(d) materialises the covariance and factorises eagerly, returning a regular Distributions.MvNormal. Use this when you want PDMat-style caching.
source

LazyMvNormal is structure-preserving for mean / cov / var / length. For repeated logpdf / rand calls (e.g. in a tight inference loop), build the eager MvNormal once via MvNormal(d) to amortise the Cholesky.

Marginalisation

julia
marginal(fg, :dy)            # equivalent to fg.dy
marginal(fg, (:y, :q))       # multi-block, order respected

Multi-block marginals materialise a dense joint covariance (the Cholesky for joint sampling needs it). Single-block marginals keep the structured per-block matrix.

FunctionalGPs.marginal Function
julia
marginal(fg::FunctionalGaussian, name::Symbol) -> LazyMvNormal
marginal(fg::FunctionalGaussian, names::Tuple{Vararg{Symbol}}) -> LazyMvNormal

Return the marginal distribution over one or more named blocks, in the order given. The returned LazyMvNormal does not factorise its covariance, so structured per-block matrices keep their representation.

source

Conditioning — posterior

FunctionalGPs extends AbstractGPs.posterior. For a FunctionalGaussian, posterior conditions the joint Gaussian on observed values for a subset of named blocks and returns a NamedTuple of LazyMvNormal over the remaining blocks.

julia
post = posterior(fg, (; y = y_obs); noise = (; y = σ²))
post.dy        # LazyMvNormal over derivative locations
post.q         # LazyMvNormal over the integral

condition uses block-aware matmul for the cross-block Σ_lo * (C \\ residual) products, so per-block fast matvec paths (Toeplitz, Khatri-Rao, …) are exploited automatically when the kernel admits them.

noise accepts the same forms as LinearObservation: scalar variance, vector of variances, or full covariance matrix. Element types propagate, so ForwardDiff.Dual and BigFloat hyperparameters trace through cleanly.

AbstractGPs.posterior Method
julia
AbstractGPs.posterior(fg::FunctionalGaussian, observed::NamedTuple; noise::NamedTuple = (;)) -> NamedTuple

Condition the joint Gaussian on observed values for a subset of named blocks and return the conditional distributions over the remaining blocks.

This extends AbstractGPs.posterior. AbstractGPs.posterior(::GP, ...) returns a posterior process; this method returns a NamedTuple of named finite-block posteriors, one LazyMvNormal per latent block.

observed is a NamedTuple mapping block names to observed values. noise optionally maps observed block names to additive Gaussian noise (a scalar variance, a vector of variances, or a covariance matrix). Blocks not present in noise are treated as noise-free.

The posterior covariance comes from a Cholesky solve and is dense, but LazyMvNormal defers factorisation so mean/cov/var access remains cheap.

source

Marginal log-likelihood

julia
= loglikelihood(fg, (; y = y_obs); noise = (; y = σ²))

This is the single hook for hyperparameter inference. Inside a Turing model:

julia
@model function gp_inference(y_obs, X_obs)
    ell ~ LogNormal(log(0.2), 0.5)
    σ² ~ LogNormal(log(0.01), 1.0)
    f = GP(with_lengthscale(SqExponentialKernel(), ell))
    fg = FunctionalGaussian(f; y = EvaluationFunctional(X_obs))
    Turing.@addlogprob! loglikelihood(fg, (; y = y_obs); noise = (; y = σ²))
    return posterior(fg, (; y = y_obs); noise = (; y = σ²))
end

A complete runnable example with NUTS hyperparameter sampling lives at examples/turing_hyperparams/turing_inference.jl.

StatsAPI.loglikelihood Method
julia
loglikelihood(fg::FunctionalGaussian, observed::NamedTuple; noise::NamedTuple = (;)) -> Real

Marginal log-likelihood of the observed block(s) under the joint Gaussian.

Equivalent to logpdf(marginal(fg, observed_names), stacked_observations), plus optional additive noise on the observed blocks.

source

Joint as a flat MvNormal

FunctionalGPs.as_mvn Function
julia
as_mvn(fg::FunctionalGaussian) -> MvNormal

Return the joint distribution as a flat MvNormal.

source

as_mvn(fg) materialises the full joint covariance and runs Cholesky eagerly, returning a regular Distributions.MvNormal. Reach for it only when you need a strictly Distributions-compatible flat object — for everything else, prefer the LazyMvNormal paths above.