---
title: "FEM Matérn methods: SPDE, B-splines, and covariance comparison"
author: "Patrick Brown"
date: "`r Sys.Date()`"
output:
  rmarkdown::html_vignette:
    toc: true
vignette: >
  %\VignetteIndexEntry{FEM Matérn methods: SPDE, B-splines, and covariance comparison}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup}
library(adlaplaceFem)
library(Matrix)
```

# Continuous model: Matérn via Whittle SPDE

A zero-mean Gaussian field $u(\mathbf{s})$ on $\mathbb{R}^d$ with Matérn covariance

$$
\operatorname{Cov}\bigl(u(\mathbf{s}),u(\mathbf{s}')\bigr)
=
\frac{\sigma^2}{2^{\nu-1}\Gamma(\nu)}
(\kappa\|\mathbf{h}\|)^\nu K_\nu(\kappa\|\mathbf{h}\|),
\quad \mathbf{h}=\mathbf{s}-\mathbf{s}',
$$

is a stationary solution of

$$
\tau\,(\kappa^2-\Delta)^{\alpha/2}\,u(\mathbf{s})=\mathcal{W}(\mathbf{s}),
\qquad \alpha=\nu+d/2,
$$

with Gaussian white noise $\mathcal{W}$ (Whittle; Lindgren, Rue & Lindström 2011).
The continuous precision operator is $\tau^2(\kappa^2-\Delta)^\alpha$.

In $d=2$:

| α | ν | SPDE | Precision operator |
|---|----|------|--------------------|
| 2 | 1 | $\tau(\kappa^2-\Delta)u=\mathcal{W}$ | $\tau^2(\kappa^2-\Delta)^2$ |
| 3 | 2 | $\tau(\kappa^2-\Delta)^{3/2}u=\mathcal{W}$ | $\tau^2(\kappa^2-\Delta)^3$ |

On a bounded domain, homogeneous Neumann boundary conditions are used; pad the
domain by about half a correlation range in applications.

# Parameterization: range and sd

The FEM term exposes hyperparameters $(\mathrm{range},\mathrm{sd})$ (optimized as
$\log\mathrm{range}$, $\log\mathrm{sd}$), not the raw SPDE pair $(\kappa,\tau)$.
The public `range` is the same **practical range** as geostatsp/INLA,
$\rho=\sqrt{8\nu}/\kappa$. Internally the AD kernels convert

$$
\kappa = \sqrt{8\nu}/\mathrm{range},
\qquad
\tau =
\begin{cases}
1\big/\bigl(\kappa\,\sigma\sqrt{4\pi}\bigr) & \alpha=2\ (\nu=1),\\
1\big/\bigl(\kappa^2\,\sigma\sqrt{8\pi}\bigr) & \alpha=3\ (\nu=2),
\end{cases}
$$

with $\sigma=\mathrm{sd}$, so that $\mathrm{sd}$ is the **marginal standard
deviation** of the Matérn field $u(\mathbf{s})$ on the linear-predictor scale.
In 2D, $\nu=\alpha-1$.

| Quantity | Meaning | Units (loaloa) |
|----------|---------|----------------|
| Coordinates / knots | Spatial location | m |
| `range` $=\rho=\sqrt{8\nu}/\kappa$ | Practical range (≈0.1 correlation) | m |
| `sd` $=\sigma$ | Marginal SD of $u(\mathbf{s})$ | same as $u$ (e.g. logit) |
| $\tau$ | Internal SPDE precision scale | derived from $\kappa,\sigma$ |

So with coordinates in metres and $\nu=2$, `range = 1e5` means a practical range
of $100\,\mathrm{km}$ and $\kappa=4\times10^{-5}\,\mathrm{m}^{-1}$.

**geostatsp / INLA `glgm`** uses the same `(range, sd)` meaning, so coefficient
tables can be compared directly:

```r
cf <- coef(fit)
c(
  range = unname(cf[["geometry_matern_log_range"]]),
  sd = unname(cf[["geometry_matern_log_sd"]])
)
```

(`glgm` may label the range row `range/1000` when the summary is in kilometres;
convert by $\times 1000$ first.)

The Gram matrices $\mathbf{C}$, $\mathbf{G}$, … use the same coordinate units as
the knots, so `range` is already on that scale.

# Finite-element weights and Grams

Write $\widetilde{u}(\mathbf{s})=\boldsymbol{\psi}(\mathbf{s})^\top\mathbf{w}$.
The weight precision is assembled from

| Matrix | Entries |
|--------|---------|
| Mass $\mathbf{C}$ | $C_{ij}=\langle\psi_i,\psi_j\rangle$ |
| Stiffness $\mathbf{G}$ | $G_{ij}=\langle\nabla\psi_i,\nabla\psi_j\rangle$ |
| Biharmonic $\mathbf{G}_2$ | $(G_2)_{ij}=\langle\Delta\psi_i,\Delta\psi_j\rangle$ |
| Third-order $\mathbf{G}_3$ | $(G_3)_{ij}=\langle\nabla\Delta\psi_i,\nabla\Delta\psi_j\rangle$ |

Working formulas (higher-order B-splines; no mass lumping):

$$
\mathbf{Q}_2=\tau^2\bigl(\kappa^4\mathbf{C}+2\kappa^2\mathbf{G}+\mathbf{G}_2\bigr),
$$

$$
\mathbf{Q}_3=\tau^2\bigl(\kappa^6\mathbf{C}+3\kappa^4\mathbf{G}+3\kappa^2\mathbf{G}_2+\mathbf{G}_3\bigr).
$$

These replace Galerkin expressions involving $\mathbf{C}^{-1}$, which destroy sparsity.
Recommended B-spline degrees: $p=2$ for α = 2; $p=3$ for α = 3.

# Tensor-product B-splines

On a rectangle, with 1-D B-splines $B_i^x$ and $B_j^y$,
$\psi_{ij}(x,y)=B_i^x(x)\,B_j^y(y)$ and the Grams factor as Kronecker sums

$$
\mathbf{C}=\mathbf{C}_x\otimes\mathbf{C}_y,
$$

$$
\mathbf{G}=\mathbf{G}_x\otimes\mathbf{C}_y+\mathbf{C}_x\otimes\mathbf{G}_y,
$$

$$
\mathbf{G}_2=\mathbf{G}_{2,x}\otimes\mathbf{C}_y+\mathbf{C}_x\otimes\mathbf{G}_{2,y}+2\,\mathbf{G}_x\otimes\mathbf{G}_y.
$$

# Numerical comparison: Matérn vs FEM

Fix α = 2, $\nu=1$, $\kappa=2$, $\tau$ chosen so the Matérn marginal variance is 1:

```{r params}
nu <- 1
kappa <- 3
# sigma^2 = Gamma(nu) / ( Gamma(alpha) (4*pi)^{d/2} kappa^{2*nu} tau^2 ) with alpha=2, d=2
# => 1 = 1 / (1 * 4*pi * kappa^2 * tau^2)  => tau^2 = 1/(4*pi*kappa^2)
tau <- 1 / sqrt(4 * pi * kappa^2)
rho <- sqrt(8 * nu) / kappa
c(kappa = kappa, tau = tau, range = rho)
```

Evaluation sites (same for both grids):

```{r sites}
knots_coarse <- list(
  x = seq(-0.2, 1.2, by = 0.2),
  y = seq(-0.2, 1.2, by = 0.2)
)
knots_fine <- list(
  x = seq(-1, 2, by = 0.1),
  y = seq(-1, 2, by = 0.1)
)

