GeoStack · box 2, 4 and 5 of the six-step cycle

Learned data assimilation: what can be learned in the analysis

The taxonomy page took the model apart. This page takes the assimilation apart: the objective, its gradient, the solver, the output, and whether the answer is computed per window or learned across windows. Eleven ways to learn one of those pieces, on the same Gulf Stream QG example and the same 10-day window of SSH maps.

TL;DR

One analysis operator, eleven insides

Sidebar: the sequential (EnKF) route, A10. Teal chips fail loudly, amber chips silently.

Notation

As on the other two pages. On this page \(\gvect{\nu}\) is the variational parameters; the QG viscosity does not appear. Losses \(\mathcal{L}\) are always minimised, so \(\mathcal{L}_\text{ELBO} = -\mathrm{ELBO}\).

SymbolMeaningKind, shape
\(\vect{x}_{0:T}\)state over the window\(\mathbb{R}^{(T+1) \times D_x}\) (time+1, height, width)
\(\vect{y}_k\), \(\vect{m}_k\), \(\gvect{\sigma}_{\varepsilon,k}\)SSH map, its mask, its error (\(\gvect{\sigma}_{\varepsilon,k}^2\) is the diagonal of \(\gmat{\Sigma}_{\varepsilon,k}\))(time, height, width) each
\(\gvect{\mu}_x\), \(\vect{u}^\text{cov}\), \(\vect{u}^\text{wind}_k\)background (prior mean of \(\vect{x}_0\)), covariates, forcing; the exogenous inputs are \(\vect{u}_k = (\vect{u}^\text{cov}, \vect{u}^\text{wind}_k)\)(height, width), (variable, height, width), (time, height, width)
\(\gmat{\Sigma}_x\), \(\gmat{\Sigma}_\eta\), \(\gmat{\Sigma}_{\varepsilon,k}\)background, model-error and observation-error covariances\(\mathbb{R}^{D_x \times D_x}\) each
\(f\)one-day model: physical or any type T1–T10 of the taxonomy page\(\mathbb{R}^{D_x} \to \mathbb{R}^{D_x}\), field → field
\(\gvect{\theta}\), \(\gvect{\alpha}\)physical parameters, hyperparameters\(\mathbb{R}^{D_\theta}\) (param,), \(\mathbb{R}^{D_\alpha}\) (hyper,)
\(\mathcal{J}\), \(\nabla_{\vect{x}} \mathcal{J}\)cost, its gradient\(\mathbb{R}\), \(\mathbb{R}^{(T+1) \times D_x}\) (time+1, height, width)
\(i\), \(N\)solver iteration, number of iterations—
\(\gvect{\theta}_\text{emu}\), \(\gvect{\theta}_\text{DA}\)weights of a learned model from the taxonomy page; weights of a learned piece of the analysis: prior, solver, gradient, initialiser, proposal, localisationweights
\(\gvect{\nu}\), \(q(\vect{x} \mid \vect{y}; \gvect{\nu})\)variational parameters, variational densityweights or (rank+2, time+1, height, width)
\(\vect{z}_\text{ens}\), \(\vect{z}_\text{mem}\)latents: ensemble coefficients (A3), solver memory (A4)\(\mathbb{R}^{N_{\mathcal{E}}}\), (channel, height, width)
\(w\)a window index, over many windows—

The master equation

Weak-constraint 4D-Var over one window. With \(p(\vect{x} \mid \vect{y}) \propto \exp(-\mathcal{J}(\vect{x}))\), every type on this page edits this cost, its gradient, its solver, or replaces all three:

\[ \mathcal{J}(\vect{x}_{0:T}; \gvect{\theta}) = \underbrace{\frac{1}{2} \Vert \vect{x}_0 - \gvect{\mu}_x \Vert^2_{\gmat{\Sigma}_x^{-1}}}_{\text{background}} + \underbrace{\frac{1}{2} \sum_{k=1}^{T} \Vert \vect{x}_k - f(\vect{x}_{k-1}, \vect{u}_k; \gvect{\theta}) \Vert^2_{\gmat{\Sigma}_\eta^{-1}}}_{\text{dynamics}} + \underbrace{\frac{1}{2} \sum_{k=1}^{T} \Vert \vect{m}_k \odot (\vect{y}_k - \vect{x}_k) \Vert^2_{\gmat{\Sigma}_{\varepsilon,k}^{-1}}}_{\text{observations}} . \]

The classical solver iterates \(\vect{x}^{(i+1)} = \vect{x}^{(i)} - \gamma \mat{P} \nabla_{\vect{x}} \mathcal{J}(\vect{x}^{(i)})\) from \(\vect{x}^{(0)}\), with step size \(\gamma\), \(N \approx 50\)–\(100\) and \(\mat{P}\) built from \(\gmat{\Sigma}_x^{1/2}\). In code, the cost, the solver and the variational objective are three short functions. qg_day and QGParams are the ones defined on the taxonomy page:

