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
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
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 GPlinfuncs: NamedTuple of name =>AbstractLinearFunctionalμ: joint mean vector (concatenated block means)Σ: joint covariance as aBlockMatrixranges: NamedTuple of name =>UnitRange{Int}giving the row/column range of each named block inμ/Σ
Example
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 = σ²))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.
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 preservationFunctionalGPs.block_range Function
block_range(fg::FunctionalGaussian, name::Symbol) -> UnitRange{Int}Return the index range of the named block in the joint mean/covariance.
sourceProperty 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.
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
LazyMvNormal{T, M, S} <: AbstractMvNormalMultivariate 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 toMvNormal(d). For repeated likelihood evaluation, build the factorised form once viaMvNormal(d)and reuse it.
Conversion
MvNormal(d)materialises the covariance and factorises eagerly, returning a regularDistributions.MvNormal. Use this when you want PDMat-style caching.
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
marginal(fg, :dy) # equivalent to fg.dy
marginal(fg, (:y, :q)) # multi-block, order respectedMulti-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
marginal(fg::FunctionalGaussian, name::Symbol) -> LazyMvNormal
marginal(fg::FunctionalGaussian, names::Tuple{Vararg{Symbol}}) -> LazyMvNormalReturn 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.
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.
post = posterior(fg, (; y = y_obs); noise = (; y = σ²))
post.dy # LazyMvNormal over derivative locations
post.q # LazyMvNormal over the integralcondition 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
AbstractGPs.posterior(fg::FunctionalGaussian, observed::NamedTuple; noise::NamedTuple = (;)) -> NamedTupleCondition 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.
Marginal log-likelihood
ℓ = loglikelihood(fg, (; y = y_obs); noise = (; y = σ²))This is the single hook for hyperparameter inference. Inside a Turing model:
@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 = σ²))
endA complete runnable example with NUTS hyperparameter sampling lives at examples/turing_hyperparams/turing_inference.jl.
StatsAPI.loglikelihood Method
loglikelihood(fg::FunctionalGaussian, observed::NamedTuple; noise::NamedTuple = (;)) -> RealMarginal 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.
Joint as a flat MvNormal
FunctionalGPs.as_mvn Function
as_mvn(fg::FunctionalGaussian) -> MvNormalReturn the joint distribution as a flat MvNormal.
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.