Gaussian priors in infinite dimensions

In this notebook we show how to construct PDE-based priors that lead to well-posed Bayesian inverse problems in infinite dimensions. Specifically, we consider a Gaussian prior \(\mu_{\rm prior} \sim \mathcal{N}( m_{\rm prior}, \mathcal{C}_{\rm prior} )\), where the covariance operator \(\mathcal{C}_{\rm prior}\) is defined as the inverse of an elliptic differential operator, i.e.

\[ \mathcal{C}_{\rm prior} = \left( \delta I - \gamma \Delta \right)^{-\alpha}, \]

equipped with homogeneous Neumann, Dirichlet or Robin boundary conditions, and \(m_{\rm prior} \in H^{\frac{\alpha}{2}}(\Omega)\), where \(\Omega \subset \mathbb{R}^d\).

The parameter \(\alpha > \frac{d}{2}\) controls the smoothness of the random field and ensures that \(\mathcal{C}_{\rm prior}\) is a trace class operator (i.e., the infinite sum of the eigenvalues of \(\mathcal{C}_{\rm prior}\) is finite). That \(\mathcal{C}_{\rm prior}\) is trace class is extremely important, as it guarantees that the pointwise variance of the samples is finite. (Recall that for a Gaussian random field \(E \left[\int_{\Omega}(m - m_{\rm prior})^2\,dx\right] = \operatorname{trace}(\mathcal{C}_{\rm prior})\).)

The parameters \(\delta>0\), \(\gamma>0\) can be constant in \(\Omega\) (in this case the prior is called stationary) or spatially varying.

It can be shown that, in the case of the BiLaplacian prior (\(\alpha=2\)) in \(d\) spatial dimensions, the relationship between the PDE coefficients \(\gamma\) and \(\delta\) and the desired marginal variance \(\sigma^2\) and correlation length \(\rho\) is

\[ \gamma = \frac{1}{s}, \quad \delta = \frac{\kappa^2}{s}, \]

where

\[ \nu = 2 - \frac{d}{2}, \quad \kappa = \frac{\sqrt{8\nu}}{\rho}, \text{ and } s = \sigma\kappa^\nu\sqrt{\frac{(4\pi)^{d/2}}{\Gamma(\nu)}}.\]

The above formulae are implemented in hm.BiLaplacianComputeCoefficients, which computes the coefficients gamma and delta of the BiLaplacian prior (\(\alpha=2\)) from the desired marginal variance \(\sigma^2\) and correlation length \(\rho\).

Import dependencies

import math
import os
import sys

sys.path.insert(0, os.environ.get("HIPPYMFEM_BASE_DIR", os.path.abspath("..")))

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
%matplotlib inline

from mpi4py import MPI
import mfem.par as mfem

import hippymfem as hm
from hippymfem import nb

mfem.Hypre.Init()
comm = MPI.COMM_WORLD

Plot the marginal variance and correlation structure at a given point

The covariance between the field at a point \(x_0\) and everywhere else is the prior covariance applied to a point source at \(x_0\). The observation operator of a single point supplies the point source (\(B^T e\)), and one application of \(R^{-1}\), the prior covariance, gives the covariance function \(c(\cdot, x_0)\); its value at \(x_0\) is the marginal variance there. The marginal variance everywhere is the diagonal of the covariance, computed exactly here: the meshes are small, and it takes two solves per degree of freedom.

def anisotropic_tensor(theta0, theta1, alpha):
    sa, ca = math.sin(alpha), math.cos(alpha)
    return np.array([[theta0 * sa**2 + theta1 * ca**2, (theta0 - theta1) * sa * ca],
                     [(theta0 - theta1) * sa * ca, theta0 * ca**2 + theta1 * sa**2]])


def make_prior(Vh, sigma2, rho, degree=2, anisotropic=False, use_robin=True,
               theta0=2.0, theta1=0.5, alpha=math.pi / 4):
    ndim = Vh.mesh.Dimension()
    gamma, delta = hm.BiLaplacianComputeCoefficients(sigma2, rho, ndim)
    if degree == 1:
        return hm.LaplacianPrior(Vh, gamma, delta, solver_type="lu")
    Theta = anisotropic_tensor(theta0, theta1, alpha) if anisotropic else None
    return hm.BiLaplacianPrior(Vh, gamma, delta, Theta=Theta, robin_bc=use_robin,
                               solver_type="lu")