import einx
import equinox as eqx
import finitevolx as fvx
import gaussx as gx
import jax
import jax.numpy as jnp
import lineax as lx
from jaxtyping import Array, Bool, Float, PRNGKeyArray

# Shapes: T = 10 maps (time); Z = T + 1 = 11 states x₀ … x₁₀ (day);
#         H, W = 130 × 130 grid with ghost ring (height, width);
#         N = H·W (cell); F = Z·N (flat); K = 16 (rank); S = 8 samples (member)

Op = lx.AbstractLinearOperator


class Window(eqx.Module):
    """One window: maps y, masks m, errors σ_ε², background μ_x, Σ_x, diagonal Σ_η."""

    maps: Float[Array, "time height width"]
    masks: Bool[Array, "time height width"]
    obs_var: Float[Array, "time height width"]
    model_err_var: Float[Array, "time height width"]
    background: Float[Array, "height width"]
    covariates: Float[Array, "variable height width"]
    forcing: Float[Array, "time height width"]
    prior_cov: Op
    grid: fvx.CartesianGrid2D


def weak_4dvar_cost(
    states: Float[Array, "day height width"],
    window: Window,
    params: QGParams,
) -> Float[Array, ""]:
    # --- background ---
    # δ₀ = x₀ − μ_x
    initial_departure = states[0] - window.background  # (H, W)
    departure_flat = einx.rearrange(                  # (H, W) → (N,)
        "height width -> (height width)", initial_departure
    )
    # today (gaussx)
    # δ₀ᵀ Σ_x⁻¹ δ₀
    background_quad = gx.quadratic_form(window.prior_cov, departure_flat)  # → ()
    # 𝒥_x = ½ δ₀ᵀ Σ_x⁻¹ δ₀
    background_cost = 0.5 * background_quad           # → ()

    # --- dynamics, all days at once ---
    previous = states[:-1]                            # (Z, Z) → (T, H, W)
    current = states[1:]                              # (Z, Z) → (T, H, W)
    batched_day = jax.vmap(qg_day, in_axes=(0, None, 0, None, None))
    # user (qg_day, taxonomy page)
    # f(x_{k−1}, u_k; θ) for every k, in parallel
    forecasts = batched_day(                          # (T, H, W) → (T, H, W)
        previous, window.covariates, window.forcing, params, window.grid
    )
    # η_k = x_k − f(x_{k−1}, u_k; θ)
    model_error = current - forecasts                 # (T, H, W)
    # η_k²
    squared_error = model_error**2                    # (T, H, W)
    # η_k² / σ_η²  (diagonal Σ_η)
    weighted_error = squared_error / window.model_err_var  # (T, H, W)
    # ∑_k η_kᵀ Σ_η⁻¹ η_k
    model_sum = einx.sum("time height width ->", weighted_error)  # → ()
    # 𝒥_η = ½ ∑_k ‖η_k‖²_{Σ_η⁻¹}
    model_cost = 0.5 * model_sum                      # → ()

    # --- observations ---
    # y_k − x_k
    misfit = window.maps - current                    # (T, H, W)
    # r_k = m_k ⊙ (y_k − x_k)
    residual = jnp.where(window.masks, misfit, 0.0)   # (T, H, W)
    # r_k²
    squared_residual = residual**2                    # (T, H, W)
    # r_k² / σ_{ε,k}²
    weighted_residual = squared_residual / window.obs_var  # (T, H, W)
    # ∑_k r_kᵀ Σ_{ε,k}⁻¹ r_k
    obs_sum = einx.sum("time height width ->", weighted_residual)  # → ()
    # 𝒥_ε = ½ ∑_k ‖m_k ⊙ (y_k − x_k)‖²_{Σ_{ε,k}⁻¹}
    obs_cost = 0.5 * obs_sum                          # → ()

    # 𝒥 = 𝒥_x + 𝒥_η + 𝒥_ε
    total_cost = background_cost + model_cost + obs_cost  # → ()
    return total_cost


def analyse(
    window: Window,
    params: QGParams,
    initial_states: Float[Array, "day height width"],
    *,
    step_size: float = 0.1,
    n_iterations: int = 100,
) -> Float[Array, "day height width"]:
    cost_gradient = jax.grad(weak_4dvar_cost)

    def iterate(states, _):
        # ∇_x 𝒥(x⁽ⁱ⁾), by reverse mode
        gradient = cost_gradient(states, window, params)  # → (Z, H, W)
        # user
        # d⁽ⁱ⁾ = P ∇_x 𝒥(x⁽ⁱ⁾)
        direction = precondition(gradient, window)    # (Z, H, W)
        # γ d⁽ⁱ⁾
        update = step_size * direction                # (Z, H, W)
        # x⁽ⁱ⁺¹⁾ = x⁽ⁱ⁾ − γ P ∇_x 𝒥(x⁽ⁱ⁾)
        states = states - update                      # (Z, H, W)
        return states, None

    analysis, _ = jax.lax.scan(                       # (Z, H, W) → (Z, H, W)
        iterate, initial_states, None, length=n_iterations
    )
    return analysis


