Skip to content
Open

V1.0 #752

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
106 commits
Select commit Hold shift + click to select a range
4e8f8d3
test(oracle): pin conjugate MLL, predict, and LOOCV to closed form
thomaspinder Aug 5, 2026
0ff8f2d
test(equivalence): pin cross-derivation agreement; xfail two-owner ji…
thomaspinder Aug 5, 2026
d195be8
test(integration): fail loudly when golden values drift
thomaspinder Aug 5, 2026
5d96ee2
test(integration): re-pin golden values to current-main behaviour
thomaspinder Aug 5, 2026
880c58f
feat(linalg): stabilised_cholesky — single stabilise-and-factor seam
thomaspinder Aug 5, 2026
5814aa3
feat(dataset): static n_total metadata; get_batch stamps full size
thomaspinder Aug 5, 2026
8c7ba10
feat(likelihoods)!: pure conditional families — drop num_datapoints a…
thomaspinder Aug 5, 2026
9b9a43c
feat!: v1.0 conditioning architecture — JointModel, Posterior, condition
thomaspinder Aug 5, 2026
b5c9884
refactor!: rename sweep — tests, benchmarks, examples onto the v1.0 API
thomaspinder Aug 6, 2026
1ce4ca1
style: ruff format after the rename sweep
thomaspinder Aug 6, 2026
f113932
docs: v1.0 migration guide, CONTEXT.md glossary, ADR-0001
thomaspinder Aug 6, 2026
bb44a85
docs: self-contained migration snippets
thomaspinder Aug 6, 2026
78debf3
test(equivalence): pin predict/MLL same-matrix consistency at non-def…
thomaspinder Aug 6, 2026
72a7212
docs: fix Sphinx build for the v1.0 API
thomaspinder Aug 6, 2026
84a133e
feat(natural-gradients): add fit_natgrads and exponential-family mach…
thomaspinder Jul 26, 2026
12fa290
fix(natural-gradients): address review findings on PR#1
thomaspinder Jul 26, 2026
bc74e69
refactor(natgrads): adapt to the v1.0 conditioning API
thomaspinder Aug 6, 2026
d53c808
docs(fit): replace Markdown code fences with RST doctest block in fit…
thomaspinder Aug 6, 2026
6799c39
docs(examples): add dual/t-SVGP tutorial notebook and wire docs nav
thomaspinder Jul 27, 2026
40f8985
docs(examples): address dual notebook review findings
thomaspinder Jul 27, 2026
ada0f1d
refactor(variational)!: remove NaturalVariationalGaussian and Expecta…
thomaspinder Jul 26, 2026
d8cb621
feat(variational): add DualVariationalGaussian and dual_elbo (t-SVGP)
thomaspinder Jul 27, 2026
413f03c
docs(examples): add natural gradients tutorial notebook
thomaspinder Jul 27, 2026
6060a74
fix(examples): avoid markdown-katex crash in the dual notebook
thomaspinder Jul 27, 2026
2410564
fix(variational): address PR#2 review findings
thomaspinder Jul 26, 2026
df14c6e
fix(variational): address PR#3 review findings
thomaspinder Jul 27, 2026
c5a2edd
docs(examples): address natgrads notebook review findings
thomaspinder Jul 27, 2026
3337bec
fix(natural-gradients): address whole-stack review findings
thomaspinder Jul 27, 2026
21c4ec0
refactor(natgrads): adapt vestigial-family removal to the v1.0 API
thomaspinder Aug 6, 2026
0fbe545
refactor(natgrads): adapt DualVariationalGaussian and dual_elbo to th…
thomaspinder Aug 6, 2026
1c0555e
refactor(natgrads): adapt natural-gradients notebook to the v1.0 API
thomaspinder Aug 6, 2026
c6d2bce
refactor(natgrads): adapt dual notebook to the v1.0 API
thomaspinder Aug 6, 2026
75aa6a3
feat(conditioning): add sparse and collapsed conditioning modes
thomaspinder Aug 6, 2026
5368ad1
refactor(variational)!: families condition through the conditioning m…
thomaspinder Aug 6, 2026
9af3f0d
test(variational): rename sweep, jitter relocation, xfail flip, confo…
thomaspinder Aug 6, 2026
8a17448
docs(examples): posterior -> model rename and jitter relocation
thomaspinder Aug 6, 2026
cc8bf6c
docs(records): variational universalisation in CONTEXT, ADR-0001, mig…
thomaspinder Aug 6, 2026
b8acedf
style: ruff-clean the safety-net test files
thomaspinder Aug 6, 2026
9032188
refactor!: complete the conditioning contract — uniform signature, Ka…
thomaspinder Aug 7, 2026
6860548
docs(api): Google-convention docstrings and RST across the natgrads s…
thomaspinder Aug 7, 2026
4690ff4
test: close the verification gaps the review found
thomaspinder Aug 7, 2026
26e4e92
docs(records): state what "universal" means as shipped
thomaspinder Aug 7, 2026
dbfce06
refactor(oilmm)!: bring OILMM onto the conditioning contract
thomaspinder Aug 7, 2026
0549b8e
test(oilmm): migrate callers and pin contract conformance
thomaspinder Aug 7, 2026
a451c1d
docs(records): OILMM is on the contract
thomaspinder Aug 7, 2026
2f61a30
docs(oilmm): migrate the latent-space cell onto the conditioning cont…
thomaspinder Aug 7, 2026
943cb19
ci(security): scan every PR, not only those targeting main
thomaspinder Aug 7, 2026
13cc134
ci(security): scan every PR, not only those targeting main
thomaspinder Aug 7, 2026
f270f9c
ci(security): scan every PR, not only those targeting main
thomaspinder Aug 7, 2026
e5ca1a2
ci(security): scan every PR, not only those targeting main
thomaspinder Aug 7, 2026
8f1c3f9
ci(security): scan every PR, not only those targeting main
thomaspinder Aug 7, 2026
466b474
ci(security): scan every PR, not only those targeting main
thomaspinder Aug 7, 2026
3547786
ci(security): scan every PR, not only those targeting main
thomaspinder Aug 7, 2026
90fca3c
ci(security): scan every PR, not only those targeting main
thomaspinder Aug 7, 2026
04cd4f1
ci(security): scan every PR, not only those targeting main
thomaspinder Aug 7, 2026
4e10dd5
ci(security): scan every PR, not only those targeting main
thomaspinder Aug 7, 2026
b49e11a
ci(security): exclude TruffleHog's Lob detector, which flags pytest n…
thomaspinder Aug 7, 2026
46be309
ci(security): exclude TruffleHog's Lob detector, which flags pytest n…
thomaspinder Aug 7, 2026
58e8845
ci(security): exclude TruffleHog's Lob detector, which flags pytest n…
thomaspinder Aug 7, 2026
23c2067
ci(security): exclude TruffleHog's Lob detector, which flags pytest n…
thomaspinder Aug 7, 2026
9eae480
ci(security): exclude TruffleHog's Lob detector, which flags pytest n…
thomaspinder Aug 7, 2026
35f470d
ci(security): exclude TruffleHog's Lob detector, which flags pytest n…
thomaspinder Aug 7, 2026
8bc89d3
ci(security): exclude TruffleHog's Lob detector, which flags pytest n…
thomaspinder Aug 7, 2026
2e4cb84
ci(security): exclude TruffleHog's Lob detector, which flags pytest n…
thomaspinder Aug 7, 2026
9ec5abb
ci(security): exclude TruffleHog's Lob detector, which flags pytest n…
thomaspinder Aug 7, 2026
2f19b8d
ci(security): exclude TruffleHog's Lob detector, which flags pytest n…
thomaspinder Aug 7, 2026
74a9106
Merge pull request #743 from thomaspinder/v1-01-safety-net
thomaspinder Aug 7, 2026
a10023e
Merge pull request #744 from thomaspinder/v1-02-conditioning
thomaspinder Aug 7, 2026
5952b01
Merge pull request #714 from thomaspinder/natgrads-01-core
thomaspinder Aug 7, 2026
4e48381
Merge pull request #715 from thomaspinder/natgrads-02-remove-vestigial
thomaspinder Aug 7, 2026
ad484a1
Merge pull request #716 from thomaspinder/natgrads-03-dual
thomaspinder Aug 7, 2026
0d4e800
Merge pull request #729 from thomaspinder/natgrads-04-notebook-natgrads
thomaspinder Aug 7, 2026
70f0e16
Merge pull request #730 from thomaspinder/natgrads-05-notebook-dual
thomaspinder Aug 7, 2026
76f2d5e
Merge pull request #745 from thomaspinder/v1-03-variational
thomaspinder Aug 7, 2026
a321ec0
Merge pull request #748 from thomaspinder/v1-04-review-fixes
thomaspinder Aug 7, 2026
41079b4
Merge pull request #749 from thomaspinder/v1-05-oilmm-conditioning
thomaspinder Aug 7, 2026
7e832b6
fix(oilmm)!: default OILMMPosterior covariance to "diagonal"
thomaspinder Aug 7, 2026
9477f88
feat(conditioning): extend pathwise sampling to SparsePosterior (#673)
thomaspinder Aug 7, 2026
984d0b2
feat(state-space): dense joint predictive covariance for the smoothed…
thomaspinder Aug 7, 2026
40380f4
feat(state-space): support TruncatedPeriodic x Matern product SDEs
thomaspinder Aug 7, 2026
87a642c
fix(linalg): register has_unit_diagonal for BlockDiag and Kronecker o…
thomaspinder Aug 7, 2026
f59b2d6
fix(state-space): make rts_smoother a genuine QR square-root smoother
thomaspinder Aug 7, 2026
ee650d9
fix(kernels): make gram() delegate to _gram, closing RFF double-compu…
thomaspinder Aug 7, 2026
03afcb9
fix(kernels): honest field defaults so RBF() type-checks, drop stale …
thomaspinder Aug 7, 2026
7f24998
refactor(kernels): merge _newton_girard and _newton_girard_matrices (…
thomaspinder Aug 7, 2026
dd31c67
ci(bench): wire bench-check into all-tests poe sequence
thomaspinder Aug 7, 2026
4608ff1
fix(state-space): adapt #669's new quasi-periodic test to the v1.0 li…
thomaspinder Aug 7, 2026
348951c
feat(objectives): add with_log_prior for MAP-style regularised fitting
thomaspinder Aug 7, 2026
711dbce
style: ruff-format after porting #515 onto v1.0
thomaspinder Aug 7, 2026
ab5fdbb
feat(likelihoods): add StudentT likelihood for robust regression (#671)
thomaspinder Aug 7, 2026
adff330
Merge branch 'main' into v1.0
thomaspinder Aug 7, 2026
207f7dd
Merge pull request #753 from thomaspinder/fix/709-kl-blockdiag-kronecker
thomaspinder Aug 8, 2026
9bc5936
Merge pull request #754 from thomaspinder/fix/668-rts-sqrt-smoother
thomaspinder Aug 8, 2026
2f9e948
Merge pull request #755 from thomaspinder/fix/682-oilmm-diagonal-default
thomaspinder Aug 8, 2026
c49e3e5
Merge pull request #756 from thomaspinder/fix/679-dead-gram-rff
thomaspinder Aug 8, 2026
95d8c25
Merge pull request #757 from thomaspinder/fix/695-kernel-abstract-final
thomaspinder Aug 8, 2026
76e598c
Merge pull request #758 from thomaspinder/fix/683-merge-newton-girard
thomaspinder Aug 8, 2026
9bfd302
Merge pull request #761 from thomaspinder/fix/671-student-t-likelihood
thomaspinder Aug 8, 2026
42a49df
Merge pull request #764 from thomaspinder/ci/638-bench-check-all-tests
thomaspinder Aug 8, 2026
2b1153a
Merge pull request #762 from thomaspinder/feat/669-quasiperiodic-sde
thomaspinder Aug 8, 2026
18b96b7
Merge pull request #759 from thomaspinder/fix/673-sparse-pathwise-sam…
thomaspinder Aug 8, 2026
683439c
Merge pull request #763 from thomaspinder/fix/515-log-prior-regularis…
thomaspinder Aug 8, 2026
4b3f3d9
Fix bug when unwrapping paramax parameters
Thomas-Christie Aug 8, 2026
6e03833
Merge pull request #767 from Thomas-Christie/unwrap-fix
thomaspinder Aug 9, 2026
1e21d3a
Merge branch 'v1.0' into fix/651-state-space-dense-covariance
thomaspinder Aug 9, 2026
2450b04
Merge pull request #760 from thomaspinder/fix/651-state-space-dense-c…
thomaspinder Aug 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,122 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Dense joint predictive covariance for state-space GPs.**
`StateSpacePrior.predict`, `StateSpaceConjugateModel.predict`, and
`StateSpacePosterior.__call__` (`gpjax.state_space`) now accept
`covariance="dense"`, returning the full joint covariance across test
points rather than marginal variances only
([#651](https://github.com/thomaspinder/GPJax/issues/651)). For the
unconditioned prior this is just the kernel's own dense gram — the
state-space SDE is an exact representation with no training data to
marginalise out. For the conditioned (smoothed) posterior it is built from
the RTS smoother's cross-covariance recursion (Särkkä & Solin 2019 §12.2):
`rts_smoother` gains an opt-in `return_gains=True` that exposes its
already-computed per-step smoother gains, and a new
`gpjax.state_space.prediction._dense_smoothed_test_covariance` chains them
into the `M x M` test-point covariance. This keeps the state-space
formulation's linear-in-`N` cost — no `N x N` gram over the training set is
ever formed — with the cross-covariance work landing at `O(M^2 d^3)`,
independent of `N` and no larger than the `O(M^2)` already required to
store the dense output. `StateSpacePosterior.filtered` /
`StateSpaceConjugateModel.predict_filter` (the *causal* predictive) keep
raising `NotImplementedError` for `covariance="dense"`: each of their test
points conditions on a different information set (observations up to its
own timestamp), so a "joint" filtered covariance is not the dense
conjugate-predictive-shaped object the smoothed path now matches, and
extending it is left to a future issue if there is demand for it.

- **`gpjax.fit_natgrads` and the `gpjax.natural_gradients` module.** Trains a variational
family by alternating one natural-gradient step on the variational distribution with
one step of a supplied Optax optimiser on everything else — kernel and likelihood
hyperparameters, the mean function, and the inducing inputs — following Salimbeni,
Eleftheriadis and Hensman (2018), [arXiv:1803.09151](https://arxiv.org/abs/1803.09151).
`VariationalGaussian` and `WhitenedVariationalGaussian` are supported. For a conjugate
model on the full batch, `natgrad_lr=1.0` reaches the optimal `q` in one iteration.

- **`DualVariationalGaussian` and `gpjax.objectives.dual_elbo`.** The dual (t-SVGP)
parameterisation of Adam, Chang, Khan and Solin (2021),
[arXiv:2111.03412](https://arxiv.org/abs/2111.03412). Instead of the moments of
`q(u)`, the family stores an unnormalised Gaussian *site* on the centred inducing
outputs — `dual_vector` is the site's first natural parameter and `dual_matrix` its
precision — from which `q(u)` is recovered through the working matrix
`R = Kzz + Kzz Lambda_2 Kzz`. Because the stored coordinates are an affine image of
the natural parameters, a natural-gradient step is a convex combination of the
current sites with a closed-form target, so no expectation-to-natural round trip is
needed and the KL is never differentiated. `fit_natgrads` dispatches on the family
and takes that step; the step size means the same thing in both branches, and from
the same starting `q` the dual and Salimbeni E-steps produce identical iterates —
provided the dual branch's computed per-point curvature stays non-negative, so that
its `beta_floor` never engages. That holds for a genuinely log-concave likelihood;
GPJax's `inv_probit` clips its probabilities away from 0 and 1, which breaks
log-concavity in the far tails, and there the two branches diverge. The
dual branch restricts the step size to the interval from zero to one, since the
update is a convex combination.
`dual_elbo` has the same *value* as `elbo` at the implied moments, for any sites and
any hyperparameters, but a different *hyperparameter gradient from `elbo` evaluated on
the matched `VariationalGaussian`*: the prior part of `q` tracks the kernel while the
data-dependent sites stay frozen. The difference is between the two
parameterisations, not between the two functions — calling `elbo` directly on a
`DualVariationalGaussian` returns the same value and the same gradients as
`dual_elbo`, which is simply the batched-marginals fast path for that family. That
frozen-site gradient is what gives the M-step its reported behaviour, so `Kzz` must
not be detached and the implied moments must not be cached on the family.
`DualVariationalGaussian` also works with plain `gpjax.fit`, where it is ordinary
gradient descent in the dual coordinates. The `VariationalParametrisationSuite` ASV
benchmark gains a `dual` axis value.

### Changed (breaking)

- **`OILMMPosterior.__call__`/`.predict` default to `covariance="diagonal"`**
(was `"dense"`) ([#682](https://github.com/JaxGaussianProcesses/GPJax/issues/682)).
The dense joint covariance costs `O(m n^2 p^2)` — an `np x np` matrix built
from `m` dense `n x n` latent covariances — which forfeits the
`O(mn^3 + nmp)` scaling that OILMM exists to provide, and is unaffordable
well before the mean/marginal-variance query is. Marginal variances are the
common case and are unaffected by the mixing matrix's off-diagonal
structure, so the cheap path is now the default; pass
`covariance="dense"` explicitly for the joint covariance.

### Removed

- **`NaturalVariationalGaussian` and `ExpectationVariationalGaussian`.** These were
parameterisation-only classes with no optimiser attached: they stored the natural or
expectation coordinates of `q(u)` but offered no way to take a natural-gradient step
in them. Natural-gradient geometry belongs to the optimiser — the Fisher matrix *is*
the Jacobian dη/dθ, so a natural-gradient step in the natural parameters θ is exactly
an ordinary gradient step in the expectation parameters η, and either coordinate system
can be recovered on the fly from whatever the family happens to store. `fit_natgrads`
therefore operates directly on `VariationalGaussian` and `WhitenedVariationalGaussian`,
which store constraint-respecting coordinates. Users of the removed classes should
switch to `VariationalGaussian` with `gpjax.fit_natgrads`.
The `VariationalParametrisationSuite` ASV benchmark loses its `natural` and
`expectation` axis values; previously recorded results for those two arms are orphaned.

### Fixed

- **`gpx.kernels.RBF()` was a type error, and `White()` carried a phantom
trainable lengthscale**
([#695](https://github.com/JaxGaussianProcesses/GPJax/issues/695)). Pyright
synthesises `__init__` signatures from dataclass fields for any kernel that
inherits its `__init__` (e.g. `RBF`, `Matern12/32/52`), and those fields had
no defaults, so the canonical `RBF()` call was flagged as missing arguments
while the nonsensical `RBF(name="xyz")` type-checked cleanly (raising
`TypeError` at runtime). Kernel fields now carry real defaults matching
their `__init__` defaults, and `name` is a `ClassVar` rather than a
dataclass field, so the synthesised and hand-written signatures agree.
Separately, `White` hardcoded `lengthscale=1.0` into
`StationaryKernel.__init__` even though `White.__call__` never reads it,
so every `White` kernel carried a real, trainable `PositiveReal` leaf with
zero gradient that showed up in optimiser state and MCMC traces; `White`
now has its own minimal `__init__` and no longer carries a lengthscale at
all (`White().lengthscale is None`, and it is absent from
`jax.tree_util.tree_flatten`). The stale `_compute_base_init` workaround in
`kernels/base.py`, whose docstring claimed "equinox modules are frozen
after `super().__init__()`" -- no longer true under the pinned Equinox
version -- was removed in favour of a plain `super().__init__(...)` call.

- **`Zero` mean function is trainable and drifts away from zero.** Fitting a
model with the default `Zero()` mean function moved its constant towards the
data mean (0.0 → 5.09 on a dataset with mean 5), silently changing the
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,18 @@ Functions `(model, Dataset) -> scalar`:
- `conjugate_mll` / `conjugate_loocv` -- for `ConjugatePosterior`
- `log_posterior_density` (alias `non_conjugate_mll`) -- for `NonConjugatePosterior`
- `elbo` / `collapsed_elbo` -- for variational families
- `dual_elbo` -- for `DualVariationalGaussian` (t-SVGP; same value as `elbo`, different hyperparameter gradient)
- `heteroscedastic_elbo` -- for heteroscedastic models

Optimise by negating: `nmll = lambda p, d: -conjugate_mll(p, d)`

### Fitting (`gpjax/fit.py`)

Three optimisers: `fit()` (Optax gradient descent with scan), `fit_scipy()` (SciPy L-BFGS-B), `fit_lbfgs()` (Optax L-BFGS with `while_loop`). All handle the constrained/unconstrained bijection automatically: `paramax.unwrap(model)` is called inside the loss function, and `eqx.partition`/`eqx.combine` with `eqx.is_array` manage trainable vs static parts.
Four optimisers: `fit()` (Optax gradient descent with scan), `fit_scipy()` (SciPy L-BFGS-B), `fit_lbfgs()` (Optax L-BFGS with `while_loop`), `fit_natgrads()` (natural-gradient steps on a variational family, alternated with Optax steps on the hyperparameters). All handle the constrained/unconstrained bijection automatically: `paramax.unwrap(model)` is called inside the loss function, and `eqx.partition`/`eqx.combine` with `eqx.is_array` manage trainable vs static parts.

### Variational inference (`gpjax/variational_families.py`)

`VariationalGaussian`, `WhitenedVariationalGaussian`, `NaturalVariationalGaussian`, `ExpectationVariationalGaussian`, `CollapsedVariationalGaussian`, `GraphVariationalGaussian`, `HeteroscedasticVariationalFamily`. All inherit from `AbstractVariationalFamily` and implement `predict()` + `prior_kl()`.
`VariationalGaussian`, `WhitenedVariationalGaussian`, `DualVariationalGaussian`, `CollapsedVariationalGaussian`, `GraphVariationalGaussian`, `HeteroscedasticVariationalFamily`. All inherit from `AbstractVariationalFamily` and implement `predict()` + `prior_kl()`.

### NumPyro integration (`gpjax/numpyro_extras.py`)

Expand Down
89 changes: 89 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# GPJax domain glossary

The ubiquitous language of this codebase. Code, docs, tests, and reviews use
these terms exactly; when a concept is missing, add it here in the same PR
that introduces it.

## Design principle

**Maths-first, comprehensible to the maths-familiar non-expert.** API names
follow the textbook equation unless the textbook word is jargon a
practitioner would not own. When the two conflict, choose the word a
scikit-learn/PyMC user already speaks and state the maths once in the
docstring (e.g. the class is `JointModel`, and its docstring says "the joint
distribution p(f, y)").

## Terms

**Prior** — the Gaussian process prior p(f), pairing a kernel with a mean
function. Queryable at any inputs: `prior(x)` returns the prior predictive.
Owns the model's single numerical-stabilisation knob, `jitter`.

**Likelihood** — the conditional distribution p(y | f). A *pure* conditional:
it holds no priors, no dataset facts, and no data sizes. (Both were tried and
removed at v1.0 — see ADR-0001.)

**JointModel** — the joint distribution p(f, y) = p(y | f) · p(f), created by
`prior * likelihood`. The *trainable* object: `gpx.fit` optimises its
hyperparameters. It carries state that must exist before conditioning
(hyperparameters; the non-conjugate latent; the heteroscedastic noise-process
model) and no derived quantities. Concrete kinds: `ConjugateModel`,
`NonConjugateModel`, `HeteroscedasticModel` — each holds state the others
cannot.

**condition** — the operation p(f, y) + 𝒟 → p(f | 𝒟), spelled
`model.condition(D)` or the operator form `model | D` (read: "f given D").

The signature is `condition(train_data)` uniformly, on every conditionable
object, with `train_data` required. It is universal across the exact, latent,
sparse, collapsed and state-space modes. Where the maths does not consume the
data — the uncollapsed variational families, which already carry the fitted
q(u) — the argument is still accepted, for interface uniformity, and the
docstring says so plainly. A signature that varied by object would be a
worse API than one argument occasionally ignored.

The one documented exclusion is the heteroscedastic path
(`HeteroscedasticModel` and `HeteroscedasticVariationalFamily`), which has no
closed-form conditioned process: it carries two latent processes, signal and
noise, so there is no single p(f | 𝒟) to return. Both raise
`NotImplementedError` naming the alternative — inference runs through
`HeteroscedasticVariationalFamily` and the `heteroscedastic_elbo` objective,
and prediction through `predict` / `predict_latents`, or by conditioning the
`signal_variational` and `noise_variational` components individually.

`prior_kl` is deliberately *not* part of this contract: it keeps a per-family
signature, because only the collapsed family's KL is a function of the data.

**Posterior** — the conditioned process p(f | 𝒟) returned by `condition`. An
*immutable* pytree: the training-covariance factorisation is computed once and
cached; every query is a view of it. The uniform query surface:
`posterior(xtest, covariance="dense"|"diagonal")`, plus per-mode views —
`log_marginal_likelihood` / `loo` / `sample_approx` on the exact mode,
`log_posterior_density` on the latent mode. Users never name the concrete
implementations behind the interface.

**variational family** — a trainable approximate posterior over inducing
values: to sparse GPs what JointModel is to exact ones. It carries the joint
model in its `model` field, and `.condition(D)` yields a Posterior like any
other; `elbo`-style objectives are its training criteria. The model's
`Prior.jitter` is the only stabilisation knob — families carry none of their
own.

**evidence / log marginal likelihood** — p(𝒟), the normalising constant of
conditioning, exposed as `posterior.log_marginal_likelihood`. "Evidence" and
"marginal likelihood" are the same quantity; the attribute uses the
GP-community's term.

**sugar** — a documented one-line composition kept for ergonomics, never a
second implementation. `model.predict(x, D)` and `model(x, D)` are sugar for
`model.condition(D)(x)`; `model | D` is sugar for `model.condition(D)`.

**objective** — a scalar function `(model, Dataset) -> ScalarFloat` consumed
by `gpx.fit`. Objectives are thin: `conjugate_mll` is the evidence view of
the conditioned posterior, not a second derivation.

**Dataset** — the data container. `n_total` records the full-dataset size
when the object is a minibatch view (stamped by `get_batch`); the minibatch
ELBO scale is derived from it, never supplied by hand. Read it through
`full_size`, which falls back to `n` for a whole dataset — production code
uses `data.full_size / data.n` and never re-spells the fallback inline.
6 changes: 3 additions & 3 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,6 @@ Prints a regression report with significance markers. No branches touched.
## Notes on bench-check

`bench-check` runs `asv check`, which builds an env for the commit
referenced by `asv.conf.json`'s `branches: ["main"]`. It only succeeds
once `benchmarks/` and `asv-constraints.txt` have landed on `main`. After
that, it's safe to add to the `all-tests` poe sequence.
referenced by `asv.conf.json`'s `branches: ["main"]`. It is wired into
the `all-tests` poe sequence, so it runs on every `uv run poe all-tests`
alongside `lint`, `docstrings` and `test`.
4 changes: 2 additions & 2 deletions benchmarks/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ def setup(self):
kernel = gpx.kernels.RBF()
mean = gpx.mean_functions.Zero()
prior = gpx.gps.Prior(kernel=kernel, mean_function=mean)
likelihood = gpx.likelihoods.Gaussian(num_datapoints=n)
likelihood = gpx.likelihoods.Gaussian()
self.posterior = prior * likelihood
self.q = VariationalGaussian(
posterior=self.posterior, inducing_inputs=X[:M_INDUCING]
model=self.posterior, inducing_inputs=X[:M_INDUCING]
)

self.jitted_mll = jax.jit(objectives.conjugate_mll)
Expand Down
Loading
Loading