set.seed(0)
sites_eval <- data.frame(
  x = runif(100),
  y = runif(100)
)


matern_cov <- function(h, nu, kappa, sigma2 = 1) {
  h <- pmax(h, 1e-12)
  r <- kappa * h
  sigma2 * (2^(1 - nu) / gamma(nu)) * (r^nu) * besselK(r, nu)
}

D <- as.matrix(dist(sites_eval))
Sigma <- matern_cov(D, nu, kappa, sigma2 = 1)
diag(Sigma) <- 1
```

FEM covariance at the same sites: $\mathbf{A}\mathbf{Q}^{-1}\mathbf{A}^\top$ for two knot resolutions.

```{r compare}
fem_cov_at_sites <- function(sites_eval, knots_list) {
  fem <- fem_bspline(
    sites_eval,
    knots_list,
    degree = 2L
  )
  Q <- fem_precision(kappa, tau, fem$C, fem$G, fem$G2, alpha = 2L)
  A <- fem$A
  X <- Matrix::solve(Q, Matrix::t(A))
  as.matrix(A %*% X)
}

Sig_coarse <- fem_cov_at_sites(sites_eval, knots_coarse)
Sig_fine <- fem_cov_at_sites(sites_eval, knots_fine)

rel_err <- function(S_hat, S) {
  sqrt(sum((S_hat - S)^2)) / sqrt(sum(S^2))
}

data.frame(
  grid = c("coarse (by = 0.2)", "fine (by = 0.1)"),
  frobenius_rel = c(rel_err(Sig_coarse, Sigma), rel_err(Sig_fine, Sigma)),
  max_abs = c(max(abs(Sig_coarse - Sigma)), max(abs(Sig_fine - Sigma))),
  mean_diag = c(mean(diag(Sig_coarse)), mean(diag(Sig_fine)))
)
```

Correlation versus distance (exact Matérn vs FEM):

```{r plot-corr, fig.width=7, fig.height=4}
upper <- upper.tri(D)
df <- data.frame(
  dist = D[upper],
  matern = Sigma[upper] / sqrt(diag(Sigma)[row(Sigma)[upper]] * diag(Sigma)[col(Sigma)[upper]]),
  coarse = Sig_coarse[upper] / sqrt(pmax(diag(Sig_coarse)[row(Sig_coarse)[upper]], 1e-8) *
    pmax(diag(Sig_coarse)[col(Sig_coarse)[upper]], 1e-8)),
  fine = Sig_fine[upper] / sqrt(pmax(diag(Sig_fine)[row(Sig_fine)[upper]], 1e-8) *
    pmax(diag(Sig_fine)[col(Sig_fine)[upper]], 1e-8))
)
df <- df[order(df$dist), ]
plot(df$dist, df$matern,
  type = "l", lwd = 2,
  ylim = c(0, 1),
  xlim = c(0, 1),
  xlab = "distance", ylab = "correlation",
  main = "Matern vs FEM correlations"
)
lines(df$dist, df$coarse, col = "tomato", lty = 2, lwd = 4)
lines(df$dist, df$fine, col = "steelblue", lty = 3, lwd = 2)
legend("topright", c("Matern", "FEM coarse", "FEM fine"),
  col = c("black", "tomato", "steelblue"), lty = c(1, 2, 3), lwd = c(2, 1, 1), bty = "n"
)
```

The finer knot grid tracks the Matérn correlation more closely (especially away from the boundary).

# References

Whittle (1954, 1963); Lindgren, Rue & Lindström (2011); Bolin & Lindgren (CSDA 2013);
Liu, Guillas & Lai (2016); Lindgren, Bolin & Rue (2022); Bakka (arXiv:1803.03765).