class LowRankGaussian(eqx.Module):
    """q(x) = 𝒩(μ_q, L_q L_qᵀ + D_q) over a whole window, flattened."""

    mean: Float[Array, "flat"]
    factor: Float[Array, "flat rank"]
    log_diag: Float[Array, "flat"]


def neg_elbo(
    posterior: LowRankGaussian,
    window: Window,
    params: QGParams,
    *,
    n_samples: int = 8,
    key: PRNGKeyArray,
) -> Float[Array, ""]:
    height, width = window.background.shape

    # --- variational density ---
    # D_q = diag(exp(log d))
    diag = jnp.exp(posterior.log_diag)                # (F,) → (F,)
    # today (gaussx)
    # Σ_q = L_q L_qᵀ + D_q
    covariance = gx.low_rank_plus_diag(diag, posterior.factor)  # (F, F) PSD op

    # --- samples ---
    # today (gaussx)
    # x⁽ᵉ⁾ = μ_q + Σ_q^{1/2} ξ,  ξ ~ 𝒩(0, I)
    samples_flat = gx.sample_mvn(                     # → (S, F)
        posterior.mean, covariance, key=key, num_samples=n_samples
    )
    samples = einx.rearrange(                         # (S, F) → (S, Z, H, W)
        "member (day height width) -> member day height width",
        samples_flat,
        height=height,
        width=width,
    )

    # --- expected cost and entropy ---
    batched_cost = jax.vmap(weak_4dvar_cost, in_axes=(0, None, None))
    # 𝒥(x⁽ᵉ⁾) for each sample
    sample_costs = batched_cost(samples, window, params)  # → (S,)
    # 𝔼_q[𝒥] ≈ (1/S) ∑_e 𝒥(x⁽ᵉ⁾)
    expected_cost = einx.mean("member ->", sample_costs)  # (S,) → ()
    # today (gaussx)
    # ℍ[q] = ½ (N (1 + log 2π) + log det Σ_q)
    entropy = gx.gaussian_entropy(covariance)         # → ()

    # ℒ_ELBO = −ELBO = 𝔼_q[𝒥(x)] − ℍ[q],  since p(x | y) ∝ exp(−𝒥(x))
    loss = expected_cost - entropy                    # → ()
    return loss

Markers: today ships on main (checked 24 Oct 2026: finitevolX 831c69f, gaussx eeda235, somax 9cb19b3); today* ships, arguments sketched; user is defined on these pages; not pinned is a library whose API was not checked. Shape column letters are defined in the header.

No Gaussian variational family ships in gaussx or vardax, so LowRankGaussian is page-defined; gaussx supplies its covariance, samples and entropy.

TypeThe edit
A1an outer loop differentiates the evidence of many analyse calls with respect to the scales inside prior_cov, obs_var and model_err_var
A2background_cost (and model_cost) → prior_net(phi, states), or qg_day → a hybrid model
A3jax.grad(weak_4dvar_cost) → an emulator's gradient, an ensemble estimate, or gradient_net(phi, states, window)
A4iterate body → states - solver_net(phi, gradient, memory); n_iterations=10; train through the scan
A5initial_states = init_net(phi, window), or precondition → a learned positive-definite map
A6analyse → analysis_net(psi, window)
A7posterior = amortised_net(psi, window); minimise neg_elbo over many windows, or forward KL on OSSEs
A8minimise neg_elbo over posterior for this window only
A9start A8 from amortised_net, run a few steps
A10replace analyse by an EnKF cycle with learned localisation and inflation
A11alternate analyse per window with a gradient step on the weights inside qg_day through model_cost

Training signals

What a type can learn from decides where it can be used. Five signals, with what each needs and which statistic it returns:

SignalLossNeedsReturns
MSE on OSSEs\(\mathcal{L}_\text{MSE} = \mathbb{E} \Vert \vect{x} - \hat{\vect{x}}(\vect{y}) \Vert^2\)simulated truthposterior mean
Forward KL\(\mathcal{L}_\text{NLL} = \mathbb{E}_{p(\vect{x}, \vect{y})}[-\log q(\vect{x} \mid \vect{y}; \gvect{\nu})]\)simulated trutha density, mass-covering
ELBO\(\mathcal{L}_\text{ELBO} = \mathbb{E}_q[\mathcal{J}(\vect{x})] - \mathbb{H}[q]\)likelihood and a model-based prior; no trutha density, mode-seeking
Held-out observations\(\mathcal{L}_\text{held-out} = \mathbb{E} \Vert \vect{m}^\text{out} \odot (\vect{y} - \hat{\vect{x}}(\vect{m}^\text{in} \odot \vect{y})) \Vert^2_{\gmat{\Sigma}_\varepsilon^{-1}}\)observations only, split \(\vect{m} = \vect{m}^\text{in} + \vect{m}^\text{out}\)the observable part of the mean
Evidence\(-\log p(\vect{y} \mid \gvect{\alpha})\)the model and observationshyperparameters
The simulation gap and the resolution gap

