Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Markov Gaussian Processes — Kalman Filtering and RTS Smoothing

Open In Colab

This notebook is the second installment of pyrox’s Markov-GP track. The companion notebook (markov_gp_sde_kernels) introduced the representation layer — MaternSDE, SumSDE, PeriodicSDE, and friends — and verified that the SDE form recovers the same continuous autocovariance as the dense kernel. Here we plug those kernels into a Kalman filter / RTS smoother and turn them into a working temporal-GP model: marginal likelihood via the forward filter, posterior smoothing via the backward pass, and predictions at arbitrary test times by re-running filter+smoother over the merged grid with test points masked out of the update step.

The full pyrox surface is exposed via three names:

  • MarkovGPPrior(sde_kernel, times) — the prior over a sorted 1-D grid.
  • markov_gp_factor(name, prior, y, noise_var) — collapsed Gaussian-likelihood factor for NumPyro models.
  • prior.condition(y, noise_var).predict(t_star) — (mean, var) at arbitrary test times.

We will exercise all three.

Background — from SDE kernel to Kalman recursion

Given an SDE-form stationary kernel (F,L,H,Qc,P∞)\big(F, L, H, Q_c, P_\infty\big) and a sorted observation grid t1<t2<⋯<tNt_1 < t_2 < \dots < t_N, the discrete-time linear-Gaussian model is

xk=Akxk−1+qk,qk∼N(0,Qk),yk=Hxk+εk,\mathbf{x}_{k} = A_k \mathbf{x}_{k-1} + \mathbf{q}_k, \qquad \mathbf{q}_k \sim \mathcal{N}(0, Q_k), \qquad y_k = H \mathbf{x}_k + \varepsilon_k,

with Ak=exp⁡(F Δtk)A_k = \exp(F\,\Delta t_k) and Qk=P∞−AkP∞Ak⊤Q_k = P_\infty - A_k P_\infty A_k^\top, and εk∼N(0,σ2)\varepsilon_k \sim \mathcal{N}(0, \sigma^2). The initial state is the stationary (0,P∞)(\mathbf{0}, P_\infty).

Forward Kalman filter (one pass through the grid):

mk−=Akmk−1,Pk−=AkPk−1Ak⊤+Qk,\mathbf{m}_k^- = A_k \mathbf{m}_{k-1}, \quad P_k^- = A_k P_{k-1} A_k^\top + Q_k,
mk=mk−+Kk(yk−Hmk−),Pk=(I−KkH)Pk−(I−KkH)⊤+σ2KkKk⊤.\mathbf{m}_k = \mathbf{m}_k^- + K_k\big(y_k - H\mathbf{m}_k^-\big), \quad P_k = (I - K_k H) P_k^- (I - K_k H)^\top + \sigma^2 K_k K_k^\top.

The marginal log-likelihood drops out as the cumulative innovation contribution

log⁡p(y1:N∣θ)=∑k=1N−12[log⁡(2πSk)+(yk−Hmk−)2Sk],Sk=HPk−H⊤+σ2.\log p(y_{1:N}\mid\theta) = \sum_{k=1}^{N} -\tfrac{1}{2}\Big[\log(2\pi S_k) + \tfrac{(y_k - H\mathbf{m}_k^-)^2}{S_k}\Big], \qquad S_k = H P_k^- H^\top + \sigma^2.

Backward RTS smoother uses the filtered sequence to produce E[xk∣y1:N]\mathbb{E}[\mathbf{x}_k \mid y_{1:N}] in a second pass.

Both passes cost O(N d3)\mathcal{O}(N\,d^3) — linear in NN, where the dense Cholesky path is O(N3)\mathcal{O}(N^3). Below we verify bit-perfect agreement between the two paths on small grids, then time them on a large grid to see the constant-factor crossover.

Setup

Detect Colab and install pyrox[colab] (which pulls in matplotlib and watermark) only when running there.

Python implementation: CPython
Python version       : 3.13.5
IPython version      : 9.10.0

jax       : 0.9.2
equinox   : 0.13.6
numpyro   : 0.20.1
pyrox     : 0.0.8
matplotlib: 3.10.8

