Posterior Covariance¶
A variational analysis returns a point estimate; this layer attaches the
uncertainty around it. The design is deliberately lazy and matrix-free:
a posterior adapter never materialises the covariance matrix. Instead it
builds a lineax operator for the Hessian (or precision) at the analysis
point, and inverses, diagonals (pointwise variances), and ensemble
estimates are delegated to
gaussx — gaussx.inv,
gaussx.diag, and gaussx.ensemble_covariance — so structured and
iterative solves come for free. See
Posterior Covariance in the Mathematical
Reference for derivations and when each approximation is trustworthy.
Adapters satisfy the PosteriorAdapter Protocol and all
produce the same Posterior container, so downstream diagnostics (e.g. the
posterior-agreement gate) are adapter-agnostic. LaplaceCovariance and
GaussNewtonHessian both build the inverse Gauss–Newton Hessian at the
mode, matrix-free; for a 4DVar model it runs through the model's
tangent-linear and adjoint rollouts, so pass the model and the window's
batch. GaussNewtonHessian exposes the Krylov budget. Choose
EnsembleCovariance when samples are cheaper than curvature or the
posterior is not Gaussian.
Container¶
vardax — Modular variational data assimilation with learned components.
All public symbols are re-exported from the private _src subpackage so
that user code imports from the top-level namespace:
Posterior
¶
Bases: Module
Posterior container — Decision D10.
A plain container for \(p(x \mid y) \approx \mathcal{N}(\text{mean}, \text{cov})\), plus optional samples and provenance.
Attributes:
| Name | Type | Description |
|---|---|---|
mean |
Float[Array, ...]
|
Posterior mean / MAP estimate. |
cov |
AbstractLinearOperator | None
|
Posterior covariance as |
samples |
Float[Array, ...] | None
|
Optional posterior samples (from a flow head, ensemble,
or score-based diffusion). |
provenance |
dict[str, Any]
|
Free-form dict carrying audit info — at minimum
|
Examples:
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> post = vdx.Posterior(mean=jnp.zeros(4))
>>> post.mean.shape
(4,)
>>> post.cov is None
True
>>> post.provenance
{}
A Gaussian posterior carries a lazy covariance operator and audit metadata:
>>> import jax, lineax as lx
>>> eye = lx.IdentityLinearOperator(jax.ShapeDtypeStruct((2,), jnp.float32))
>>> gauss = vdx.Posterior(
... mean=jnp.zeros(2), cov=eye, provenance={"adapter": "demo"}
... )
>>> gauss.cov.mv(jnp.ones(2)).shape, gauss.provenance["adapter"]
((2,), 'demo')
Source code in src/vardax/_src/posterior/container.py
Adapters¶
vardax — Modular variational data assimilation with learned components.
All public symbols are re-exported from the private _src subpackage so
that user code imports from the top-level namespace:
LaplaceCovariance
¶
Bases: Module
Laplace approximation at MAP.
Algorithm
- Linearise the observation operator (and, for 4DVar, the model rollout) at the analysis.
- Assemble the Gauss-Newton precision \(B^{-1} + \sum_t G_t^\top R_t^{-1} G_t\) matrix-free, with \(G_t = H'_t M'_{t \leftarrow 0}\).
- Return its inverse as a lazy operator applied by
solver.
Attributes:
| Name | Type | Description |
|---|---|---|
prior_cov_op |
AbstractLinearOperator | None
|
Background-error covariance \(B\). Default |
obs_cov_op |
AbstractLinearOperator | None
|
Observation-error covariance \(R\) at one time. Default
|
solver |
AbstractLinearSolver
|
Linear solver for the lazy inverses (default CG,
|
Pass \(B\) and \(R\) explicitly only to evaluate the posterior under different error statistics from the ones the analysis used.
Examples:
>>> import jax, jax.numpy as jnp, lineax as lx
>>> import vardax as vdx
>>> from types import SimpleNamespace
>>> eye = lx.IdentityLinearOperator(jax.ShapeDtypeStruct((4,), jnp.float32))
>>> laplace = vdx.LaplaceCovariance(prior_cov_op=eye, obs_cov_op=eye)
>>> model = SimpleNamespace(obs_op=vdx.LinearObs(operator=eye))
>>> post = laplace(jnp.zeros(4), model, batch=None)
>>> post.mean.shape
(4,)
>>> # B = R = H = I, so P* = (I + I)^{-1} = I / 2
>>> bool(jnp.allclose(post.cov.mv(jnp.ones(4)), 0.5, atol=1e-2))
True
Weaker observations (R = 3 I) leave a larger posterior, \(P^* = (1 + 1/3)^{-1} = 3/4\):
>>> weak = jnp.float32(3.0) * eye
>>> post = vdx.LaplaceCovariance(prior_cov_op=eye, obs_cov_op=weak)(
... jnp.zeros(4), model, batch=None
... )
>>> bool(jnp.allclose(post.cov.mv(jnp.ones(4)), 0.75, atol=1e-2))
True
A model with no obs_op is rejected:
>>> laplace(
... jnp.zeros(4), SimpleNamespace(), batch=None
... )
Traceback (most recent call last):
...
AttributeError: ...
Source code in src/vardax/_src/posterior/laplace.py
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
GaussNewtonHessian
¶
Bases: Module
Gauss-Newton Hessian inversion at MAP.
Computes \((B^{-1} + \sum_t (H'_t M'_t)^\top R_t^{-1} (H'_t M'_t))^{-1}\)
for 4DVar models and \((B^{-1} + H'^\top R^{-1} H')^{-1}\) for
single-time ones, like
LaplaceCovariance, with CG capped at
n_krylov iterations per covariance mat-vec.
Algorithm
- Linearise the observation operator (and, for 4DVar, the model rollout) at the analysis.
- Assemble the precision \(B^{-1} + \sum_t G_t^\top R_t^{-1} G_t\) matrix-free, with \(G_t = H'_t M'_{t \leftarrow 0}\).
- Return its inverse as a lazy operator applied by CG with at
most
n_krylovsteps.
Attributes:
| Name | Type | Description |
|---|---|---|
prior_cov_op |
AbstractLinearOperator | None
|
\(B\). Default |
obs_cov_op |
AbstractLinearOperator | None
|
\(R\) at one time. Default |
n_krylov |
int
|
Maximum CG iterations per covariance mat-vec. |
tol |
float
|
CG absolute and relative tolerance. |
Examples:
Linear dynamics \(x_{t+1} = a x_t\), a fully observed two-state window, \(B = R = I\): the observations at \(t = 0, 1\) both inform \(x_0\), so \(P^* = 1 / (1 + 1 + a^2)\).
>>> import jax, jax.numpy as jnp, lineax as lx, vardax
>>> class Decay:
... dt = 1.0
...
... def step(self, x, dt):
... return 0.5 * x
>>> eye = lx.IdentityLinearOperator(jax.ShapeDtypeStruct((2,), jnp.float32))
>>> model = vardax.StrongFourDVar(
... forward=Decay(),
... obs_op=vardax.MaskedIdentity(),
... prior_mean=jnp.zeros(2),
... prior_cov_op=eye,
... obs_cov_op=eye,
... )
>>> batch = vardax.Batch1D(input=jnp.ones((1, 2, 2)), mask=jnp.ones((1, 2, 2)))
>>> adapter = vardax.GaussNewtonHessian(prior_cov_op=eye, obs_cov_op=eye)
>>> post = adapter(model(batch)[0], model, batch)
>>> bool(jnp.allclose(post.cov.mv(jnp.ones(2)), 1 / 2.25, atol=1e-5))
True
The Krylov budget is recorded in the provenance:
A point batch is not supported yet:
>>> pts = vardax.PointObsBatch(
... coords=jnp.zeros((1, 1)),
... values=jnp.zeros(1),
... sigma=jnp.ones(1),
... valid=jnp.ones(1, bool),
... )
>>> adapter(model(batch)[0], model, pts)
Traceback (most recent call last):
...
UnsupportedInputError: The posterior adapters do not support ...
Source code in src/vardax/_src/posterior/gauss_newton.py
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | |
EnsembleCovariance
¶
Bases: Module
Sample-covariance posterior from an ensemble of analyses.
Algorithm
- Average the analyses over the ensemble axis to get \(\bar x\).
- Flatten each member and form the Bessel-corrected sample covariance as a low-rank operator (never dense).
- Scale it by the inflation factor \(\lambda\).
- Package mean, covariance, the members as samples and the
provenance into a
Posterior.
Attributes:
| Name | Type | Description |
|---|---|---|
n_members |
int
|
Expected ensemble size (used for diagnostics). |
inflation |
float
|
Multiplicative inflation factor \(\lambda\) applied to the sample covariance. Default 1.0 (no inflation). |
Examples:
Three scalar-pair analyses: the mean is the member average.
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> analyses = jnp.array([[0.0, 1.0], [2.0, 3.0], [4.0, 8.0]])
>>> post = vdx.EnsembleCovariance(n_members=3)(analyses, None, None)
>>> bool(jnp.allclose(post.mean, jnp.array([2.0, 4.0])))
True
>>> post.samples.shape
(3, 2)
Inflation scales the covariance (here by 4):
>>> big = vdx.EnsembleCovariance(n_members=3, inflation=4.0)(
... analyses, None, None
... )
>>> v = jnp.array([1.0, 1.0])
>>> bool(jnp.allclose(big.cov.mv(v), 4.0 * post.cov.mv(v)))
True
Members of any shape are flattened, so a (M, 2, 2) stack gives a
4 x 4 covariance:
>>> grid = jnp.arange(12.0).reshape(3, 2, 2) ** 2
>>> vdx.EnsembleCovariance(n_members=3)(grid, None, None).cov.mv(
... jnp.ones(4)
... ).shape
(4,)
Source code in src/vardax/_src/posterior/ensemble.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
Likelihoods¶
vardax — Modular variational data assimilation with learned components.
All public symbols are re-exported from the private _src subpackage so
that user code imports from the top-level namespace:
GaussianMarkLikelihood
¶
Bases: Module
Serialise a Gaussian posterior into a mark-likelihood dict.
A JSON-friendly summary of a Posterior: its mean,
the diagonal of its covariance, any samples, the provenance and the
event metadata.
Attributes:
| Name | Type | Description |
|---|---|---|
posterior |
Posterior
|
A vardax |
event_metadata |
dict[str, Any]
|
Free-form dict (event_id, time, geometry, instruments_used, …). Passed through verbatim into the output. |
Use .to_dict() to get a JSON-friendly representation that
downstream consumers (GeoCatalog, hierarchical Bayesian models,
DuckDB ingest) can store as-is.
Examples:
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> post = vdx.Posterior(mean=jnp.zeros(3))
>>> mark = vdx.GaussianMarkLikelihood(
... posterior=post, event_metadata={"event_id": "e1"}
... )
>>> sorted(mark.to_dict())
['cov_diag', 'event_metadata', 'mean', 'provenance', 'samples']
Without a covariance the diagonal is None:
Composed with an ensemble posterior, the diagonal and samples are serialised as nested lists:
>>> analyses = jnp.array([[0.0, 1.0], [2.0, 3.0], [4.0, 8.0]])
>>> ens = vdx.EnsembleCovariance(n_members=3)(analyses, None, None)
>>> out = vdx.GaussianMarkLikelihood(posterior=ens).to_dict()
>>> len(out["cov_diag"]), len(out["samples"])
(2, 3)
Source code in src/vardax/_src/posterior/adapter.py
to_dict
¶
Return a JSON-friendly mark-likelihood representation.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
A dict with keys |
dict[str, Any]
|
|
dict[str, Any]
|
lists and missing parts become |