OSSE-trained types learn the climate of the QG model: its eddy sizes, its energy, its errors. Observation-trained types learn the real ocean, but gridded SSH maps resolve little below roughly 100 km, so held-out-pixel losses cannot teach anything finer. The practical combination is to pretrain on OSSEs for the small scales and fine-tune with the ELBO or held-out observations for the large ones.

The eleven types

Each card gives the equation, the operator signature with what is learned and what it trains on, and attribute rows. The training signal is a row, as on the taxonomy page.

A1

Learned hyperparameters

replaces: hand-tuned α

Keep everything, and fit the numbers that shape \(\gmat{\Sigma}_x\), \(\gmat{\Sigma}_\eta\), \(\gmat{\Sigma}_\varepsilon\), localisation and inflation by maximising the evidence over many real windows \(w\):

\[ \gvect{\alpha}^\star = \mathrm{argmax}_{\gvect{\alpha}} \sum_{w} \log p(\vect{y}^{(w)} \mid \gvect{\alpha}) \] \[ \log p(\vect{y} \mid \gvect{\alpha}) \approx -\mathcal{J}(\hat{\vect{x}}; \gvect{\alpha}) - \frac{1}{2} \log \det \nabla^2_{\vect{x}} \mathcal{J}(\hat{\vect{x}}; \gvect{\alpha}) + \text{const}. \]

The right-hand side is the Laplace approximation at the analysis \(\hat{\vect{x}}\). The cheaper alternative is to match the innovation statistics of the state-estimation page.

iny, m, σ_ε(time, height, width) ×3μ_x(height, width)α(hyper,)outx̂_0:T(time+1, height, width)learnedα(hyper,)trainreal windows, obs only(window, time, height, width) ×3
Replaces
manual tuning of Σ_x, Σ_η, Σ_ε scales, localisation radius, inflation
Signal
marginal likelihood, or innovation consistency; observations only
Output
the classical analysis with better weights
Failure
loud a bad α shows up in 2𝒥⋆/D_y and the innovations
Cost
an outer loop over many classical analyses

The DA counterpart of T1. Always worth doing first: every later type inherits α, and a learned piece trained with a wrong Σ_ε learns to compensate for it.

A2

Learned prior

replaces: the background and dynamics terms

The observation term stays; the regulariser becomes learned. Three forms, from least to most physical:

\[ \mathcal{J}_\text{learned}(\vect{x}) = \frac{1}{2} \sum_k \Vert \vect{m}_k \odot (\vect{y}_k - \vect{x}_k) \Vert^2_{\gmat{\Sigma}_{\varepsilon,k}^{-1}} + \beta \mathcal{R}(\vect{x}), \] \[ \mathcal{R}(\vect{x}) \in \left\{ -\log p(\vect{x}; \gvect{\theta}_\text{DA}), \Vert \vect{x} - g_\text{proj}(\vect{x}; \gvect{\theta}_\text{DA}) \Vert^2, \frac{1}{2} \sum_k \Vert \vect{x}_k - f(\vect{x}_{k-1}; \gvect{\theta}_\text{emu}) \Vert^2_{\gmat{\Sigma}_\eta^{-1}} \right\}. \]

The first is a density (a flow or an energy model on windows). The second is a projection: \(g_\text{proj}\) maps a window to the nearest plausible one, as in 4DVarNet. The third is weak-constraint 4D-Var with any hybrid model from the taxonomy page as \(f(\cdot; \gvect{\theta}_\text{emu})\).

iny, m, σ_ε(time, height, width) ×3outx̂_0:T(time+1, height, width)learnedθ_DAprior networktrainplausible windows(sim, time+1, height, width)
Replaces
½‖x₀ − μ_x‖²_{Σ_x⁻¹} and the dynamics term
Signal
density fitting on simulated or reanalysis windows (no observations needed), or end to end with A4
Output
the minimiser of the new cost: a MAP under the learned prior
Failure
loud the cost is explicit and its value is checkable
Cost
as 4D-Var, plus the prior's gradient

The dynamical form is the important one: it keeps physics in the prior and makes every model on the taxonomy page a candidate. The projection form is expressive but carries no physics, and it is not a normalised density, so it cannot be sampled.

A3

Approximate gradient