def correlationStructure(prior, Vh, center):
    B = hm.assemblePointwiseObservation(Vh, np.atleast_2d(center))
    e = B.createVecLeft()
    e.set(1.0)
    rhs = Vh.vector()
    B.multTranspose(e, rhs)
    corrStruct = Vh.vector()
    prior.Rsolver.solve(corrStruct, rhs)
    return corrStruct


def makeCorrelationPlots(mesh, sigma2, rho, coords, degree=2, cmap="viridis", **kwargs):
    ndim = mesh.Dimension()
    Vh = hm.FunctionSpace.H1(mesh, 1)
    prior = make_prior(Vh, sigma2, rho, degree=degree, **kwargs)

    pointwise_variance = prior.pointwise_variance(method="Exact")
    correlation_struc = correlationStructure(prior, Vh, coords)

    print("Prescribed marginal variance: ", sigma2)
    print("Actual marginal variance:", correlation_struc.norm("linf"))

    if ndim == 1:
        fig, axes = plt.subplots(1, 2, figsize=[15, 5])
        nb.plot(Vh, pointwise_variance, ax=axes[0], mytitle="Marginal variance")
        nb.plot(Vh, correlation_struc, ax=axes[1], mytitle="Correlation")
        ymax = correlation_struc.max()
        axes[1].vlines(x=[coords], ymin=[0], ymax=[0.98 * ymax], colors="black", ls="--", lw=2)
        arr = mpatches.FancyArrowPatch((coords, 0.1 * ymax), (coords + rho, 0.1 * ymax),
                                       arrowstyle="<->,head_width=.15", mutation_scale=20)
        axes[1].add_patch(arr)
        axes[1].annotate(r"$\rho = %g$" % rho, (0.5, 0.5), xycoords=arr, ha="center",
                         va="center", fontsize=15, bbox=dict(fc="white", ec="none"))
    else:
        nb.multi1_plot([(Vh, pointwise_variance), (Vh, correlation_struc)],
                       ["Marginal variance", "Correlation"], same_colorbar=False, cmap=cmap)
    plt.show()

The 1D case

With Robin boundary conditions the marginal variance is close to the prescribed value up to the boundary; with the natural (Neumann) condition it doubles there, the boundary artifact the Robin condition is designed to remove.

mesh = mfem.ParMesh(comm, mfem.Mesh.MakeCartesian1D(100, 1.0))
sigma2 = 4
rho = 0.25
coords = 0.5

makeCorrelationPlots(mesh, sigma2, rho, coords)

coords = 0.75

makeCorrelationPlots(mesh, sigma2, rho, coords, use_robin=False)
Prescribed marginal variance:  4
Actual marginal variance: 4.003204476208765
../_images/1732f6b9c63d4bfdbc2923e14e0c0f3cd1a3c15af824bcfe0483eb29c1dbdbc9.png
Prescribed marginal variance:  4
Actual marginal variance: 4.034133932273951
../_images/53a800958e80527fa656b8d3b8498e7afd4cd888a5767f772d07de4a4597621e.png

The 2D case

mesh = mfem.ParMesh(comm, mfem.Mesh.MakeCartesian2D(64, 64, mfem.Element.TRIANGLE))
sigma2 = 4
rho = 0.25
coords = (0.5, 0.5)

makeCorrelationPlots(mesh, sigma2, rho, coords, cmap="jet")

coords = (0.75, 0.75)
makeCorrelationPlots(mesh, sigma2, rho, coords, cmap="jet")

coords = (0.75, 0.75)
makeCorrelationPlots(mesh, sigma2, rho, coords, cmap="jet", use_robin=False)
Prescribed marginal variance:  4
Actual marginal variance: 3.98422011521004
../_images/0f83e349f96a4a3923a950b91fb85030750962c4bbcd808fef9d44b59542e320.png
Prescribed marginal variance:  4
Actual marginal variance: 3.9964579488847014
../_images/7c88db8803b621f38b9e139875d56fd4c44c430042b5ebe95e06c1821e7c8e84.png
Prescribed marginal variance:  4
Actual marginal variance: 4.077372573874734
../_images/c074af03709288f1742c07232643cc860f3280dd74ea13d582b2c8ce66b67889.png

Generating Samples

A sample is the prior’s square root applied to white noise: sample_noise draws the noise element by element from a counter-based generator (so a sample is the same on any number of MPI ranks), and sample solves with it.

