Amortized Inference¶
Where the variational methods on the Models page solve an optimisation problem per analysis, an amortized posterior pays the cost once at training time: a network is fit to map observations directly to an approximate posterior, so inference is a single forward pass. See Amortized Inference in the Mathematical Reference for the underlying theory and the fidelity/speed trade-offs.
AmortizedPosterior composes two exchangeable parts: an observation
encoder that summarises (possibly masked, possibly irregular) observations
into a conditioning vector, and a posterior head that turns that vector
into a distribution over states. Heads span the fidelity ladder — point
estimates (RegressionHead), full densities via conditional normalizing
flows (ConditionalFlowHead), and score-based diffusion sampling
(ScoreDiffusionHead). Amortized posteriors should pass the same
validation gates (simulation-based calibration, posterior
agreement) as their variational counterparts.
Posterior and configuration¶
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:
AmortizedPosterior
¶
Bases: Module
Amortized variational posterior \(q_\phi(x \mid y)\).
Composes an encoder \(c = E(y, m)\) with a density head \(q_\phi(x \mid c)\); a trained model answers any new observation with one forward pass.
Algorithm
- Encode each batch element's
(input, mask)into a context vector (vmapped over the batch). - Hand the context to the head:
__call__takes its mode,sampledraws from it,log_probevaluates the density.
Attributes:
| Name | Type | Description |
|---|---|---|
encoder |
Any
|
|
head |
Any
|
|
config |
AmortizedConfig
|
|
Examples:
>>> import jax
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> key = jax.random.PRNGKey(0)
>>> posterior = vdx.AmortizedPosterior(
... encoder=vdx.MLPObsEncoder(input_size=12, context_dim=6, key=key),
... head=vdx.RegressionHead(context_dim=6, state_shape=(3, 4), key=key),
... config=vdx.AmortizedConfig(n_samples=7),
... )
>>> ones = jnp.ones((2, 3, 4))
>>> batch = vdx.Batch1D(input=ones, mask=ones)
>>> posterior(batch).shape
(2, 3, 4)
Samples default to config.n_samples and can be overridden:
>>> posterior.sample(batch, jax.random.PRNGKey(1)).shape
(2, 7, 3, 4)
>>> posterior.sample(batch, jax.random.PRNGKey(1), n=3).shape
(2, 3, 3, 4)
One log-density comes back per batch element, here at the mode:
Source code in src/vardax/_src/amortized/posterior.py
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 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
sample
¶
Draw posterior samples per batch element.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
batch
|
Batch1D | Batch2D
|
Observations |
required |
key
|
PRNGKeyArray
|
PRNG key, split into one key per batch element. |
required |
n
|
int | None
|
Number of samples per element. Defaults to |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, ...]
|
Samples stacked as |
Source code in src/vardax/_src/amortized/posterior.py
log_prob
¶
Per-sample log-density of x under q_φ(·|y).
For ScoreDiffusionHead this raises NotImplementedError
(no closed-form density). Use sample instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Float[Array, ...]
|
States whose density is evaluated, one per batch element. |
required |
batch
|
Batch1D | Batch2D
|
Observations |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' B']
|
The log-density \(\log q_\phi(x \mid y)\) for each batch element. |
Source code in src/vardax/_src/amortized/posterior.py
as_analysis_step
¶
Adapt to pipekit_cycle.AnalysisStep (Decision D8).
Returns:
| Type | Description |
|---|---|
_AmortizedAnalysisStep
|
A callable |
_AmortizedAnalysisStep
|
masks the non-finite entries of |
Examples:
Non-finite observations are treated as unobserved:
>>> import jax
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> key = jax.random.PRNGKey(0)
>>> posterior = vdx.AmortizedPosterior(
... encoder=vdx.MLPObsEncoder(input_size=12, context_dim=6, key=key),
... head=vdx.RegressionHead(context_dim=6, state_shape=(3, 4), key=key),
... config=vdx.AmortizedConfig(),
... )
>>> obs = jnp.ones((1, 3, 4)).at[0, 0, 0].set(jnp.nan)
>>> step = posterior.as_analysis_step()
>>> step(obs, obs, obs_op=None, obs_err_cov=None).shape
(1, 3, 4)
Source code in src/vardax/_src/amortized/posterior.py
AmortizedConfig
¶
Bases: Module
Configuration for an AmortizedPosterior.
Attributes:
| Name | Type | Description |
|---|---|---|
head_type |
str
|
One of |
n_samples |
int
|
Default number of posterior samples to draw in
|
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If |
Examples:
The defaults give a regression head and 64 samples:
>>> import vardax as vdx
>>> config = vdx.AmortizedConfig()
>>> config.head_type, config.n_samples
('regression', 64)
Both fields can be set:
An unknown head type is rejected:
>>> vdx.AmortizedConfig(head_type="bogus")
Traceback (most recent call last):
...
ConfigurationError: AmortizedConfig.head_type must be one of ...
Source code in src/vardax/_src/amortized/config.py
Observation encoders¶
IdentityObsEncoder passes observations straight through — appropriate
when they are already a fixed-size vector; MLPObsEncoder learns the
summary.
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:
IdentityObsEncoder
¶
Bases: Module
Concatenate input and mask into a flat context vector.
Zero parameters, the encoder is purely structural:
\(c = [\mathrm{vec}(y); \mathrm{vec}(m)]\). The output dimension is
input.size + mask.size.
Examples:
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> enc = vdx.IdentityObsEncoder()
>>> enc(jnp.ones((3, 4)), jnp.zeros((3, 4))).shape
(24,)
The first half is the data and the second half the mask:
>>> ctx = enc(jnp.ones((2, 2)), jnp.zeros((2, 2)))
>>> ctx.tolist()
[1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0]
It feeds a RegressionHead whose context size
is twice the field size:
>>> import jax
>>> head = vdx.RegressionHead(
... context_dim=24, state_shape=(3, 4), key=jax.random.PRNGKey(0)
... )
>>> head.map_estimate(enc(jnp.ones((3, 4)), jnp.ones((3, 4)))).shape
(3, 4)
Source code in src/vardax/_src/amortized/encoder.py
MLPObsEncoder
¶
Bases: Module
Two-layer MLP encoder from flat (input, mask) to context.
Computes \(c = \mathrm{MLP}([\mathrm{vec}(y); \mathrm{vec}(m)])\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_size
|
int
|
Flattened size of the input field ( |
required |
context_dim
|
int
|
Dimension of the context vector \(c\). |
required |
hidden_dim
|
int
|
Width of the MLP hidden layers. |
64
|
depth
|
int
|
Number of hidden layers of the MLP. |
2
|
key
|
PRNGKeyArray
|
PRNG key for the MLP weights. |
required |
Attributes:
| Name | Type | Description |
|---|---|---|
mlp |
MLP
|
|
Examples:
>>> import jax
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> enc = vdx.MLPObsEncoder(
... input_size=12, context_dim=6, key=jax.random.PRNGKey(0)
... )
>>> enc(jnp.ones((3, 4)), jnp.ones((3, 4))).shape
(6,)
Any field shape with the right size is flattened first:
It is the encoder of an AmortizedPosterior,
where it is vmapped over the batch:
>>> head = vdx.RegressionHead(
... context_dim=6, state_shape=(3, 4), key=jax.random.PRNGKey(1)
... )
>>> posterior = vdx.AmortizedPosterior(enc, head, vdx.AmortizedConfig())
>>> ones = jnp.ones((2, 3, 4))
>>> posterior(vdx.Batch1D(input=ones, mask=ones)).shape
(2, 3, 4)
Source code in src/vardax/_src/amortized/encoder.py
Posterior heads¶
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:
RegressionHead
¶
Bases: Module
Gaussian regression head: q_φ(x|y) = N(μ_φ(y), diag(σ²_φ(y))).
Two MLPs share the context: one for the mean, one for the log
variance. Outputs are reshaped to the user-specified
state_shape.
Algorithm
- Map the context to the flat mean \(\mu\) and log variance
\(\log \sigma^2\) with the two MLPs, and reshape them to
state_shape. map_estimatereturns \(\mu\).samplereturns \(\mu + \sigma \odot \varepsilon\) with \(\varepsilon \sim \mathcal{N}(0, I)\).log_probreturns the diagonal-Gaussian log-density summed over all state entries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
context_dim
|
int
|
Dimension of the context vector from the encoder. |
required |
state_shape
|
tuple[int, ...]
|
Shape of a single posterior sample (e.g.
|
required |
hidden_dim
|
int
|
Width of the MLP hidden layers. |
64
|
depth
|
int
|
Number of hidden layers of each MLP. |
2
|
key
|
PRNGKeyArray
|
PRNG key for the MLP weights. |
required |
Attributes:
| Name | Type | Description |
|---|---|---|
mlp_mu |
MLP
|
MLP from context to flat mean (size |
mlp_log_var |
MLP
|
MLP from context to flat log variance. |
Examples:
>>> import jax
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> head = vdx.RegressionHead(
... context_dim=6, state_shape=(3, 4), key=jax.random.PRNGKey(0)
... )
>>> ctx = jnp.ones(6)
>>> head.map_estimate(ctx).shape
(3, 4)
>>> head.sample(ctx, jax.random.PRNGKey(1), 5).shape
(5, 3, 4)
The density is highest at the mean:
>>> mode = head.map_estimate(ctx)
>>> bool(head.log_prob(mode, ctx) > head.log_prob(mode + 3.0, ctx))
True
The head scalar log-density has no batch axis:
Source code in src/vardax/_src/amortized/heads.py
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 148 149 150 151 | |
ConditionalFlowHead
¶
Bases: Module
Conditional normalising flow head (stub).
Implements \(x = f_\phi(z; c_\psi(y))\) with \(z \sim \mathcal{N}(0, I)\). Exact
density via change-of-variables. Requires gauss_flows for the
flow primitives; ships as a stub until that dependency is added to
pyproject.toml.
Examples:
Constructing the stub raises until the dependency lands:
>>> import vardax as vdx
>>> vdx.ConditionalFlowHead()
Traceback (most recent call last):
...
NotImplementedError: ConditionalFlowHead requires `gauss_flows` ...
Source code in src/vardax/_src/amortized/heads.py
ScoreDiffusionHead
¶
Bases: Module
Score-based diffusion head (stub).
Learns \(s_\phi(x, t \mid y) \approx \nabla_x \log p_t(x \mid y)\); samples via reverse SDE solved with diffrax. Ships as a stub pending the reverse-SDE pipeline.
Examples:
Constructing the stub raises until the pipeline lands:
>>> import vardax as vdx
>>> vdx.ScoreDiffusionHead()
Traceback (most recent call last):
...
NotImplementedError: ScoreDiffusionHead is not yet implemented. ...