replaces: the adjoint

Keep the cost and the solver; compute \(\nabla_{\vect{x}} \mathcal{J}\) more cheaply. With \(\mat{F}_k = \mat{J}_f(\vect{x}_{k-1})\) the tangent-linear model and \(\tilde{\mat{X}}_k \in \mathbb{R}^{D_x \times N_{\mathcal{E}}}\) ensemble anomalies:

\[ \underbrace{\mat{F}_k^\top \to \mat{J}_f(\vect{x}_{k-1}; \gvect{\theta}_\text{emu})^\top}_{\text{emulator adjoint}} \] \[ \underbrace{\vect{x}_0 = \gvect{\mu}_x + \tilde{\mat{X}}_0 \vect{z}_\text{ens}, \text{ } \mat{F}_{k \leftarrow 0} \tilde{\mat{X}}_0 \approx \tilde{\mat{X}}_k}_{\text{ensemble-variational, no adjoint}} \] \[ \underbrace{\nabla_{\vect{x}} \mathcal{J} \approx g_\text{grad}(\vect{x}, \vect{y}; \gvect{\theta}_\text{DA})}_{\text{learned gradient}} . \]

Here \(\vect{z}_\text{ens} \in \mathbb{R}^{N_{\mathcal{E}}}\) has one entry per ensemble member and \(\mat{F}_{k \leftarrow 0}\) maps an initial perturbation to day \(k\).

iny, m, σ_ε(time, height, width) ×3μ_x(height, width)α(hyper,)outx̂_0:T(time+1, height, width)learnedθ_emu, θ_DAemulator or gradient netX̃(member, time+1, height, width)trainOSSE windows(sim, time+1, height, width) + obs
Replaces
the adjoint run in each iteration
Signal
emulator: step-1 trajectories with a Jacobian term; ensemble: none
Output
a point estimate, near the 4D-Var MAP if the gradient is right
Failure
silent a biased gradient converges to the wrong point and the iterations still look healthy
Cost
forward runs only (ensemble), or one network evaluation per iteration

The test for any approximate gradient: at the true minimiser it must vanish. Check it by evaluating the exact gradient once at the end; that single adjoint run turns a silent failure into a loud one.

A4

Learned solver (unrolled)

replaces: the optimiser

A network decides each update from the gradient and an internal memory \(\vect{z}_\text{mem}\). The loop is unrolled for a fixed \(N \approx 10\) iterations and trained end to end, usually together with a learned prior (A2):

\[ \begin{aligned} \vect{x}^{(i+1)} &= \vect{x}^{(i)} - g_\text{step}\left(\nabla_{\vect{x}} \mathcal{J}_\text{learned}(\vect{x}^{(i)}), \vect{z}_\text{mem}^{(i)}; \gvect{\theta}_\text{DA}\right),\\ \vect{z}_\text{mem}^{(i+1)} &= g_\text{mem}\left(\nabla_{\vect{x}} \mathcal{J}_\text{learned}(\vect{x}^{(i)}), \vect{z}_\text{mem}^{(i)}; \gvect{\theta}_\text{DA}\right),\\ \gvect{\theta}_\text{DA}^\star &= \mathrm{argmin}_{\gvect{\theta}_\text{DA}} \mathcal{L}_\text{MSE}(\gvect{\theta}_\text{DA}), \text{ } \mathcal{L}_\text{MSE}(\gvect{\theta}_\text{DA}) = \mathbb{E}_{(\vect{x}, \vect{y})} \Vert \vect{x} - \vect{x}^{(N)}(\vect{y}; \gvect{\theta}_\text{DA}) \Vert^2 . \end{aligned} \]
iny, m, σ_ε(time, height, width) ×3outx̂_0:T(time+1, height, width)learnedθ_DAsolver g_step, memory g_mem, priorz_mem(channel, height, width)trainOSSE windows(sim, time+1, height, width) + obs
Replaces
L-BFGS or Gauss–Newton, and usually the prior too
Signal
MSE against simulated truth; or held-out observations on real data
Output
after end-to-end MSE training, an estimate of the posterior mean, not the minimiser of 𝒥_learned
Failure
silent the cost is no longer what is minimised, so its value does not certify the output
Cost
N ≈ 10 gradient evaluations, versus 50–100 classically

4DVarNet's core. The variational cost becomes an architecture: a good inductive bias for the network, not a probability model. Treat its output as a mean, and do not read its cost as a posterior.

A5

Warm start or learned preconditioner

replaces: the initial guess and the metric

The classical solver still decides the answer. The network only proposes where to start, or reshapes the space so fewer iterations are needed:

