Skip to content

Vision

gaussx is a JAX/Equinox library for structured linear operators, Gaussian distributions, and exponential-family primitives.

Or, less politely: your covariance matrix has structure, your code is throwing it away, and we would like a word.

The thirty-second version

Somewhere in your program there is a matrix. It is not a random pile of numbers --- it is a Kronecker product, or block diagonal, or low-rank plus a diagonal, or Toeplitz, or the precision matrix of a Markov chain. It knows something about itself.

And then you call jnp.linalg.solve on it, and all of that knowledge is thrown in the bin, and your laptop spends four minutes doing arithmetic that a pocket calculator could have done in one.

This is, we regret to say, barbaric.

import gaussx

K = gaussx.Kronecker(A, B)   # 10,000 x 10,000, made of two 100 x 100 factors

x  = gaussx.solve(K, y)      # 100 x 100 twice, not 10,000 x 10,000 once
ld = gaussx.logdet(K)        # a weighted sum of two small logdets
L  = gaussx.cholesky(K)      # a Kronecker of two small Cholesky factors

Same three lines you already write. Same math you already know. Around half a million times less arithmetic (\(10^{12}\) flops down to \(2 \times 10^6\)). You are welcome.

If your matrix is... Dense LAPACK gaussx
Kronecker \(A \otimes B\), factors \(n_1, n_2\) \(O((n_1 n_2)^3)\) \(O(n_1^3 + n_2^3)\)
Block diagonal, \(k\) blocks of size \(m\) \(O((km)^3)\) \(O(k m^3)\)
Low-rank + diagonal, rank \(k\) \(O(n^3)\) \(O(n k^2)\) via Woodbury
Block-tridiagonal (state-space), \(N\) steps \(O((Nd)^3)\) \(O(N d^3)\)
Toeplitz \(O(n^3)\) \(O(n \log n)\) matvecs via FFT
Genuinely unstructured and enormous it will not finish matrix-free CG + stochastic logdet

Those are not marketing numbers. They are the exponents. Exponents do not negotiate.

Why this keeps mattering

Linear algebra is the load-bearing wall of scientific computing, and almost everyone treats it as wallpaper. Nearly every algorithm you care about eventually bottlenecks on the same tiny handful of operations:

  • Linear regression solves \((X^\top X)\beta = X^\top y\)
  • Gaussian processes need \((K + \sigma^2 I)^{-1} y\) and \(\log|K + \sigma^2 I|\) for every likelihood evaluation
  • Kalman filters compute the gain \(K = P H^\top (H P H^\top + R)^{-1}\) at every time step
  • Variational inference wants \(\log|\Sigma|\) and samples from \(\mathcal{N}(\mu, \Sigma)\) --- both need a square root
  • Natural gradients invert the Fisher information, \(F^{-1}\nabla\mathcal{L}\), at every update
  • Ensemble methods build empirical covariances that are low-rank by construction
  • Spatial statistics on grids produce \(K_x \otimes K_y\), where \(O(N^3)\) collapses to \(O(n_x^3 + n_y^3)\)
  • PDE solvers invert elliptic operators and precondition iterative methods
  • Optimal transport runs Sinkhorn iterations that are just matvecs wearing a hat

Five verbs --- solve, logdet, cholesky, trace, sqrt --- carry entire research fields on their back. They deserve to be written once, by someone paying attention.

The part that is genuinely annoying

Here is what actually happens in the wild. The Woodbury identity gets hand-coded in the GP library. Then again in the filtering library. Then again, subtly differently and with one sign wrong, in the Bayesian optimization library. Every implementation is correct, and every implementation is alone.

So the bug fix in repo #1 never reaches repo #3. The clever preconditioner in repo #2 dies with the PhD student who wrote it. And every newcomer must climb a wall of bespoke matrix code before touching the science they actually came for.

We find this waste offensive. Not "inefficient" --- offensive. gaussx exists to end it: write the structured operators and the dispatch once, carefully, and let everyone downstream inherit the good version.

But surely someone has done this?

Partly. Everyone has done a piece of it, and we say that with real affection --- gaussx is built on two of them.

Library What it does beautifully Where it stops
lineax Superb solvers (CG, Cholesky, LU, GMRES, ...), a clean operator abstraction, properly JAX-native No Kronecker / block-diagonal / low-rank operators; no logdet, trace, or sqrt
CoLA A rich operator zoo and real matrix functions Multi-backend by design, so not Equinox-native; thinner solver coverage
TFP on JAX Battle-tested LinearOperators with batching Arrives towing an entire TensorFlow

So: lineax has the solvers but not the shapes. CoLA has the shapes but not the home. TFP has both and a suitcase.

gaussx is the missing middle --- lineax's solvers, a broader operator zoo, a Gaussian distribution layer on top, and every last line of it native to JAX and Equinox. One pip install. No suitcase.

Who this is for

The GP researcher. "My spatiotemporal kernel is Kronecker-structured. I want logdet and solve to notice that by themselves, forever, without me re-deriving the eigendecomposition trick at 1am."

The data-assimilation person. "Ensemble covariance is low-rank. Kalman gain is Woodbury. I want to write low_rank_plus_diag(noise, U) and receive a real structured operator, not a 40,000 x 40,000 dense array and a memory error."

The Bayesian ML researcher. "Natural gradients. I need natural \(\leftrightarrow\) expectation conversions and Fisher information for Gaussians, over structured precision operators, differentiable end to end."

The library author. "I am building the next GP / filtering / optimization package and I refuse to hand-roll solve, logdet, and cholesky with a per-type dispatch table one more time."