def makeSamplePlots(mesh, sigma2, rho, degree=2, with_correlation=False, same_colorbar=True,
                    cmap=None, **kwargs):
    ndim = mesh.Dimension()
    Vh = hm.FunctionSpace.H1(mesh, 1)
    prior = make_prior(Vh, sigma2, rho, degree=degree, **kwargs)

    def sample(prior):
        noise = prior.noise_vector()
        prior.sample_noise(1.0, noise)
        s = Vh.vector()
        prior.sample(noise, s)
        return s

    plot_items = [(Vh, sample(prior)) for i in range(2)]
    labels = ["Sample 1", "Sample 2"]

    if with_correlation:
        center = tuple(ndim * [0.5])
        plot_items = [(Vh, correlationStructure(prior, Vh, center))] + plot_items
        labels = ["Correlation"] + labels

    nb.multi1_plot(plot_items, labels, same_colorbar=same_colorbar, cmap=cmap)
    plt.show()


hm.parRandom.set_seed(1)

Isotropic samples

mesh = mfem.ParMesh(comm, mfem.Mesh.MakeCartesian2D(64, 64, mfem.Element.TRIANGLE))
sigma2 = 4
rho = 0.25
makeSamplePlots(mesh, sigma2, rho)
../_images/748d58e163027e01e43854a49829dfe3a840e0e252cfc1605ee5adbc7ba9a723.png

Anisotropic samples with \(\alpha = \frac{\pi}{4}\)

kwargs = {"theta0": 2.0, "theta1": 0.5, "alpha": math.pi / 4, "anisotropic": True}
makeSamplePlots(mesh, sigma2, rho, **kwargs)

coords = (0.5, 0.5)
makeCorrelationPlots(mesh, sigma2, rho, coords, cmap="jet", **kwargs)

makeSamplePlots(mesh, sigma2, rho, with_correlation=True, same_colorbar=False, **kwargs)
../_images/60b6872212f3ab9580fefe42ae523929010915cb818ba1530b9a0524206378bf.png
Prescribed marginal variance:  4
Actual marginal variance: 4.002680048624528
../_images/d99c10a9f63bdaadb82b70314a2416cdbb63ff7f859e7dadce7551b21319f93f.png ../_images/544aa80c545cf3fd06c419b7b67c869bfe6a61c722dbc0c4a772c6142e61e060.png

Anisotropic samples with \(\alpha = -\frac{\pi}{4}\)

kwargs = {"theta0": 2.0, "theta1": 0.5, "alpha": -math.pi / 4, "anisotropic": True}
makeSamplePlots(mesh, sigma2, rho, **kwargs)

coords = (0.5, 0.5)
makeCorrelationPlots(mesh, sigma2, rho, coords, cmap="jet", **kwargs)

makeSamplePlots(mesh, sigma2, rho, with_correlation=True, same_colorbar=False, **kwargs)
../_images/9ef49231570cbfff5ba6b526923a180e9688631e52d389d1adca4f37b7fe0aef.png
Prescribed marginal variance:  4
Actual marginal variance: 3.833567562702267
../_images/bebb6eb675c3d2441df115df4d25b53b725e809cbfae8494a06fb2bb8d5accef.png ../_images/c1c83203e4f5e33b6b6a590af2aade2f17beb789a0224cd84d4a249c4b0634a3.png

Mesh Dependence and Independence

A prior is a property of the continuous problem, so refining the mesh should not change it. In two dimensions that holds for the BiLaplacian prior (\(\alpha = 2 > d/2\)) but not for the Laplacian prior (\(\alpha = 1 = d/2\)), whose covariance is not trace class: its samples get rougher and its pointwise variance grows as the mesh is refined, most visibly where the mesh is refined locally.

The locally refined mesh bisects, four times, the triangles whose centroids lie in \((0.2, 0.5) \times (0.3, 0.7)\); MFEM’s conforming refinement keeps it free of hanging nodes.

def locallyRefinedMesh():
    serial = mfem.Mesh.MakeCartesian2D(16, 16, mfem.Element.TRIANGLE)
    for _ in range(4):
        verts = np.asarray(serial.GetVertexArray(), dtype=float).reshape(serial.GetNV(), 2)
        marked = []
        for e in range(serial.GetNE()):
            c = verts[list(serial.GetElementVertices(e))].mean(axis=0)
            if 0.2 < c[0] < 0.5 and 0.3 < c[1] < 0.7:
                marked.append(e)
        serial.GeneralRefinement(mfem.intArray(marked), 0)
    return mfem.ParMesh(comm, serial)