\[ \vect{x}^{(0)} = g_\text{init}(\vect{y}, \vect{m}, \gvect{\sigma}_\varepsilon, \gvect{\mu}_x; \gvect{\theta}_\text{DA}) \] \[ \vect{x}^{(i+1)} = \vect{x}^{(i)} - \gamma \mat{P}(\gvect{\theta}_\text{DA}) \nabla_{\vect{x}} \mathcal{J}(\vect{x}^{(i)}), \text{ } \mat{P}(\gvect{\theta}_\text{DA}) \succ 0 . \]
iny, m, σ_ε(time, height, width) ×3μ_x(height, width)α(hyper,)outx̂_0:T(time+1, height, width)learnedθ_DAinitialiser or preconditionertrain4D-Var analyses(window, time+1, height, width)
Replaces
x⁽⁰⁾ = μ_x and the plain Σ_x^{1/2} change of variables
Signal
the analyses of box 2 itself, or the iteration count
Output
the classical MAP, unchanged
Failure
loud a bad guess only costs iterations
Cost
fewer iterations; never more wrong

The safest learned DA. Its training targets are box 2 outputs, so no truth and no OSSE are needed, and its worst case is plain 4D-Var. The speed-up is bounded by how far the classical solve has to travel.

A6

Amortised point analysis

replaces: the whole solve

One forward pass from observations to state, trained across windows:

\[ \hat{\vect{x}}_{0:T} = g_\text{amort}(\vect{y}_{1:T}, \vect{m}_{1:T}, \gvect{\sigma}_{\varepsilon,1:T}, \gvect{\mu}_x, \vect{u}_{1:T}; \gvect{\nu}) \] \[ \gvect{\nu}^\star = \mathrm{argmin}_{\gvect{\nu}} \mathcal{L}_\text{MSE}(\gvect{\nu}), \text{ } \mathcal{L}_\text{MSE}(\gvect{\nu}) = \mathbb{E} \Vert \vect{x}_{0:T} - \hat{\vect{x}}_{0:T} \Vert^2 \Rightarrow \hat{\vect{x}} = \mathbb{E}[\vect{x} \mid \vect{y}] . \]
iny, m, σ_ε(time, height, width) ×3μ_x(height, width)α(hyper,)outx̂_0:T(time+1, height, width)learnedνnetworktrainOSSE windows(sim, time+1, height, width) + obs
Replaces
objective, gradient and solver
Signal
MSE on OSSEs; or held-out observations on real windows
Output
posterior mean, no uncertainty
Failure
silent an unusual window gets a confident, smooth, wrong field
Cost
one forward pass

The direct analysis of the state-estimation page. Its output can still be scored for free: evaluate the classical 𝒥 and the innovations on x̂. That check costs one model run and catches most silent failures.

A7

Amortised variational inference

replaces: the whole solve, with a density

A conditional density over the window, trained either on simulated truth (forward KL) or on real observations (ELBO):

\[ \text{forward KL: } \mathcal{L}_\text{NLL}(\gvect{\nu}) = \mathbb{E}_{p(\vect{x}, \vect{y})}\left[-\log q(\vect{x} \mid \vect{y}; \gvect{\nu})\right] \] \[ \text{ELBO: } \mathcal{L}_\text{ELBO}(\gvect{\nu}) = -\mathbb{E}_{\vect{y}} \mathbb{E}_{q}\left[\log p(\vect{y} \mid \vect{x}) + \log p(\vect{x}) - \log q(\vect{x} \mid \vect{y}; \gvect{\nu})\right] \]

The family of \(q(\cdot; \gvect{\nu})\) sets what uncertainty it can express:

FamilyFormParameters per window
Mean-field\(\mathcal{N}(\gvect{\mu}_q, \mathrm{diag}(\gvect{\sigma}^2_q))\)(2, time+1, height, width)
Low rank + diagonal\(\mathcal{N}(\gvect{\mu}_q, \mat{L}_q \mat{L}_q^\top + \mat{D}_q)\)(rank+2, time+1, height, width)
Markov\(q(\vect{x}_0 \mid \vect{y}; \gvect{\nu}) \prod_k q(\vect{x}_k \mid \vect{x}_{k-1}, \vect{y}; \gvect{\nu})\)one transition network
Flow or diffusion\(\vect{x} = g_\text{flow}(\gvect{\xi}; \vect{y}, \gvect{\nu})\)a conditional generator
iny, m, σ_ε(time, height, width) ×3μ_x(height, width)α(hyper,)outμ_q,0:T(time+1, height, width)samples(member, time+1, height, width)learnedνvariational parameterstrainOSSE windows(sim, time+1, height, width) + obsreal windows, obs only(window, time, height, width) ×3
Replaces
the whole solve, and the ensemble
Signal
forward KL on OSSEs (needs truth) or ELBO on real observations (needs the likelihood and a model-based prior, no truth)
Output
a density: mean and samples
Failure
silent forward KL tends to be well spread; ELBO tends to be under-dispersed
Cost
one forward pass per sample

