Models¶
The seven analysis methods are peers — sibling equinox.Module classes
with no inheritance between them, each owning exactly the assumptions its
math requires. Pick by problem structure: a single analysis time with a
linear observation operator wants OI or
3DVar; an assimilation window with perfect-model
dynamics wants strong-constraint 4DVar; admitting
model error turns that into weak-constraint 4DVar;
the operational linearise-and-iterate formulation with control-variable
transform is incremental 4DVar; and replacing
the inner-loop optimiser with a trained ConvLSTM gives
4DVarNet.
Every model exposes .as_analysis_step(), returning a lightweight wrapper
that satisfies the pipekit-cycle AnalysisStep Protocol —
that is the seam through which all seven plug into
VarDACycle / VarSmootherCycle interchangeably.
Classical methods¶
Closed-form and optimisation-based analyses. OptimalInterpolation is the
linear-Gaussian BLUE solution and refuses non-linear observation operators
at construction; ThreeDVar minimises the same cost iteratively and accepts
non-linear operators; the three 4DVar variants extend the cost over a time
window. IncrementalConfig collects the outer/inner-loop knobs of
IncrementalFourDVar.
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:
OptimalInterpolation
¶
Bases: Module
BLUE / OI — closed-form linear-Gaussian analysis.
Algorithm
- For each sample (
jax.vmap), form the masked operator \(m \odot H(\cdot)\) and the innovation \(d = m \odot y - m \odot H(x_b)\). - Linearise the masked operator at \(x_b\) to get \(H\) and build the innovation operator \(H B H^\top + R\) without materialising it.
- Solve \((H B H^\top + R) v = d\) with CG, or with
solverwhen given. - Return \(x_b + B H^\top v\).
Attributes:
| Name | Type | Description |
|---|---|---|
obs_op |
Any
|
Observation operator. Must be linear (its
|
prior_mean |
Float[Array, '*state']
|
Background \(x_b\). |
prior_cov_op |
AbstractLinearOperator
|
Background-error covariance \(B\) as a
|
obs_cov_op |
AbstractLinearOperator
|
Observation-error covariance \(R\) as a
|
cg_atol |
float
|
CG absolute tolerance for the inner solve. |
cg_rtol |
float
|
CG relative tolerance for the inner solve. |
cg_max_steps |
int
|
CG iteration cap. |
solver |
AbstractLinearSolver | None
|
Optional linear solver for the innovation system, used
instead of CG (the |
Examples:
With \(B = R = I\), identity \(H\) and everything observed, the analysis splits the innovation in half: \(x^* = y / 2\).
>>> import jax, jax.numpy as jnp, lineax as lx, vardax
>>> eye = lx.IdentityLinearOperator(jax.ShapeDtypeStruct((1, 3), jnp.float32))
>>> oi = vardax.OptimalInterpolation(
... obs_op=vardax.MaskedIdentity(),
... prior_mean=jnp.zeros((1, 3)),
... prior_cov_op=eye,
... obs_cov_op=eye,
... )
>>> batch = vardax.Batch1D(input=jnp.ones((1, 1, 3)), mask=jnp.ones((1, 1, 3)))
>>> xa = oi(batch)
>>> xa.shape
(1, 1, 3)
>>> bool(jnp.allclose(xa, 0.5, atol=1e-4))
True
Unobserved entries (zero mask) keep the background, and an exact Cholesky solver replaces CG:
>>> exact = vardax.OptimalInterpolation(
... obs_op=vardax.MaskedIdentity(),
... prior_mean=jnp.zeros((1, 3)),
... prior_cov_op=eye,
... obs_cov_op=eye,
... solver=lx.Cholesky(),
... )
>>> gap = vardax.Batch1D(
... input=jnp.ones((1, 1, 3)), mask=jnp.array([[[1.0, 0.0, 1.0]]])
... )
>>> bool(jnp.allclose(exact(gap), jnp.array([0.5, 0.0, 0.5]), atol=1e-5))
True
Composed with the Laplace adapter, the posterior covariance of this problem is \((I + I)^{-1} = I / 2\):
>>> post = oi.posterior(batch)
>>> bool(jnp.allclose(post.cov.mv(jnp.ones((1, 3))), 0.5, atol=1e-2))
True
Source code in src/vardax/_src/models/optimal_interpolation.py
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 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 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | |
posterior
¶
posterior(batch: Batch1D, adapter: Any = None) -> Any
Analyse one window and return its Posterior.
Runs the analysis, then adapter(analysis, self, batch); the
default adapter is LaplaceCovariance
with this model's \(B\) and \(R\), and with this model's solver
when one is set.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
batch
|
Batch1D
|
A |
required |
adapter
|
Any
|
Optional |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The window's posterior: mean and lazy covariance. |
Raises:
| Type | Description |
|---|---|
ShapeError
|
If |
UnsupportedInputError
|
For a point batch. |
Source code in src/vardax/_src/models/optimal_interpolation.py
as_analysis_step
¶
Adapt to pipekit_cycle.AnalysisStep (Decision D8).
Returns:
| Type | Description |
|---|---|
_OIAnalysisStep
|
An analysis step that returns the BLUE analysis around a |
_OIAnalysisStep
|
supplied forecast, treating |
Source code in src/vardax/_src/models/optimal_interpolation.py
ThreeDVar
¶
Bases: Module
3D variational analysis.
with the residual masked by \(m\) where observations are missing.
Algorithm
- For each sample in the batch (
jax.vmap), start from the background \(x_b\) (or the parameterization's initial control). - Evaluate the background term
\(\tfrac12 \delta x^\top B^{-1} \delta x\) with a
cov_solversolve; under aparameterizationthe penalty \(\mathrm{pen}(\theta)\) replaces it. - Evaluate the observation term from the masked residual \(m \odot (y - H(x))\) and an \(R^{-1}\) solve; a point batch uses the \(\sigma_i\)-weighted point cost instead.
- Minimise the sum with
minimiserfor at mostmax_stepsiterations and return the minimising state.
Attributes:
| Name | Type | Description |
|---|---|---|
obs_op |
Any
|
Observation operator (linear or nonlinear). |
prior_mean |
Float[Array, '*state']
|
Background \(x_b\). |
prior_cov_op |
AbstractLinearOperator
|
\(B\). |
obs_cov_op |
AbstractLinearOperator | None
|
\(R\). |
minimiser |
AbstractMinimiser | Minimiser
|
|
minimiser_adjoint |
AbstractAdjoint
|
|
max_steps |
int
|
Iteration cap on the inner solver when |
cov_solver |
AbstractLinearSolver
|
|
parameterization |
StateParameterization | None
|
Optional
|
Examples:
With \(B = R = I\), identity \(H\) and everything observed, the analysis agrees with the closed-form BLUE, \(x^* = y / 2\).
>>> import jax, jax.numpy as jnp, lineax as lx, vardax
>>> eye = lx.IdentityLinearOperator(jax.ShapeDtypeStruct((1, 3), jnp.float32))
>>> three = vardax.ThreeDVar(
... obs_op=vardax.MaskedIdentity(),
... prior_mean=jnp.zeros((1, 3)),
... prior_cov_op=eye,
... obs_cov_op=eye,
... )
>>> batch = vardax.Batch1D(input=jnp.ones((1, 1, 3)), mask=jnp.ones((1, 1, 3)))
>>> xa = three(batch)
>>> xa.shape
(1, 1, 3)
>>> bool(jnp.allclose(xa, 0.5, atol=1e-3))
True
A nonlinear \(H(x) = x^2\) through
CallableObs. Where the mask is zero the
analysis falls back to the background, and analyse also
returns the minimiser diagnostics:
>>> sq = vardax.ThreeDVar(
... obs_op=vardax.CallableObs(fn=lambda x: x**2),
... prior_mean=jnp.ones((1, 3)),
... prior_cov_op=eye,
... obs_cov_op=eye,
... )
>>> half = vardax.Batch1D(
... input=jnp.full((1, 1, 3), 4.0),
... mask=jnp.array([[[1.0, 0.0, 0.0]]]),
... )
>>> xa, result = sq.analyse(half)
>>> xa.shape, result.cost.shape
((1, 1, 3), (1,))
>>> bool(jnp.allclose(xa[0, 0, 1:], 1.0, atol=1e-3))
True
Composition with a posterior adapter: the default
LaplaceCovariance returns
\(P^* = (I + I)^{-1}\) for the first example's model.
>>> post = three.posterior(batch)
>>> bool(jnp.allclose(post.cov.mv(jnp.ones((1, 3))), 0.5, atol=1e-2))
True
Source code in src/vardax/_src/models/threedvar.py
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 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 | |
analyse
¶
analyse(
batch: Batch1D | PointObsBatch,
) -> tuple[Float[Array, " batch *state"], MinimiseResult]
The analysis, as __call__, with the minimiser's diagnostics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
batch
|
Batch1D | PointObsBatch
|
A gridded |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, ' batch *state']
|
|
MinimiseResult
|
|
tuple[Float[Array, ' batch *state'], MinimiseResult]
|
leading sample axis: |
tuple[Float[Array, ' batch *state'], MinimiseResult]
|
\(\lVert \nabla J \rVert_2\) at the minimum, |
tuple[Float[Array, ' batch *state'], MinimiseResult]
|
for a custom minimiser) and |
tuple[Float[Array, ' batch *state'], MinimiseResult]
|
(the state, or \(\theta\) under a |
Raises:
| Type | Description |
|---|---|
InputTypeError
|
For a |
MissingInputError
|
If |
Source code in src/vardax/_src/models/threedvar.py
posterior
¶
posterior(batch: Batch1D, adapter: Any = None) -> Any
Analyse one window and return its Posterior.
Runs the analysis, then adapter(analysis, self, batch); the
default adapter is LaplaceCovariance
with this model's \(B\) and \(R\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
batch
|
Batch1D
|
A |
required |
adapter
|
Any
|
Optional |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The window's posterior: mean and lazy covariance. |
Raises:
| Type | Description |
|---|---|
ShapeError
|
If |
UnsupportedInputError
|
For a point batch. |
Source code in src/vardax/_src/models/threedvar.py
as_analysis_step
¶
Adapt to pipekit_cycle.AnalysisStep (Decision D8).
Returns:
| Type | Description |
|---|---|
_ThreeDVarAnalysisStep
|
An analysis step that minimises the cost around a supplied |
_ThreeDVarAnalysisStep
|
forecast, treating |
Source code in src/vardax/_src/models/threedvar.py
StrongFourDVar
¶
Bases: Module
Strong-constraint 4DVar.
Algorithm
- For each window (
jax.vmap), start from the background \(x_b\) (or the parameterization's initial control). - Roll the forward model out from \(x_0\) for one step fewer than
the window has times, with optional
jax.checkpointrematerialisation. - Sum the masked, \(R^{-1}\)-weighted observation misfit over every
time of the trajectory; a point batch uses the \(\sigma_i\)-weighted
cost at each
time_indexinstead. - Add the background term \(\tfrac12 \delta x_0^\top B^{-1} \delta x_0\) (or the parameterization penalty).
- Minimise over \(x_0\) with
minimiser, differentiating through the rollout by reverse-mode autodiff.
Attributes:
| Name | Type | Description |
|---|---|---|
forward |
Any
|
|
obs_op |
Any
|
Observation operator. |
prior_mean |
Float[Array, ' N']
|
Background \(x_b\) — the initial state vector at \(t=0\). |
prior_cov_op |
AbstractLinearOperator
|
\(B\). |
obs_cov_op |
AbstractLinearOperator | None
|
\(R\). |
minimiser |
AbstractMinimiser | Minimiser
|
|
minimiser_adjoint |
AbstractAdjoint
|
|
forward_adjoint |
Any
|
|
max_steps |
int
|
Iteration cap on the outer solver when |
cov_solver |
AbstractLinearSolver
|
|
parameterization |
StateParameterization | None
|
Optional
|
checkpoint |
bool | int
|
Rematerialise the rollout in reverse mode. |
Examples:
Trivial dynamics (\(M_t(x) = x\)) and a single timestep reduce strong 4DVar to 3DVar, so with \(B = R = I\) the analysis is \(x_0^* = y / 2\).
>>> import jax, jax.numpy as jnp, lineax as lx, vardax
>>> class Identity:
... dt = 1.0
...
... def step(self, x, dt):
... return x
>>> eye = lx.IdentityLinearOperator(jax.ShapeDtypeStruct((3,), jnp.float32))
>>> strong = vardax.StrongFourDVar(
... forward=Identity(),
... obs_op=vardax.MaskedIdentity(),
... prior_mean=jnp.zeros(3),
... prior_cov_op=eye,
... obs_cov_op=eye,
... )
>>> batch = vardax.Batch1D(input=jnp.ones((1, 1, 3)), mask=jnp.ones((1, 1, 3)))
>>> strong(batch).shape
(1, 3)
A decaying forward model \(M(x) = x / 2\) and observations at \(t = 0\) and \(t = 1\) both inform \(x_0\): with \(x_b = 0\) the analysis is \(x_0^* = (1 + 1/2) / (1 + 1 + 1/4) = 2/3\).
>>> class Decay:
... dt = 1.0
...
... def step(self, x, dt):
... return 0.5 * x
>>> window = vardax.Batch1D(input=jnp.ones((1, 2, 3)), mask=jnp.ones((1, 2, 3)))
>>> decay = vardax.StrongFourDVar(
... forward=Decay(),
... obs_op=vardax.MaskedIdentity(),
... prior_mean=jnp.zeros(3),
... prior_cov_op=eye,
... obs_cov_op=eye,
... )
>>> bool(jnp.allclose(decay(window), 2 / 3, atol=1e-3))
True
checkpoint=True rematerialises the rollout in reverse mode and
gives the same analysis; the Laplace posterior of the window is
\(P^* = 1 / 2.25\):
>>> remat = vardax.StrongFourDVar(
... forward=Decay(),
... obs_op=vardax.MaskedIdentity(),
... prior_mean=jnp.zeros(3),
... prior_cov_op=eye,
... obs_cov_op=eye,
... checkpoint=True,
... )
>>> bool(jnp.allclose(remat(window), decay(window), atol=1e-4))
True
>>> post = remat.posterior(window)
>>> bool(jnp.allclose(post.cov.mv(jnp.ones(3)), 1 / 2.25, atol=1e-3))
True
A forward without with_adjoint cannot take a forward_adjoint:
>>> vardax.StrongFourDVar(
... forward=Decay(),
... obs_op=vardax.MaskedIdentity(),
... prior_mean=jnp.zeros(3),
... prior_cov_op=eye,
... obs_cov_op=eye,
... forward_adjoint=object(),
... )
Traceback (most recent call last):
...
UnsupportedInputError: forward_adjoint was set but Decay has no ...
Source code in src/vardax/_src/models/strong_fourdvar.py
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 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 | |
posterior
¶
posterior(batch: Batch1D, adapter: Any = None) -> Any
Analyse one window and return its Posterior.
Runs the analysis, then adapter(analysis, self, batch); the
default adapter is LaplaceCovariance
with this model's \(B\) and \(R\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
batch
|
Batch1D
|
A |
required |
adapter
|
Any
|
Optional |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The window's posterior: mean and lazy covariance. |
Raises:
| Type | Description |
|---|---|
ShapeError
|
If |
UnsupportedInputError
|
For a point batch. |
Source code in src/vardax/_src/models/strong_fourdvar.py
as_analysis_step
¶
Adapt to pipekit_cycle.AnalysisStep.
Returns:
| Type | Description |
|---|---|
_StrongFourDVarAnalysisStep
|
An analysis step that treats non-finite observations as |
_StrongFourDVarAnalysisStep
|
missing and returns the analysed initial state. |
Source code in src/vardax/_src/models/strong_fourdvar.py
WeakFourDVar
¶
Bases: Module
Weak-constraint 4DVar with augmented control vector.
Algorithm
- Start from the background \(x_0 = x_b\) and zero model errors.
- Roll the free model out with the forcing \(\eta_t\) added after every step.
- Sum the masked, \(R^{-1}\)-weighted observation misfit over the trajectory, the \(Q^{-1}\)-weighted model-error cost over the \(T\) steps, and the background term (or the parameterization penalty).
- Minimise jointly over \((x_0, \eta)\) with
minimiserand return the pair.
Attributes:
| Name | Type | Description |
|---|---|---|
forward |
Any
|
|
obs_op |
Any
|
Observation operator. |
prior_mean |
Float[Array, ' N']
|
Background \(x_b\) — the initial state. |
prior_cov_op |
AbstractLinearOperator
|
\(B\). |
obs_cov_op |
AbstractLinearOperator | None
|
\(R\). |
model_err_cov_op |
AbstractLinearOperator
|
\(Q\) — covariance of the per-step model error \(\eta_t\). Defaults to identity scaled by a small variance if not supplied (effectively strong-constraint with a tiny relaxation). |
minimiser |
AbstractMinimiser | Minimiser
|
As in |
minimiser_adjoint |
AbstractAdjoint
|
As in |
max_steps |
int
|
As in |
cov_solver |
AbstractLinearSolver
|
|
parameterization |
StateParameterization | None
|
Optional
|
forward_adjoint |
Any
|
As in |
checkpoint |
bool | int
|
As in |
Examples:
Trivial dynamics and a single timestep: no model-error steps remain (\(T = 0\)) and the analysis initial state reduces to the 3DVar / BLUE answer \(x_0^* = y / 2\) for \(B = R = I\).
>>> import jax, jax.numpy as jnp, lineax as lx, vardax
>>> class Identity:
... dt = 1.0
...
... def step(self, x, dt):
... return x
>>> eye = lx.IdentityLinearOperator(jax.ShapeDtypeStruct((3,), jnp.float32))
>>> weak = vardax.WeakFourDVar(
... forward=Identity(),
... obs_op=vardax.MaskedIdentity(),
... prior_mean=jnp.zeros(3),
... prior_cov_op=eye,
... obs_cov_op=eye,
... model_err_cov_op=eye,
... )
>>> batch = vardax.Batch1D(input=jnp.ones((1, 1, 3)), mask=jnp.ones((1, 1, 3)))
>>> x0_star, eta_star = weak(batch)
>>> x0_star.shape, eta_star.shape
((1, 3), (1, 0, 3))
A two-time window has one model-error step. With a tiny model-error covariance the free model is almost exact and the analysis approaches the strong-constraint \(x_0^* = 2/3\) for \(M(x) = x / 2\):
>>> class Decay:
... dt = 1.0
...
... def step(self, x, dt):
... return 0.5 * x
>>> tiny = lx.TaggedLinearOperator(
... lx.DiagonalLinearOperator(jnp.full(3, 1e-4)),
... lx.positive_semidefinite_tag,
... )
>>> weak = vardax.WeakFourDVar(
... forward=Decay(),
... obs_op=vardax.MaskedIdentity(),
... prior_mean=jnp.zeros(3),
... prior_cov_op=eye,
... obs_cov_op=eye,
... model_err_cov_op=tiny,
... )
>>> window = vardax.Batch1D(input=jnp.ones((1, 2, 3)), mask=jnp.ones((1, 2, 3)))
>>> x0_star, eta_star = weak(window)
>>> eta_star.shape
(1, 1, 3)
>>> bool(jnp.allclose(x0_star, 2 / 3, atol=5e-2))
True
Composed with the Laplace adapter, posterior returns the \(x_0\)
block of the inverse augmented precision:
Source code in src/vardax/_src/models/weak_fourdvar.py
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 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 | |
posterior
¶
posterior(batch: Batch1D, adapter: Any = None) -> Any
Analyse one window and return its Posterior.
Runs the analysis, then adapter(analysis, self, batch); the
default adapter is LaplaceCovariance
with this model's \(B\) and \(R\).
The covariance is that of \(x_0\): the \(x_0\) block of the inverse Gauss–Newton precision over \((x_0, \eta)\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
batch
|
Batch1D
|
A |
required |
adapter
|
Any
|
Optional |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The window's posterior: mean and lazy covariance. |
Raises:
| Type | Description |
|---|---|
ShapeError
|
If |
UnsupportedInputError
|
For a point batch, or for a model with
a |
Source code in src/vardax/_src/models/weak_fourdvar.py
as_analysis_step
¶
Adapt to pipekit_cycle.AnalysisStep.
Returns:
| Type | Description |
|---|---|
_WeakFourDVarAnalysisStep
|
An analysis step that treats non-finite observations as |
_WeakFourDVarAnalysisStep
|
missing and returns the analysed initial state only. |
Source code in src/vardax/_src/models/weak_fourdvar.py
IncrementalFourDVar
¶
Bases: Module
Operational incremental 4DVar (Decision D11).
Algorithm
- Start from \(x^{(0)} = x_b\) (the control \(\chi = 0\) under the control-variable transform).
- For each of
n_outeriterations, linearise the residual \(r\) at the current iterate withjax.linearizeandjax.vjp. - Solve the Gauss-Newton system
\((P + r'^\top R^{-1} r') \, \delta = -\nabla J\) with CG for at
most
n_innersteps, where \(P = B^{-1}\) in state space and \(P = I\) under the transform. - Update the iterate with \(\delta\) and map back to the state.
Functionally equivalent to StrongFourDVar
(same problem, same answer in the converged limit) but with a
specialised inner solver: Gauss-Newton outer iterations and CG
inner iterations on the linearised cost. Use this for production /
long-window 4DVar.
Attributes:
| Name | Type | Description |
|---|---|---|
forward |
Any
|
|
obs_op |
Any
|
Observation operator. |
prior_mean |
Float[Array, ' N']
|
Background \(x_b\) — the initial state. |
prior_cov_op |
AbstractLinearOperator
|
\(B\). |
obs_cov_op |
AbstractLinearOperator | None
|
\(R\). |
config |
IncrementalConfig
|
|
prior_sqrt_op |
AbstractLinearOperator | None
|
Optional square root \(S\) of \(B\) (\(S S^\top = B\)),
used by the control-variable transform: a Cholesky factor, a
structured operator's own square root, or a Lanczos
|
forward_adjoint |
Any
|
As in |
checkpoint |
bool | int
|
As in |
Examples:
Trivial dynamics (\(M_t(x) = x\)) and a single timestep reduce the problem to 3DVar, so with \(B = R = I\) the converged analysis is \(x_0^* = y / 2\).
>>> import jax, jax.numpy as jnp, lineax as lx, vardax
>>> class Identity:
... dt = 1.0
...
... def step(self, x, dt):
... return x
>>> eye = lx.IdentityLinearOperator(jax.ShapeDtypeStruct((3,), jnp.float32))
>>> inc = vardax.IncrementalFourDVar(
... forward=Identity(),
... obs_op=vardax.MaskedIdentity(),
... prior_mean=jnp.zeros(3),
... prior_cov_op=eye,
... obs_cov_op=eye,
... )
>>> batch = vardax.Batch1D(input=jnp.ones((1, 1, 3)), mask=jnp.ones((1, 1, 3)))
>>> xa = inc(batch)
>>> xa.shape
(1, 3)
>>> bool(jnp.allclose(xa, 0.5, atol=1e-2))
True
On a linear decaying model \(M(x) = x / 2\) with observations at \(t = 0, 1\) the Gauss-Newton step is exact, so one outer iteration reproduces the strong-constraint answer \(x_0^* = 2/3\):
>>> class Decay:
... dt = 1.0
...
... def step(self, x, dt):
... return 0.5 * x
>>> window = vardax.Batch1D(input=jnp.ones((1, 2, 3)), mask=jnp.ones((1, 2, 3)))
>>> one = vardax.IncrementalFourDVar(
... forward=Decay(),
... obs_op=vardax.MaskedIdentity(),
... prior_mean=jnp.zeros(3),
... prior_cov_op=eye,
... obs_cov_op=eye,
... config=vardax.IncrementalConfig(n_outer=1, cvt=False),
... )
>>> bool(jnp.allclose(one(window), 2 / 3, atol=1e-3))
True
Composed with the Laplace adapter (via posterior), the window's
covariance is \(P^* = 1 / 2.25\):
>>> post = one.posterior(window)
>>> bool(jnp.allclose(post.cov.mv(jnp.ones(3)), 1 / 2.25, atol=1e-3))
True
Source code in src/vardax/_src/models/incremental_fourdvar.py
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 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 | |
posterior
¶
posterior(batch: Batch1D, adapter: Any = None) -> Any
Analyse one window and return its Posterior.
Runs the analysis, then adapter(analysis, self, batch); the
default adapter is LaplaceCovariance
with this model's \(B\) and \(R\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
batch
|
Batch1D
|
A |
required |
adapter
|
Any
|
Optional |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The window's posterior: mean and lazy covariance. |
Raises:
| Type | Description |
|---|---|
ShapeError
|
If |
UnsupportedInputError
|
For a point batch. |
Source code in src/vardax/_src/models/incremental_fourdvar.py
as_analysis_step
¶
Adapt to pipekit_cycle.AnalysisStep.
Returns:
| Type | Description |
|---|---|
_IncrementalFourDVarAnalysisStep
|
An analysis step that treats non-finite observations as |
_IncrementalFourDVarAnalysisStep
|
missing and returns the analysed initial state. |
Source code in src/vardax/_src/models/incremental_fourdvar.py
IncrementalConfig
¶
Bases: Module
Configuration for IncrementalFourDVar.
A plain container of the outer/inner iteration budget and the control-variable-transform switch.
Attributes:
| Name | Type | Description |
|---|---|---|
n_outer |
int
|
Number of Gauss-Newton outer iterations (typical: 3). |
n_inner |
int
|
Max CG iterations per outer (typical: 20-50). The inner solve stops there whether or not it has met the tolerance (truncated CG, as in operational incremental 4DVar). |
cg_atol |
float
|
Inner CG absolute tolerance. |
cg_rtol |
float
|
Inner CG relative tolerance. |
cvt |
bool
|
Solve the inner system over the control variable \(\chi\) with
\(x_0 = x_b + B^{1/2} \chi\) (default). The background term is
\(\tfrac12 \lVert \chi \rVert^2\), the inner Hessian
\(I + B^{1/2\top} G'^\top R^{-1} G' B^{1/2}\) has eigenvalues
≥ 1, and \(B\) is never solved against. |
covariance_tol |
float
|
Absolute and relative tolerance of the CG solves
that apply \(R^{-1}\) (and \(B^{-1}\) when |
Examples:
The defaults are three outer and thirty inner iterations with the control-variable transform on:
>>> import vardax as vdx
>>> cfg = vdx.IncrementalConfig()
>>> cfg.n_outer, cfg.n_inner, cfg.cvt
(3, 30, True)
State-space solving (cvt=False) with a bigger budget:
Source code in src/vardax/_src/models/incremental_fourdvar.py
ReducedBasisFourDVar
¶
Bases: Module
Reduced-basis 4DVar.
Without a basis the control is \(x_0\) itself and the first term is \(\tfrac12 \|x_0 - x_b\|^2_{B^{-1}}\).
Algorithm
- Pick the control and its map to the initial state: \(x_0\) itself,
the coefficients \(X\) with \(x_0 = x_b + \Phi X\) (
basis), or \(\theta\) with \(x_0 = \psi(\theta)\) (parameterization). - Roll the forward model out from \(x_0\) for the window length.
- Add the prior term (\(B^{-1}\),
basis.prior_invor the parameterization penalty) to the masked observation misfit summed over the trajectory. Masked entries are marginalised out of \(R\), so a correlated \(R\) is not biased by the gaps. - Minimise the full nonlinear cost with
minimiser(no linearisation or inner loop) and decode the minimiser to \(x_0^*\).
Attributes:
| Name | Type | Description |
|---|---|---|
forward |
Any
|
|
obs_op |
Any
|
Observation operator (linear or nonlinear). Supplies
|
prior_mean |
Float[Array, ' N']
|
Background \(x_b\). |
prior_cov_op |
AbstractLinearOperator
|
\(B\). Used only when |
obs_cov_op |
AbstractLinearOperator | None
|
\(R\). |
basis |
Any
|
Optional |
parameterization |
StateParameterization | None
|
Optional
|
minimiser |
AbstractMinimiser | Minimiser
|
|
minimiser_adjoint |
AbstractAdjoint
|
|
max_steps |
int
|
Iteration cap on the outer solver when |
cov_solver |
AbstractLinearSolver
|
|
forward_adjoint |
Any
|
As in |
checkpoint |
bool | int
|
As in |
Examples:
Trivial dynamics, one timestep, \(B = R = I\) and no basis: the cost is that of 3DVar, so \(x_0^* = y / 2\).
>>> import jax, jax.numpy as jnp, lineax as lx, vardax
>>> class Identity:
... dt = 1.0
...
... def step(self, x, dt):
... return x
>>> eye = lx.IdentityLinearOperator(jax.ShapeDtypeStruct((4,), jnp.float32))
>>> common = dict(
... forward=Identity(),
... obs_op=vardax.MaskedIdentity(),
... prior_mean=jnp.zeros(4),
... prior_cov_op=eye,
... obs_cov_op=eye,
... )
>>> batch = vardax.Batch1D(input=jnp.ones((1, 1, 4)), mask=jnp.ones((1, 1, 4)))
>>> xa = vardax.ReducedBasisFourDVar(**common)(batch)
>>> xa.shape
(1, 4)
>>> bool(jnp.allclose(xa, 0.5, atol=1e-3))
True
With a basis the increment is confined to \(\mathrm{span}(\Phi)\). A single unit-variance constant mode gives \(X^* = \tfrac{\sum_i y_i}{1 + N}\) and \(x_0^* = X^* \mathbf{1}\):
>>> basis = vardax.linear_basis(jnp.ones((4, 1)))
>>> xa = vardax.ReducedBasisFourDVar(**common, basis=basis)(batch)
>>> bool(jnp.allclose(xa, 4.0 / 5.0, atol=1e-3))
True
Composed with a Fourier basis from fourier_basis,
the analysis is still a state-space array:
>>> fb = vardax.fourier_basis([jnp.linspace(0.0, 1.0, 4)], 2)
>>> xb = vardax.ReducedBasisFourDVar(**common, basis=fb)(batch)
>>> xb.shape
(1, 4)
basis and parameterization are exclusive:
>>> param = vardax.LinearParameterization(basis, jnp.zeros(4))
>>> vardax.ReducedBasisFourDVar(
... **common, basis=basis, parameterization=param
... )
Traceback (most recent call last):
...
ConfigurationError: Pass basis= or parameterization=, not both; ...
Source code in src/vardax/_src/models/reduced_basis_fourdvar.py
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 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 | |
posterior
¶
posterior(batch: Batch1D, adapter: Any = None) -> Any
Analyse one window and return its Posterior.
Runs the analysis, then adapter(analysis, self, batch); the
default adapter is LaplaceCovariance
with this model's \(B\) and \(R\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
batch
|
Batch1D
|
A |
required |
adapter
|
Any
|
Optional |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
The window's posterior: mean and lazy covariance. |
Raises:
| Type | Description |
|---|---|
ShapeError
|
If |
UnsupportedInputError
|
For a point batch, or for a model with
|
Source code in src/vardax/_src/models/reduced_basis_fourdvar.py
as_analysis_step
¶
Adapt to pipekit_cycle.AnalysisStep (Decision D8).
Returns:
| Type | Description |
|---|---|
_ReducedBasisFourDVarAnalysisStep
|
An analysis step that accepts single arrays or windowed lists, |
_ReducedBasisFourDVarAnalysisStep
|
swaps in the supplied forecast and operators, and returns the |
_ReducedBasisFourDVarAnalysisStep
|
analysis (rolled to the window end for a smoother). |
Source code in src/vardax/_src/models/reduced_basis_fourdvar.py
Neural-field 4DVar¶
SpaceTimeFieldVar fits one continuous
field \(u(t, s) = f_\theta(t, s)\) over the whole window to point
observations, with an optional PDE residual as a soft constraint and
weight decay in place of \(B\); no rollout. It returns a
SpaceTimeFieldResult, queryable anywhere, and ensemble(...) gives a
deep-ensemble spread.
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:
SpaceTimeFieldVar
¶
Bases: Module
Neural-field 4D-Var: fit one continuous field \(u(t, s) = f_\theta(t, s)\).
Minimises, over the network weights \(\theta\),
with the observations of a PointObsBatch
(\(m_k = 0\) on padding rows, which contribute nothing, NaN
included), the physics term of an optional
ResidualPenalty and weight decay \(\alpha\).
There is no background covariance and no rollout: the whole window
is the field, which can be queried and differentiated anywhere
(Oh 2025).
Coordinates put time first: the field maps a point
(t, *s) of shape (1 + D,) to (K,) (or a scalar), the
observation coordinates are (N_obs, 1 + D) in the same order, and
so are the collocation points, as the residuals of
vardax.examples expect. The observed quantity is the field's single
output (FieldPointObs); a multi-channel field needs its own wrapper.
Scale of \(\lambda\): the observation term is a sum over the \(N_\text{obs}\) valid observations, each weighted by \(1/\sigma_k^2\), while the residual term is a mean over the collocation points. A unit change of the field everywhere moves the first by about \(N_\text{obs}/(2\sigma^2)\) and the second by about \(\lambda/2\) times the squared residual it causes, so \(\lambda\) trades off against \(N_\text{obs}/(2\sigma^2)\) and must grow with it to keep the same balance (the advection test uses \(\lambda = 10^3\) for 100 observations with \(\sigma = 0.05\), where \(N_\text{obs}/(2\sigma^2) = 2 \times 10^4\)).
The fit runs in one eqx.filter_jit-compiled function of the model,
the observations and the key, so repeated analyses with new
observations (or a new weight_decay or penalty weight, both array
leaves) of the same shapes do not recompile.
The penalty's collocation points are fixed during a fit: a
ResidualPenalty with a sampler draws them once, from key,
at the start of the analysis, so the cost the minimiser sees is
deterministic (its patience test, best-iterate tracking and implicit
adjoint all assume that). Draw more points, or refit with new keys,
for a better estimate of the expected residual.
Algorithm
- Split the field's leaves into the control \(\theta\) (by
filter_spec) and the frozen rest. - Draw the penalty's collocation points once from
keywhen it has asampler. - Evaluate the field at the observation coordinates and form the masked, \(\sigma\)-weighted observation cost.
- Add the weight decay \(\tfrac{\alpha}{2}\lVert\theta\rVert^2\) and, if present, the mean squared PDE residual at the collocation points.
- Minimise over \(\theta\) with
minimiserin one compiled function and return the fitted field with the cost, gradient norm and step count.
Attributes:
| Name | Type | Description |
|---|---|---|
field |
Any
|
The initial network, a single-example |
residual |
ResidualPenalty | None
|
Optional |
weight_decay |
Float[Array, '']
|
\(\alpha \ge 0\), stored as a 0-d array (a leaf: it can be changed without recompiling and differentiated). |
minimiser |
OptaxMinimiser | OptimistixMinimiser
|
An |
filter_spec |
Any
|
Which leaves of |
t0 |
float
|
Start of the window, \(t_0\) (default |
Examples:
A field linear in fixed features, \(f_w(t, x) = w_0 + w_1 x\), with no physics is Bayesian linear regression: the analysis is the ridge solution, here \(w^\star = (0, 1)\) to within the small weight decay.
>>> import equinox as eqx, jax, jax.numpy as jnp, vardax
>>> class Line(eqx.Module):
... w: jax.Array
...
... def __call__(self, s):
... return (self.w[0] + self.w[1] * s[1])[None]
>>> obs = vardax.PointObsBatch(
... coords=jnp.array([[0.0, -1.0], [0.5, 0.0], [1.0, 1.0], [0.0, 9.0]]),
... values=jnp.array([-1.0, 0.0, 1.0, jnp.nan]), # last row: padding
... sigma=jnp.ones(4),
... valid=jnp.array([True, True, True, False]),
... )
>>> ana = vardax.SpaceTimeFieldVar(
... field=Line(jnp.zeros(2)),
... weight_decay=1e-6,
... minimiser=vardax.OptimistixMinimiser(),
... )
>>> result = ana(obs)
>>> result.initial_condition(jnp.array([[0.5], [2.0]])).round(3)
Array([0.5, 2. ], dtype=float32)
A deep ensemble refits from random initialisations; the result's array leaves carry a member axis:
>>> def init(key):
... return Line(jax.random.normal(key, (2,)))
>>> members = ana.ensemble(obs, init, 3, key=jax.random.PRNGKey(0))
>>> members.cost.shape
(3,)
The cycle adapter returns the analysis on a grid directly:
A negative weight decay is rejected:
>>> vardax.SpaceTimeFieldVar(
... field=Line(jnp.zeros(2)), weight_decay=-1.0
... )
Traceback (most recent call last):
...
ConfigurationError: weight_decay must be >= 0, got -1.0.
References
Oh, Y. (2025). On the effect of neural field reparameterization for 4DVAR. arXiv:2509.21751.
Source code in src/vardax/_src/models/spacetime_field.py
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 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 | |
ensemble
¶
ensemble(
obs: PointObsBatch,
field_init: Any,
n_members: int,
*,
key: PRNGKeyArray,
) -> SpaceTimeFieldResult
A deep ensemble: refit from n_members random initialisations.
Each member re-initialises the field with field_init and draws
its own collocation points (when the penalty has a sampler), then
fits as __call__ does; the members run under eqx.filter_vmap.
The spread of the members is a heuristic uncertainty: small where
the observations pin the field down, large away from them. A
\(\theta\)-space Laplace push-forward \(J \Sigma_\theta J^\top\)
(\(J = \partial u / \partial\theta\)) is not implemented.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obs
|
PointObsBatch
|
The observations, one sample. |
required |
field_init
|
Any
|
|
required |
n_members
|
int
|
Number of members, at least 1. |
required |
key
|
PRNGKeyArray
|
Split into one initialisation key and one collocation key per member. |
required |
Returns:
| Type | Description |
|---|---|
SpaceTimeFieldResult
|
A |
SpaceTimeFieldResult
|
array leaves carry a leading axis of size |
Raises:
| Type | Description |
|---|---|
ConfigurationError
|
If |
Source code in src/vardax/_src/models/spacetime_field.py
as_analysis_step
¶
as_analysis_step(
grid: Float[Array, "N D"] | None = None,
*,
key: PRNGKeyArray | None = None,
) -> _SpaceTimeFieldAnalysisStep
Adapt to pipekit_cycle.AnalysisStep.
The step fits the field to the cycle's observations. With a
grid it returns the analysis at the window start on it,
\(u(t_0, s_j)\) (SpaceTimeFieldResult.initial_condition), the
state a ForwardModel steps on; without one it returns the
SpaceTimeFieldResult itself.
The collocation points are drawn once, here, from key,
and reused by every cycle. pipekit_cycle.DACycle passes no
time: the times are the first column of each cycle's
PointObsBatch, so they must be window-relative, in the
same window \([t_0, t_1]\) every cycle as the collocation points.
The step checks that (when the times are concrete): \(t_0\) and the
valid observation times must lie in the collocation points' time
range, widened by 10 % on each side for the gaps a random draw
leaves at the ends.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grid
|
Float[Array, 'N D'] | None
|
Spatial points \(s_j\) of the returned state, one per row,
or |
None
|
key
|
PRNGKeyArray | None
|
Collocation key; required when the penalty has a
|
None
|
Returns:
| Type | Description |
|---|---|
_SpaceTimeFieldAnalysisStep
|
The adapter. |
Raises:
| Type | Description |
|---|---|
GridError
|
If |
MissingInputError
|
If the penalty has a sampler and |
ConfigurationError
|
If \(t_0\) is outside the collocation window. |
Source code in src/vardax/_src/models/spacetime_field.py
SpaceTimeFieldResult
¶
Bases: Module
A fitted space-time field and its minimisation diagnostics.
Attributes:
| Name | Type | Description |
|---|---|---|
field |
Any
|
The analysis \(f_{\theta^\star}\), a single-example callable
of one space-time point |
t0 |
float
|
Start of the window, \(t_0\), used by |
cost |
Float[Array, '']
|
\(J(\theta^\star)\). |
grad_norm |
Float[Array, '']
|
\(\lVert\nabla J(\theta^\star)\rVert_2\); a first-order optimiser stopped by its step budget leaves it non-zero. |
num_steps |
Int[Array, '']
|
Optimiser steps taken. |
An ensemble returns one result
whose array leaves carry a leading member axis; evaluate it with
eqx.filter_vmap(lambda r: r.initial_condition(grid))(result).
Examples:
Fit a line to three points, then query the fitted field:
>>> import equinox as eqx, jax, jax.numpy as jnp, vardax
>>> class Line(eqx.Module):
... w: jax.Array
...
... def __call__(self, s):
... return (self.w[0] + self.w[1] * s[1])[None]
>>> obs = vardax.PointObsBatch(
... coords=jnp.array([[0.0, -1.0], [0.5, 0.0], [1.0, 1.0]]),
... values=jnp.array([-1.0, 0.0, 1.0]),
... sigma=jnp.ones(3),
... valid=jnp.ones(3, bool),
... )
>>> ana = vardax.SpaceTimeFieldVar(
... field=Line(jnp.zeros(2)),
... weight_decay=1e-6,
... minimiser=vardax.OptimistixMinimiser(),
... )
>>> result = ana(obs)
>>> result.evaluate(jnp.array([[0.0, 2.0], [0.0, 3.0]])).round(2)
Array([2., 3.], dtype=float32)
>>> bool(result.grad_norm < 1e-3)
True
The analysis at the window start on a spatial grid is the initial condition for a forecast, and a non-2-D grid is rejected:
>>> result.initial_condition(jnp.array([[0.5], [2.0]])).shape
(2,)
>>> result.initial_condition(
... jnp.array([0.5, 2.0])
... )
Traceback (most recent call last):
...
GridError: grid must be 2-D (N, D); got shape (2,).
Source code in src/vardax/_src/models/spacetime_field.py
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 | |
evaluate
¶
Evaluate the field at space-time points.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coords
|
Float[Array, 'N P']
|
Space-time points |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'N ...']
|
One value per point for a single-channel field, else one |
Float[Array, 'N ...']
|
row of |
Source code in src/vardax/_src/models/spacetime_field.py
initial_condition
¶
The analysis at the window start, \(u(t_0, s_j)\), on a grid.
This is the initial state for a forecast with a ForwardModel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
grid
|
Real[Array, 'N D']
|
Spatial points \(s_j\), one per row. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'N ...']
|
One value per grid point for a single-channel field, else one |
Float[Array, 'N ...']
|
row of |
Raises:
| Type | Description |
|---|---|
GridError
|
If |
Source code in src/vardax/_src/models/spacetime_field.py
Learned solvers — 4DVarNet¶
End-to-end-trainable 4DVar: the variational cost is kept explicit, but the
inner-loop descent direction is produced by a ConvLSTM
gradient modulator instead of a hand-tuned optimiser. The 1D
variant operates on Batch1D (e.g. Lorenz-96 trajectories); the 2D variant
on Batch2D / Batch2DMultivar fields (e.g. SSH reconstruction). The
inner-loop iteration functions live on the
Costs, Priors & Solvers page; training utilities on
Training & Adjoints.
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:
FourDVarNet1D
¶
Bases: Module
End-to-end 4DVarNet model for 1-D spatiotemporal reconstruction.
Minimises the variational cost
using n_solver_steps learned gradient steps modulated by a ConvLSTM,
with the differentiation strategy selected by solver_adjoint.
Algorithm
- Start from the masked observations, \(x_0 = m \odot y\), with a zero ConvLSTM state.
- For each of
n_solver_stepsiterations, compute \(\nabla J(x_k)\), pass it through the ConvLSTM gradient modulator \(\mathcal{G}\) and update \(x_{k+1} = x_k - \alpha\, \mathcal{G}(\cdot)\). - Return \(x_K\). A
KStepAdjointruns the one-step solver and anImplicitAdjointthe fixed-point projection instead; any other adjoint unrolls with ordinary backprop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state_dim
|
int
|
Spatial size \(N\) of the state. |
required |
n_time
|
int
|
Number of time steps \(T\) in a window. |
required |
latent_dim
|
int
|
Dimensionality of the prior's latent code. |
32
|
hidden_dim
|
int
|
Number of hidden channels of the ConvLSTM. |
64
|
n_solver_steps
|
int
|
Number of solver iterations to unroll. |
15
|
alpha
|
float
|
Gradient step-size \(\alpha\). |
0.2
|
prior_weight
|
float
|
Weight \(\lambda\) for the prior cost term. |
1.0
|
solver_adjoint
|
AbstractAdjoint | None
|
|
None
|
key
|
PRNGKeyArray
|
PRNG key for the prior and gradient-modulator weights. |
required |
Attributes:
| Name | Type | Description |
|---|---|---|
prior |
BilinAEPrior1D
|
BilinAEPrior1D learned prior. |
grad_mod |
ConvLSTMGradMod1D
|
ConvLSTMGradMod1D learned gradient modulator. |
Examples:
A tiny model reconstructs a batch with the shape of its input:
>>> import jax
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> model = vdx.FourDVarNet1D(
... state_dim=4,
... n_time=3,
... latent_dim=2,
... hidden_dim=3,
... n_solver_steps=2,
... key=jax.random.PRNGKey(0),
... )
>>> batch = vdx.Batch1D(input=jnp.ones((2, 3, 4)), mask=jnp.ones((2, 3, 4)))
>>> model(batch).shape
(2, 3, 4)
Selecting the one-step adjoint changes only the gradient path, not the forward value:
>>> fast = vdx.FourDVarNet1D(
... state_dim=4,
... n_time=3,
... latent_dim=2,
... hidden_dim=3,
... n_solver_steps=2,
... solver_adjoint=vdx.OneStepAdjoint(),
... key=jax.random.PRNGKey(0),
... )
>>> bool(jnp.allclose(fast(batch), model(batch), atol=1e-5))
True
Compose it with train_step for one optimiser
update:
>>> import optax
>>> target = vdx.Batch1D(
... input=batch.input, mask=batch.mask, target=jnp.zeros((2, 3, 4))
... )
>>> optimizer = optax.adam(1e-3)
>>> opt_state = optimizer.init(model)
>>> model, opt_state, loss = vdx.train_step(model, target, optimizer, opt_state)
>>> loss.shape
()
Source code in src/vardax/_src/model.py
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 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 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 | |
as_analysis_step
¶
Adapt to pipekit_cycle.AnalysisStep (Decision D8).
Returns:
| Type | Description |
|---|---|
_FourDVarNet1DAnalysisStep
|
A callable |
_FourDVarNet1DAnalysisStep
|
masks the non-finite entries of |
Examples:
Non-finite observations become unobserved entries:
>>> import jax
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> model = vdx.FourDVarNet1D(
... state_dim=4,
... n_time=3,
... latent_dim=2,
... hidden_dim=3,
... n_solver_steps=1,
... key=jax.random.PRNGKey(0),
... )
>>> step = model.as_analysis_step()
>>> obs = jnp.ones((1, 3, 4)).at[0, 0, 0].set(jnp.nan)
>>> step(obs, obs, obs_op=None, obs_err_cov=None).shape
(1, 3, 4)
Source code in src/vardax/_src/model.py
FourDVarNet2D
¶
Bases: Module
End-to-end 4DVarNet model for 2-D spatiotemporal reconstruction.
Minimises the same variational cost as FourDVarNet1D,
over fields with two spatial axes.
Algorithm
- Start from the masked observations, \(x_0 = m \odot y\), with a zero ConvLSTM state.
- For each of
n_solver_stepsiterations, compute \(\nabla J(x_k)\), pass it through the ConvLSTM gradient modulator \(\mathcal{G}\) and update \(x_{k+1} = x_k - \alpha\, \mathcal{G}(\cdot)\). - Return \(x_K\). A
KStepAdjointruns the one-step solver; any other supported adjoint unrolls with ordinary backprop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_time
|
int
|
Number of time steps \(T\) in a window. |
required |
height
|
int
|
Spatial height \(H\). |
required |
width
|
int
|
Spatial width \(W\). |
required |
latent_dim
|
int
|
Dimensionality of the prior's latent code. |
64
|
hidden_dim
|
int
|
Number of hidden channels of the ConvLSTM. |
64
|
n_solver_steps
|
int
|
Number of solver iterations to unroll. |
15
|
alpha
|
float
|
Gradient step-size \(\alpha\). |
0.2
|
prior_weight
|
float
|
Weight \(\lambda\) for the prior cost term. |
1.0
|
solver_adjoint
|
AbstractAdjoint | None
|
|
None
|
key
|
PRNGKeyArray
|
PRNG key for the prior and gradient-modulator weights. |
required |
Attributes:
| Name | Type | Description |
|---|---|---|
prior |
BilinAEPrior2D
|
BilinAEPrior2D learned prior. |
grad_mod |
ConvLSTMGradMod2D
|
ConvLSTMGradMod2D learned gradient modulator. |
Examples:
A tiny model reconstructs a batch with the shape of its input:
>>> import jax
>>> import jax.numpy as jnp
>>> import vardax as vdx
>>> model = vdx.FourDVarNet2D(
... n_time=3,
... height=4,
... width=5,
... latent_dim=2,
... hidden_dim=3,
... n_solver_steps=2,
... key=jax.random.PRNGKey(0),
... )
>>> batch = vdx.Batch2D(
... input=jnp.ones((1, 3, 4, 5)), mask=jnp.ones((1, 3, 4, 5))
... )
>>> model(batch).shape
(1, 3, 4, 5)
Selecting the one-step adjoint changes only the gradient path, not the forward value:
>>> fast = vdx.FourDVarNet2D(
... n_time=3,
... height=4,
... width=5,
... latent_dim=2,
... hidden_dim=3,
... n_solver_steps=2,
... solver_adjoint=vdx.OneStepAdjoint(),
... key=jax.random.PRNGKey(0),
... )
>>> bool(jnp.allclose(fast(batch), model(batch), atol=1e-5))
True
Source code in src/vardax/_src/model.py
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 | |
as_analysis_step
¶
Adapt to pipekit_cycle.AnalysisStep (Decision D8).
Returns:
| Type | Description |
|---|---|
_FourDVarNet2DAnalysisStep
|
A callable |
_FourDVarNet2DAnalysisStep
|
masks the non-finite entries of |