mesh1 = mfem.ParMesh(comm, mfem.Mesh.MakeCartesian2D(16, 16, mfem.Element.TRIANGLE))
mesh2 = mfem.ParMesh(comm, mfem.Mesh.MakeCartesian2D(64, 64, mfem.Element.TRIANGLE))
mesh3 = locallyRefinedMesh()

plt.figure(figsize=(18, 6))
for i, (m, title) in enumerate(((mesh1, "Coarse mesh"), (mesh2, "Fine mesh"), (mesh3, "Locally refined"))):
    nb.plot(m, subplot_loc=131 + i, mytitle=title)
plt.show()
../_images/1d99e71650151fe93cd48742dcc7b51cdc570a8a2e1f40b2dc0fd964d8c3bb69.png

Laplacian plots

sigma2 = 4
rho = 0.25
coords = (0.5, 0.5)

meshes = {"coarse": mesh1, "fine": mesh2, "refined": mesh3}

for name, mesh in meshes.items():
    print("#### %s mesh" % name)
    makeSamplePlots(mesh, sigma2, rho, degree=1)
    makeCorrelationPlots(mesh, sigma2, rho, coords, degree=1, cmap="jet")
#### coarse mesh
../_images/a1d66d43193692e0cead0101e2e30030bd35ef50e460c70fd528dda0b4868ba3.png
Prescribed marginal variance:  4
Actual marginal variance: 26.770020794493547
../_images/31334aa873a9ca7cb8d65ea96393bbf3382c1890c814b47a52390b69528a2cfc.png
#### fine mesh
../_images/d8572cea7cedd362cf10ee2ca3694309c2b40d3d9b35e2907330e548d2e91385.png
Prescribed marginal variance:  4
Actual marginal variance: 44.27759009876061
../_images/1514bec47bef18ea633d4a5734e07c20d8c14e389e9644a729adfff54e92b467.png
#### refined mesh
../_images/2af6bffe6d6ebc0dce6b5f163747cf386abd6bed893b9c90a30b2699915796e9.png
Prescribed marginal variance:  4
Actual marginal variance: 59.818487698341194
../_images/684600512e881e3ba6bd07296c49484998d78bd164ef2b83b079dc46538e6c41.png

BiLaplacian Plots

for name, mesh in meshes.items():
    print("#### %s mesh" % name)
    makeSamplePlots(mesh, sigma2, rho, degree=2)
    makeCorrelationPlots(mesh, sigma2, rho, coords, degree=2, cmap="jet")
#### coarse mesh
../_images/04525e736f8d5070309f33e3449fd5e9fddcd9d3bd8919b5b9a42c672228eb27.png
Prescribed marginal variance:  4
Actual marginal variance: 3.9152890106357536
../_images/11fe6d701de4b2cf4979286d7bc4fe6b52701a1f1528d455743036463688d48e.png
#### fine mesh
../_images/d6fa601b822a0c83fd0827702cd8ba74fd3a0e9d858ac0f2ab044c2bc4cdf158.png
Prescribed marginal variance:  4
Actual marginal variance: 3.98422011521004
../_images/0f83e349f96a4a3923a950b91fb85030750962c4bbcd808fef9d44b59542e320.png
#### refined mesh
../_images/96ba39f8909967155a074c7331e94a42adeedc01e9dc65b3094d46d59841f59a.png
Prescribed marginal variance:  4
Actual marginal variance: 3.9118855544860858
../_images/eb78c7d77719b578475de704684ec722bb95e7dc22769ffa09ffcf1474ecb095.png

Adapted for hIPPyMFEM from hIPPYlib’s tutorial 07_GaussianPriors.

Copyright (c) 2016-2018, The University of Texas at Austin & University of California, Merced.
Copyright (c) 2019-2022, The University of Texas at Austin, University of California–Merced, Washington University in St. Louis.
Copyright (c) 2023-, The University of Texas at Austin, University of California–Merced.
Copyright (c) 2026, Peng Chen, Georgia Institute of Technology.
See file COPYRIGHT for details.

This file is part of hIPPyMFEM, free software under the GNU General Public License version 2.0 dated June 1991 (GPL-2.0-only). hIPPyMFEM follows the programming model of hIPPYlib.