The newcomer. "I would like gaussx.solve(K, y) to simply work." It does. That is the whole idea.

Five principles we will not apologise for

# Principle In practice
1 Extend, don't replace We build on lineax's AbstractLinearOperator and its solvers. Rewriting excellent code to put your own name on it is vanity, not engineering.
2 Structure drives dispatch Operators carry structural tags (PSD, symmetric, Kronecker, low-rank, ...). Primitives read the type and the tags and pick the fast path. Plain isinstance checks. No metaclasses, no registry, no mystery.
3 Math-first layers solve(A, b), logdet(A), cholesky(A). You should be able to read the source with the paper open and see the same equation.
4 One distribution, many strategies MultivariateNormal takes any covariance operator and any solver strategy. The distribution owns the math; the strategy owns the numerics; neither is allowed to meddle.
5 Readable tensor code Every reshape and contraction goes through einx, so a Kronecker matvec reads like Roth's column lemma instead of like index golf.

Principle 5 is the one people call fussy. It is not fussy. It is the difference between code you can review and code you merely hope about.

What gaussx refuses to be

A library that does everything does nothing especially well. We have chosen our scope, and we intend to defend it.

Not this Go here instead
A GP modelling library (kernels with priors, model shells, inference loops) pyrox-gp
Probabilistic programming (MCMC, SVI, samplers) NumPyro
General-purpose optimization optax / optimistix
PDE discretization, grids, boundary conditions finitevolX / spectraldiffx
Ensemble filtering applications filterax
Bayesian optimizers as optax transforms optax_bayes
Multi-backend support (PyTorch, NumPy, ...) JAX only, on purpose, forever

gaussx is structured operators and the primitives over them. That is the whole job. Everything else is somebody else's excellent library, and we would rather link to it than imitate it.

The family

gaussx is the linear-algebra floor that the rest of the stack stands on.

flowchart TD
    subgraph FOUND["Foundations"]
        LX["lineax<br/><small>solvers, operators, tags</small>"]
        MF["matfree<br/><small>Lanczos, SLQ, Hutchinson</small>"]
        EQX["equinox + jaxtyping + einx"]
    end

    GX["<b>gaussx</b><br/><small>structured operators · primitives · Gaussians</small>"]

    subgraph DOWN["Downstream"]
        PYROX["pyrox-gp<br/><small>GP models on NumPyro</small>"]
        FVX["finitevolX<br/><small>finite-volume PDE ops</small>"]
    end

    subgraph PLAN["Downstream (planned)"]
        SDX["spectraldiffx<br/><small>pseudospectral PDE ops</small>"]
        FLTR["filterax<br/><small>ensemble filtering</small>"]
        OB["optax_bayes<br/><small>natural-gradient optimizers</small>"]
    end

    LX --> GX
    MF --> GX
    EQX --> GX
    GX --> PYROX
    GX --> FVX
    GX -.-> SDX
    GX -.-> FLTR
    GX -.-> OB

    click LX "https://github.com/patrick-kidger/lineax" _blank
    click MF "https://github.com/pnkraemer/matfree" _blank
    click PYROX "https://github.com/jejjohnson/pyrox" _blank
    click FVX "https://github.com/jejjohnson/finitevolX" _blank
    click SDX "https://github.com/jejjohnson/spectraldiffx" _blank
    click FLTR "https://github.com/jejjohnson/filterax" _blank
    click OB "https://github.com/jejjohnson/optax_bayes" _blank

The interesting one is the PDE column. Under the SPDE view, an elliptic differential operator and a Gaussian precision operator are the same object --- so finitevolX already takes its tridiagonal solves and Nyström preconditioner straight from gaussx, and spectraldiffx is queued to take the capacitance (Woodbury) correction from the very same code that runs a GP marginal likelihood. Fix the CG loop once; every library downstream gets better. See Unified Solvers for how that boundary is drawn, and the Architecture page for the full stack.


Why "GaussX"?

Gauss + JAX. And no, that is not merely branding.

Carl Friedrich Gauss is arguably the most consequential figure in the history of computational mathematics, and his fingerprints are on nearly every algorithm in this library. Gaussian elimination, ancestor of every matrix factorization. The method of least squares, which he invented in 1801 to find a lost asteroid --- a genuinely outrageous flex. The Gaussian distribution. The Gauss-Markov theorem, which explains why least squares is optimal. The Cholesky decomposition is Gaussian elimination in formal dress. Even the FFT traces back to a trick Gauss wrote down in 1805, a century and a half before Cooley and Tukey published it.

The Gaussian distribution earns its place too. It is the maximum-entropy distribution for a given mean and variance --- the least presumptuous thing you can assume. It is the fixed point of the Central Limit Theorem --- where sums end up regardless of where they started. It is the conjugate prior that makes linear-Gaussian Bayesian inference exact rather than approximate. And it is fully described by exactly two things: a mean vector, and a covariance matrix.

That covariance matrix --- its structure, its factorization, its determinant, its inverse --- is precisely and entirely what gaussx computes. Every primitive in the library (solve, logdet, cholesky, trace, sqrt, inv) exists because somebody, somewhere, needs to do something to a Gaussian covariance. GP regression needs solve and logdet. Kalman filtering needs cholesky. Variational inference needs sqrt. Natural gradients need inv. The structured operators exist because real covariances are never the shapeless dense blobs the textbook drew.

One mathematician, two centuries, five verbs, and a matrix that knows what it is.

Voilà.