Compiler    : GCC 11.2.0
OS          : Linux
Release     : 6.8.0-1044-azure
Machine     : x86_64
Processor   : x86_64
CPU cores   : 16
Architecture: 64bit

1. Equivalence with the dense GP — bit-perfect agreement

The whole story rests on a single equivalence: on the same kernel and the same data, the Kalman / RTS path must produce the same log marginal likelihood and the same posterior marginals as the dense GP. We verify this directly on a small irregular grid for all three Matern orders.

order    nu     KF log-marg    dense log-marg      |diff|
------------------------------------------------------------
    0   0.5   -8.3862650295     -8.3862650295    8.88e-15
    1   1.5   -7.8576203280     -7.8576203280    0.00e+00
    2   2.5   -7.6724597592     -7.6724597592    6.22e-15

Both paths agree to machine precision. That is the load-bearing test: every other claim about the Markov path follows from this.

2. End-to-end fit on a noisy time series

Fit a MarkovGPPrior with a Matern-3/2 kernel to a noisy oscillatory signal and visualize the smoothed posterior.

<Figure size 1000x450 with 1 Axes>

Inside the data window the smoother shrinks the uncertainty band tightly around the data. Outside (left of 0 and right of 8) the predictive variance grows back toward the stationary prior P∞P_\infty — the standard “forecast variance opens up” behaviour of stationary GPs.

3. Posterior parity with the dense path

Side-by-side: the dense GPPrior posterior and the Markov MarkovGPPrior posterior must coincide. We use a smaller dataset so the dense path is tractable.

<Figure size 1100x400 with 2 Axes>
max |mean_kf - mean_dense| = 3.13e-14
max |var_kf  - var_dense | = 1.65e-14

Bit-perfect agreement between the Markov path and the dense GP — every visual difference is sub-machine-precision noise.

4. Cost crossover — when does Markov beat dense?

The Markov path costs O(N d3)\mathcal{O}(N\,d^3) per pass, and the dense Cholesky path costs O(N3)\mathcal{O}(N^3). The constant factor on the Markov side is non-trivial — each Kalman step involves an expm\mathrm{expm} (or closed-form transition) plus several d×dd \times d matvecs. We measure the marginal-likelihood evaluation time across a sweep of NN values.

<Figure size 800x450 with 1 Axes>

The constant-factor advantage favours the dense path on tiny grids (one Cholesky on a small matrix is unbeatable), but the cubic vs. linear scaling crosses over quickly. The Markov path is the only practical option once NN grows past a few thousand.

5. Composition — SumSDE for trend + roughness

MarkovGPPrior accepts any SDEKernel. Sums of kernels stay efficient because the state space is the block diagonal of the components — total cost is still linear in NN, with state dimension ∑idi\sum_i d_i. We fit a SumSDE of a smooth Matern-5/2 (long lengthscale) plus a rough Matern-1/2 (short lengthscale) to a signal that has both components, then read off the individual contributions.

<Figure size 1000x450 with 1 Axes>
log marginal (SumSDE):              86.21
log marginal (smooth-only Matern-5/2): -698.05
log marginal (rough-only  Matern-1/2): -483.77

The composite model substantially outperforms either single-Matern model, as expected — the data has both a smooth low-frequency component and a high-frequency residual the smooth model alone can’t explain.

6. Inside a NumPyro model — markov_gp_factor

The collapsed-Gaussian-likelihood factor drops into a NumPyro model the same way gp_factor does, but uses Kalman filtering for the marginal likelihood. We sketch a tiny model that learns the kernel hyperparameters by MAP — full SVI / MCMC works the same way, but is heavier than this notebook needs.

MAP variance:    0.540
MAP lengthscale: 1.063

The same model would run under MCMC or full SVI without code changes — markov_gp_factor slots into NumPyro identically to gp_factor, but with the linear-time cost profile.

What’s next

This PR ships Gaussian-likelihood temporal regression on a single time axis. Subsequent waves cover non-Gaussian likelihoods on top of the Markov path (CVI / EP), spatio-temporal Markov priors, and natural-gradient inference for the posterior over latent trajectories.