Differentiable training¶
Train dynamics (or observation operators) through the filter: run a full
assimilation over T windows, score it with a log-likelihood or
truth-matching loss, and differentiate with respect to the model
parameters. Two gradient strategies are provided:
differentiable_assimilate— a fixed-shapejax.lax.scanover the forecast–analysis cycle with the full forward+backward tape (optionallyjax.checkpoint-ed forO(√T)memory). Use when you want exact gradients, including the cross-time terms that flow through the analysis updates.road_enkf_loss_and_grad/road_enkf_grad_step— the ROAD-EnKF local-gradient strategy, withjax.lax.stop_gradientbetween cycles. Backward-pass memory isO(Nₑ · Nₓ)independent ofT, at the cost of dropping cross-time gradient terms.
Both refuse stochastic filters (StochasticEnKF,
ETKF_Livings) and the stochastic
AdditiveInflator at call time — randomness in the analysis breaks the
deterministic tape these strategies rely on. The deterministic
ETKF analysis core (rank-N_y QR + symmetric eigh) is what
keeps the exact-gradient path differentiability-safe.
Full-tape assimilation¶
filterax — differentiable ensemble Kalman filters and processes for JAX.
differentiable_assimilate(filter_: AbstractSequentialFilter, dynamics: AbstractDynamics, obs_op: AbstractObsOperator, init_ensemble: Float[Array, 'N_e N_x'], observations: Float[Array, 'T N_y'], obs_times: Float[Array, ' T'], obs_noise: lx.AbstractLinearOperator, *, inflator: AbstractInflator | None = None, t0: float | Float[Array, ''] = 0.0, adjoint: Any | None = None, checkpoint: bool = False, **analysis_extra: Any) -> AssimilationResult
¶
Scan-based assimilation loop suitable for jax.grad / jax.jit.
Drop-in replacement for the L2 model.assimilate(...) Python loop
when the caller needs a single fused XLA While (large T,
short compile, optional gradient checkpointing). Observations and
timestamps are stacked along a leading time axis so the loop body
has a static shape.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filter_
|
AbstractSequentialFilter
|
Deterministic L1 filter ( |
required |
dynamics
|
AbstractDynamics
|
Forward model applied with :func: |
required |
obs_op
|
AbstractObsOperator
|
Observation operator |
required |
init_ensemble
|
Float[Array, 'N_e N_x']
|
Prior ensemble |
required |
observations
|
Float[Array, 'T N_y']
|
Stacked obs |
required |
obs_times
|
Float[Array, ' T']
|
Stacked timestamps |
required |
obs_noise
|
AbstractLinearOperator
|
Observation error covariance |
required |
inflator
|
AbstractInflator | None
|
Optional deterministic inflator
( |
None
|
t0
|
float | Float[Array, '']
|
Starting time used for the first forecast window. Default 0. |
0.0
|
adjoint
|
Any | None
|
Gradient strategy for the scan — a
:class: |
None
|
checkpoint
|
bool
|
Deprecated — use
|
False
|
**analysis_extra
|
Any
|
Forwarded to |
{}
|
Returns:
| Type | Description |
|---|---|
AssimilationResult
|
class: |
AssimilationResult
|
|
AssimilationResult
|
because every deterministic filter returns one. |
Examples:
>>> import jax.numpy as jnp
>>> import lineax as lx
>>> import filterax as flx
>>> particles = jnp.array([[0.0, 0.0], [1.0, 1.0], [2.0, 0.0]])
>>> observations = jnp.array([[0.5], [0.4]]) # (T, N_y) = (2, 1)
>>> obs_times = jnp.array([1.0, 2.0])
>>> R = lx.DiagonalLinearOperator(0.1 * jnp.ones(1))
>>> result = flx.differentiable_assimilate(
... flx.filters.ETKF(),
... lambda x, t0, t1: x, # identity dynamics
... lambda x: x[:1], # observe the first component
... particles,
... observations,
... obs_times,
... R,
... )
>>> result.analysis_history.shape, result.log_likelihoods.shape
((2, 3, 2), (2,))
Source code in src/filterax/_train/_differentiable.py
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 | |
Adjoint strategies¶
Declarative gradient policies for the scan, in the shared
pipekit/diffrax vocabulary. DirectAdjoint is exact with the full
tape; RecursiveCheckpointAdjoint is exact with recomputation;
TruncatedAdjoint(k) cuts gradient flow more than k cycles back —
biased, O(1) backward memory in T, and tolerant of chaotic gradient
explosion. Structurally identical pipekit_cycle.adjoints specs are
accepted interchangeably.
Differentiable data-assimilation surface.
Gradient strategies for training through the filter:
- :func:
differentiable_assimilate— fixed-shape :func:jax.lax.scanwhose gradient strategy is selected withadjoint=: :class:DirectAdjoint(exact, full tape), :class:RecursiveCheckpointAdjoint(exact,O(√T)memory via recomputation), or :class:TruncatedAdjoint(biased: trailing-kcycles only —O(1)memory inTand tolerant of chaotic gradient explosion). The names match the shared pipekit / diffrax vocabulary;pipekit_cycle.adjointsspecs are accepted interchangeably. - :func:
road_enkf_loss_and_grad/ :func:road_enkf_grad_step— ROAD-EnKF local-gradient strategy with :func:jax.lax.stop_gradientbetween cycles. Backward-pass memoryO(Nₑ · Nₓ)independent ofT; drops cross-time gradient terms.
Both refuse stochastic filters (StochasticEnKF, ETKF_Livings)
and the stochastic AdditiveInflator at call time — see
design_docs/features/differentiable_da.md §5.2 / §5.5.
DirectAdjoint
dataclass
¶
RecursiveCheckpointAdjoint
dataclass
¶
Exact gradients via :func:jax.checkpoint recomputation.
Attributes:
| Name | Type | Description |
|---|---|---|
checkpoints |
int | None
|
Reserved for schedule control; the current
implementation uses :func: |
Source code in src/filterax/_train/_adjoints.py
TruncatedAdjoint
dataclass
¶
Gradients flow through the trailing k cycles only.
Cross-cycle flow through the ensemble carry is cut more than
k cycles back; per-cycle outputs keep their local gradients,
so history-summed losses accumulate one local term per window
(the ROAD-EnKF estimator at k=1) instead of exploding
cross-window products.
Attributes:
| Name | Type | Description |
|---|---|---|
k |
int
|
Number of trailing cycles that propagate gradients through
the ensemble carry. Must be at least 1; |
Source code in src/filterax/_train/_adjoints.py
ROAD-EnKF local gradients¶
road_enkf_loss_and_grad evaluates the loss and its local gradient in one
pass; road_enkf_grad_step wraps it with an optax optimizer update for a
ready-made training step. Both live in filterax.differentiable.
Differentiable data-assimilation surface.
Gradient strategies for training through the filter:
- :func:
differentiable_assimilate— fixed-shape :func:jax.lax.scanwhose gradient strategy is selected withadjoint=: :class:DirectAdjoint(exact, full tape), :class:RecursiveCheckpointAdjoint(exact,O(√T)memory via recomputation), or :class:TruncatedAdjoint(biased: trailing-kcycles only —O(1)memory inTand tolerant of chaotic gradient explosion). The names match the shared pipekit / diffrax vocabulary;pipekit_cycle.adjointsspecs are accepted interchangeably. - :func:
road_enkf_loss_and_grad/ :func:road_enkf_grad_step— ROAD-EnKF local-gradient strategy with :func:jax.lax.stop_gradientbetween cycles. Backward-pass memoryO(Nₑ · Nₓ)independent ofT; drops cross-time gradient terms.
Both refuse stochastic filters (StochasticEnKF, ETKF_Livings)
and the stochastic AdditiveInflator at call time — see
design_docs/features/differentiable_da.md §5.2 / §5.5.
road_enkf_loss_and_grad(dynamics: AbstractDynamics, init_ensemble: Float[Array, 'N_e N_x'], observations: Float[Array, 'T N_y'], obs_times: Float[Array, ' T'], obs_noise: lx.AbstractLinearOperator, obs_op: AbstractObsOperator, *, filter_: AbstractSequentialFilter | None = None, inflator: AbstractInflator | None = None, t0: float | Float[Array, ''] = 0.0, **analysis_extra: Any) -> tuple[Float[Array, ''], PyTree]
¶
Local-gradient NLL loss and dynamics gradient on a filter window.
Per-step :func:equinox.filter_value_and_grad over the forecast +
analysis cycle, with :func:jax.lax.stop_gradient between cycles.
Backward-pass memory is O(Nₑ · Nₓ) independent of T (cf.
:func:differentiable_assimilate whose tape grows linearly in
T).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dynamics
|
AbstractDynamics
|
Forward model. Trainable: every array leaf of this
|
required |
init_ensemble
|
Float[Array, 'N_e N_x']
|
Prior ensemble |
required |
observations
|
Float[Array, 'T N_y']
|
Stacked observation history |
required |
obs_times
|
Float[Array, ' T']
|
Stacked timestamps |
required |
obs_noise
|
AbstractLinearOperator
|
Observation error covariance |
required |
obs_op
|
AbstractObsOperator
|
Observation operator |
required |
filter_
|
AbstractSequentialFilter | None
|
Deterministic L1 filter ( |
None
|
inflator
|
AbstractInflator | None
|
Optional deterministic inflator
( |
None
|
t0
|
float | Float[Array, '']
|
Starting time used for the first forecast window. |
0.0
|
**analysis_extra
|
Any
|
Forwarded to |
{}
|
Returns:
| Type | Description |
|---|---|
Float[Array, '']
|
|
PyTree
|
observation-space negative log-likelihood and |
tuple[Float[Array, ''], PyTree]
|
is a PyTree of the same structure as |
tuple[Float[Array, ''], PyTree]
|
array leaves carry gradients; static / non-array fields are |
tuple[Float[Array, ''], PyTree]
|
|
Source code in src/filterax/_train/_road.py
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 | |
road_enkf_grad_step(dynamics: AbstractDynamics, optimizer: optax.GradientTransformation, opt_state: optax.OptState, init_ensemble: Float[Array, 'N_e N_x'], observations: Float[Array, 'T N_y'], obs_times: Float[Array, ' T'], obs_noise: lx.AbstractLinearOperator, obs_op: AbstractObsOperator, *, filter_: AbstractSequentialFilter | None = None, inflator: AbstractInflator | None = None, t0: float | Float[Array, ''] = 0.0, **analysis_extra: Any) -> tuple[AbstractDynamics, optax.OptState, Float[Array, '']]
¶
One ROAD-EnKF descent step composed with an :mod:optax optimizer.
Convenience wrapper that runs :func:road_enkf_loss_and_grad, threads
the gradient through optimizer.update, and applies the updates
to the dynamics module. Returns (new_dynamics, new_opt_state, loss)
so the caller can log loss and continue the training loop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dynamics
|
AbstractDynamics
|
Forward model to update. |
required |
optimizer
|
GradientTransformation
|
Any |
required |
opt_state
|
OptState
|
Optimizer state. Initialise with the inexact (gradient-eligible) leaves only — matches what the gradient update will write to:: Filtering on |
required |
init_ensemble
|
Float[Array, 'N_e N_x']
|
Prior ensemble |
required |
observations
|
Float[Array, 'T N_y']
|
Stacked observation history |
required |
obs_times
|
Float[Array, ' T']
|
Stacked timestamps |
required |
obs_noise
|
AbstractLinearOperator
|
Observation error covariance |
required |
obs_op
|
AbstractObsOperator
|
Observation operator |
required |
filter_
|
AbstractSequentialFilter | None
|
Deterministic L1 filter (default |
None
|
inflator
|
AbstractInflator | None
|
Optional deterministic inflator. |
None
|
t0
|
float | Float[Array, '']
|
Starting time used for the first forecast window. |
0.0
|
**analysis_extra
|
Any
|
Forwarded to |
{}
|
Returns:
| Type | Description |
|---|---|
AbstractDynamics
|
|
OptState
|
the advanced optimizer state, and the loss at the current |
Float[Array, '']
|
|
Source code in src/filterax/_train/_road.py
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 | |