The ELBO route is the one that trains on the real ocean without truth, because the model enters through log p(x). The Markov family is the natural choice here: it respects time and is a learned smoother.

A8

Per-window variational inference

replaces: the point estimate

Fit a Gaussian to each window's posterior directly. With \(\gvect{\xi} \sim \mathcal{N}(\vect{0}, \mat{I})\), samples are reparameterised, and the ELBO uses the classical cost because \(p(\vect{x} \mid \vect{y}) \propto \exp(-\mathcal{J}(\vect{x}))\):

\[ \vect{x} = \gvect{\mu}_q + \mat{L}_q \gvect{\xi}_1 + \mat{D}_q^{1/2} \gvect{\xi}_2 \] \[ \min_{\gvect{\mu}_q, \mat{L}_q, \mat{D}_q} \mathcal{L}_\text{ELBO} = \mathbb{E}_{q}\left[\mathcal{J}(\vect{x})\right] - \mathbb{H}[q]. \]

As the rank grows, \(\gvect{\mu}_q\) approaches the posterior mean and \(\mat{L}_q \mat{L}_q^\top + \mat{D}_q\) the posterior covariance. With one sample at \(\gvect{\xi} = \vect{0}\), it collapses to 4D-Var.

iny, m, σ_ε(time, height, width) ×3μ_x(height, width)α(hyper,)outμ_q,0:T(time+1, height, width)samples(member, time+1, height, width)learnedμ_q, L_q, D_q(rank+2, time+1, height, width)trainnone—
Replaces
4D-Var's point estimate and its Laplace covariance
Signal
this window's ELBO; no training set
Output
a Gaussian posterior for the window
Failure
loud the ELBO value is a direct check
Cost
4D-Var × samples per iteration

The honest reference for A7, and box 2 extended to give a density. With gaussx for the low-rank algebra and the somax adjoint, it is mostly a change of loss in code you already have.

A9

Semi-amortised inference

amortised proposal, per-window refinement

Start from an amortised answer, then take a few classical or variational steps on the actual window:

\[ \gvect{\nu}^{(0)}_w = g_\text{prop}(\vect{y}^{(w)}; \gvect{\theta}_\text{DA}) \] \[ \gvect{\nu}^{(i+1)}_w = \gvect{\nu}^{(i)}_w - \gamma \nabla_{\gvect{\nu}} \mathcal{L}_{\text{ELBO},w}(\gvect{\nu}^{(i)}_w) \text{ for } i = 0, \ldots, N_\text{refine} - 1 . \]

A related route is guided diffusion: learn the prior score \(g_\text{score}(\vect{x}, \tau; \gvect{\theta}_\text{DA}) \approx \nabla_{\vect{x}} \log p_\tau(\vect{x})\) from states alone, and at inference add \(\nabla_{\vect{x}} \log p(\vect{y} \mid \vect{x})\) with this window's masks to every denoising step.

iny, m, σ_ε(time, height, width) ×3μ_x(height, width)α(hyper,)outμ_q,0:T(time+1, height, width)samples(member, time+1, height, width)learnedθ_DAproposal or prior scoretrainOSSE windows(sim, time+1, height, width) + obs
Replaces
the cold start of A8, or the fixed answer of A7
Signal
proposal trained as A7; refinement uses each window's ELBO
Output
a density refined on the window
Failure
loud refinement steps that move far flag an out-of-distribution window
Cost
one pass plus N_refine ≪ N iterations

Closes the amortisation gap, and the size of the refinement is a built-in novelty detector. Guided diffusion has the same advantage for the observing system: masks change at inference without retraining.

A10

Learned filter pieces

sidebar: the sequential route

For the EnKF, keep the forecast–analysis cycle and learn its fragile parts: localisation \(\gmat{\rho}(\gvect{\theta}_\text{DA})\), inflation \(\alpha_\text{infl}(\gvect{\theta}_\text{DA})\), or the gain itself, and run the forecast with an emulator. Trained end to end through the cycle on a rollout loss:

\[ \mat{G}_k = \left(\gmat{\rho} \odot \tilde{\mat{X}}_k (\mat{H}_k \tilde{\mat{X}}_k)^\top\right) \left(\mat{H}_k \tilde{\mat{X}}_k (\mat{H}_k \tilde{\mat{X}}_k)^\top + \gmat{\Sigma}_{\varepsilon,k}\right)^{-1} \] \[ \tilde{\mat{X}}_k \leftarrow \alpha_\text{infl} \tilde{\mat{X}}_k . \]
iny_k, m_k(height, width) per daymembers(member, height, width)outanalysis members(member, height, width) per daylearnedθ_DAρ, α_infl, or gain nettrainOSSE windows(sim, time+1, height, width) + obs
Replaces
hand-tuned localisation and inflation; the forecast model
Signal
rollout error of the filter on OSSEs
Output
an ensemble per day, streaming
Failure
loud innovations and spread–error ratio are monitored every cycle
Cost
members × forward runs; no adjoint anywhere

Only forward runs, so step-map emulators (T6–T8) fit here without the Jacobian problem. Kept short on this page; the window route is the focus.

A11

Joint model and state learning

learning a hybrid from observations alone

To learn a model from data you need the states, and to get the states you need the model. Alternate, with any A-type as the E-step:

\[ \begin{aligned} \text{E-step: } & q^{(w)}(\vect{x}) \approx p(\vect{x} \mid \vect{y}^{(w)}; \gvect{\theta}_\text{emu}) \text{ for each window } w,\\ \text{M-step: } & \gvect{\theta}_\text{emu} \leftarrow \mathrm{argmin}_{\gvect{\theta}_\text{emu}} \sum_w \mathbb{E}_{q^{(w)}}\left[\frac{1}{2} \sum_k \Vert \vect{x}_k - f(\vect{x}_{k-1}; \gvect{\theta}_\text{emu}) \Vert^2_{\gmat{\Sigma}_\eta^{-1}}\right]. \end{aligned} \]
iny, m, σ_ε(time, height, width) ×3μ_x(height, width)α(hyper,)outμ_q,0:T(time+1, height, width)learnedθ_emua model from the taxonomy pagetrainreal windows, obs only(window, time, height, width) ×3
Replaces
the need for a richer reference model
Signal
observations only; variational EM puts both steps in one ELBO
Output
a corrected model f(·; θ_emu) and analysed states
Failure
silent model and states can co-adapt: the model learns the analysis's errors
Cost
an analysis per window per EM iteration

The bridge between the two pages: it is how a correction-type hybrid (T1–T4, T7) is trained when no high-resolution truth exists. Fix Σ_η and α before starting, or the M-step can explain everything as model error.

Model type × assimilation type

Any model from the taxonomy page can sit inside any type that uses a model. A6 and A7 use a model only to make training data (OSSEs) or, for the ELBO, inside \(\log p(\vect{x})\).

● natural   ◐ works, with a caveat   ○ does not fit

AssimilationPhysical fT1–T5 (classical integrator)T6–T8 (step maps)T9 rolloutT10 embedding
A1 hyperparameters●●●◐◐
A2 dynamical prior●●◐○◐
A3 approx. gradient●●◐◐◐
A4 learned solver●●◐◐◐
A5 warm start●●●●●
A8 per-window VI●●◐◐◐
A9 semi-amortised●●◐◐◐
A10 learned filter●●●○◐
A11 joint learning◐●●◐◐

The caveats: T6–T8 need a trustworthy Jacobian wherever a gradient flows through them (A2–A4, A8, A9); T9 has no one-day step, so it cannot enter a weak-constraint dynamics term or cycle in a filter; T10 moves the whole problem into the embedding, with observations entering through the decoder. A11 with the physical model only calibrates θ, which is A1's job done the hard way.

What the structure implies

Score every output with the classical cost

Whatever produced \(\hat{\vect{x}}\), computing \(\mathcal{J}(\hat{\vect{x}})\), \(2 \mathcal{J}^\star / D_y\) and the innovations costs one model run. It is the cheapest guard against silent failure, and it makes the box-2 diagnostics of the state-estimation page apply to every learned type.

A learned cost is not a posterior

Once a cost is trained end to end for MSE (A2 with A4), its minimiser is no longer the point of interest and \(\exp(-\mathcal{J}_\text{learned})\) is not a calibrated density. Uncertainty then has to come from somewhere else: an ensemble of solvers, a VI head, or A8 run around the output.

The safe ladder

A1 → A5 → A8 → A9 keeps the classical machinery in charge at every step and adds learning only where it pays: tuning, speed, uncertainty, then amortisation with refinement. A4, A6 and A7 jump straight to amortisation and give up the checks; they are worth it when per-window cost is the binding constraint and the training distribution covers the windows you will see.

Learning the model needs the analysis

A11 is the only way to train a correction-type hybrid from observations alone. It inherits every weakness of its E-step, and it can let the model absorb analysis errors. Fix α first (A1), and keep the model's learned part structural (T1, T2, T4) so it cannot explain everything.

Where each type lives

TypeHook in the stack
A1, A5, A8the 4D-Var path (vardax) on a somax model, with gaussx for \(\gmat{\Sigma}_x^{1/2}\), low-rank covariances and log-determinants
A2the dynamics term takes any somax model; a learned density from the flow and probabilistic libraries
A3, A10ensemble machinery (filterax); emulators from the taxonomy page as forecast or gradient
A4, A6, A7, A9a training loop over windows in pipekit, with the DA cycle supplying OSSE pairs
A11an outer loop alternating the DA cycle with rollout_loss-style updates of the model's weights