Smoothers¶
Backward-pass refiners that turn a sequential-filter history into the
smoothing distribution p(x_t | y_{1:T}) — conditioning every state on the
whole observation record rather than only the past. All smoothers consume
the stacked forecast_history / analysis_history produced by an
AssimilationResult and return a
SmoothingResult.
Rough selection guide: EnKS (Evensen & van Leeuwen 2000) is the standard
single backward pass and the default choice; EnsembleRTS is the ensemble
analogue of the classical Rauch-Tung-Striebel recursion (it coincides with
EnKS for zero model error); EnsembleSqrtSmoother is the deterministic
square-root variant, paired with a forward square-root filter (ETKF, EnSRF,
ESTKF); FixedLagSmoother smooths only a trailing window of length lag —
the bounded-memory / streaming option, equivalent to EnKS when
lag ≥ T − 1; and IES is the standalone iterative ensemble smoother
(Chen & Oliver 2013) that solves an inverse problem over a single
observation window, for strongly nonlinear cases where one backward pass is
not enough.
These are importable both from the top level (filterax.EnKS) and from the
themed namespace (filterax.smoothers.EnKS).
Reference¶
filterax — differentiable ensemble Kalman filters and processes for JAX.
EnKS
¶
Bases: Module
Ensemble Kalman Smoother — Evensen & van Leeuwen (2000).
Standard single backward pass over a stored filter history. The
smoother gain at step t is the sample cross-covariance times the
pseudoinverse of the sample forecast covariance,
factored through the Nₑ × Nₑ Gram matrix F Fᵀ so the dense
Nₓ × Nₓ forecast covariance is never materialised.
Compatible with any :class:AbstractSequentialFilter (StochasticEnKF,
ETKF, EnSRF, LETKF, ETKF_Livings, EnSRF_Serial, ESTKF). Pass the
forecast_history and analysis_history produced by an
:class:AssimilationResult.
Complexity: O(T (Nₑ² Nₓ + Nₑ³)) for the whole backward pass.
Examples:
>>> import jax
>>> from filterax import EnKS
>>> k1, k2 = jax.random.split(jax.random.key(0))
>>> forecast = jax.random.normal(k1, (3, 4, 2)) # (T, N_e, N_x)
>>> analysis = jax.random.normal(k2, (3, 4, 2))
>>> result = EnKS().smooth(forecast, analysis)
>>> result.smoothed_history.shape
(3, 4, 2)
Source code in src/filterax/_smoothers.py
smooth(forecast_history: Float[Array, 'T N_e N_x'], analysis_history: Float[Array, 'T N_e N_x']) -> SmoothingResult
¶
Run the full backward pass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forecast_history
|
Float[Array, 'T N_e N_x']
|
Stacked forecast ensembles
|
required |
analysis_history
|
Float[Array, 'T N_e N_x']
|
Stacked analysis ensembles
|
required |
Returns:
| Type | Description |
|---|---|
SmoothingResult
|
class: |
SmoothingResult
|
and |
SmoothingResult
|
chains into a follow-up forecast, matching the |
SmoothingResult
|
|
Source code in src/filterax/_smoothers.py
EnsembleRTS
¶
Bases: Module
Ensemble Rauch-Tung-Striebel smoother.
Conceptual ensemble analog of the classical RTS recursion. With the
cross-covariance form used here — G_t = C^{af}_{t,t+1}
(C^{ff}_{t+1})⁺ — the algorithm coincides with :class:EnKS for
zero model error (Evensen 2003, §5; see also the smoothers design
doc). The class exists so callers can name the RTS interpretation
explicitly; the implementation is shared with :class:EnKS.
Compatible with any :class:AbstractSequentialFilter. Complexity:
O(T (Nₑ² Nₓ + Nₑ³)).
Source code in src/filterax/_smoothers.py
smooth(forecast_history: Float[Array, 'T N_e N_x'], analysis_history: Float[Array, 'T N_e N_x']) -> SmoothingResult
¶
Run the backward RTS pass — see :meth:EnKS.smooth.
Source code in src/filterax/_smoothers.py
EnsembleSqrtSmoother
¶
Bases: Module
Ensemble Square Root Smoother — Tippett et al. (2003); Whitaker & Compo (2002).
Deterministic backward pass paired with a forward square-root filter
(ETKF, EnSRF, ESTKF). Differs from :class:EnKS in how the
smoothed perturbations are produced: instead of the raw per-member
correction δ_j = G_t (X^s_{t+1}[j] - X^f_{t+1}[j]), the
smoothed mean and perturbations are updated separately, with the
perturbation half applied through a symmetric square root of the
ensemble-space cov-update transform
This avoids accumulating cross-member coupling across many backward
steps — the same reason :class:ETKF uses a symmetric square-root
transform on the forward pass.
Mathematically agrees with :class:EnKS in the infinite-ensemble
limit; with finite ensembles the mean is identical (both produce
m^a_t + G_t (m^s_{t+1} - m^f_{t+1})) and the perturbations
differ only in their ensemble-space rotation.
Complexity: O(T (Nₑ² Nₓ + Nₑ³)) — same as :class:EnKS.
Source code in src/filterax/_smoothers.py
smooth(forecast_history: Float[Array, 'T N_e N_x'], analysis_history: Float[Array, 'T N_e N_x']) -> SmoothingResult
¶
Run the square-root backward pass — see :meth:EnKS.smooth.
Source code in src/filterax/_smoothers.py
FixedLagSmoother
¶
Bases: Module
Fixed-lag ensemble smoother (Anderson & Anderson 1999; Nerger et al. 2012).
At each position s, runs the backward smoother only over the
window [s, min(T-1, s + lag)]. Equivalent to :class:EnKS when
lag ≥ T-1; equivalent to "no smoothing" when lag == 0.
The batch interface is the one exposed here — for an online /
streaming setting (where states older than lag are finalised and
discarded from memory) the user maintains the rolling buffer
explicitly and calls :meth:smooth over the current window.
Attributes:
| Name | Type | Description |
|---|---|---|
lag |
int
|
Number of backward steps. |
Complexity: O(T · lag · (Nₑ² Nₓ + Nₑ³)) for the batch sweep.
Source code in src/filterax/_smoothers.py
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 | |
smooth(forecast_history: Float[Array, 'T N_e N_x'], analysis_history: Float[Array, 'T N_e N_x']) -> SmoothingResult
¶
Run the windowed backward pass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forecast_history
|
Float[Array, 'T N_e N_x']
|
Stacked forecast ensembles. |
required |
analysis_history
|
Float[Array, 'T N_e N_x']
|
Stacked analysis ensembles. |
required |
Returns:
| Type | Description |
|---|---|
SmoothingResult
|
class: |
SmoothingResult
|
produced by an EnKS-style backward pass restricted to the |
SmoothingResult
|
window |
Source code in src/filterax/_smoothers.py
IES
¶
Bases: Module
Iterative Ensemble Smoother — Chen & Oliver (2013); Evensen et al. (2019).
Standalone iterative ensemble method that solves an inverse problem
in a single window of observations. With perturbed observations
d^j = y + ε^j, each ε^j ~ 𝒩(0, Γ) drawn once, each iteration is:
where C⁰_θθ is the initial ensemble covariance and G_i the
ensemble-average sensitivity at the current iterate. The anchor to
θ_0^j (the initial ensemble member) is what distinguishes IES
from :class:~filterax.EKI's drift-style iteration: rather than walking
θ toward a MAP with a step-size schedule, IES re-targets θ_0
plus a Gauss-Newton correction every iteration. For a linear G the
first iterate with α = 1 is already the ensemble posterior and later
iterates stay there. α < 1 damps the iteration, Levenberg-Marquardt
style, for strongly nonlinear G.
Use case: history matching, reservoir simulation, and any inverse
problem where the forward model maps parameters to a full
observation time series and a single backward pass on a sequential
filter would be insufficient. For sequential filtering followed by a
backward refinement, use :class:EnKS instead.
Attributes:
| Name | Type | Description |
|---|---|---|
n_iterations |
int
|
Static iteration count. |
step_size |
float
|
Damping factor |
seed |
int
|
Integer for the default PRNG key of the perturbations. |
base_key |
PRNGKeyArray | None
|
Optional explicit PRNG key (takes precedence over
|
Source code in src/filterax/_smoothers.py
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 | |
solve(particles: Float[Array, 'J N_p'], obs: Float[Array, ' N_d'], noise_cov: lx.AbstractLinearOperator, forward_model: Callable[[Float[Array, ' N_p']], Float[Array, ' N_d']]) -> AnalysisResult
¶
Run the iterative smoother to its fixed iteration count.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
particles
|
Float[Array, 'J N_p']
|
Initial ensemble |
required |
obs
|
Float[Array, ' N_d']
|
Observation vector |
required |
noise_cov
|
AbstractLinearOperator
|
Observation noise covariance |
required |
forward_model
|
Callable[[Float[Array, ' N_p']], Float[Array, ' N_d']]
|
|
required |
Returns:
| Type | Description |
|---|---|
AnalysisResult
|
class: |
Source code in src/filterax/_smoothers.py
Exact linear-Gaussian smoother¶
When the dynamics and observation operator are linear and the noise is Gaussian, no ensemble is needed: KalmanSmoother runs the exact Kalman filter forward and the Rauch-Tung-Striebel smoother backward (through gaussx's kalman_filter and rts_smoother) and returns the filtered and smoothed Gaussian marginals in a KalmanSmoothingResult. It is the exact counterpart of EnKS and EnsembleRTS, and the ground truth they converge to as the ensemble grows.
Its prior sits at the first observation time and each transition spans the actual gap to the next one, so irregular observation times need no regridding; KalmanSmoother.from_sde builds the transitions from a gaussx SDE kernel such as gaussx.MaternSDE. Masked observation entries are marginalised out exactly, and a time with every entry masked only predicts.
filterax — differentiable ensemble Kalman filters and processes for JAX.
KalmanSmoother
¶
Bases: Module
Exact Kalman filter and Rauch–Tung–Striebel smoother over irregular times.
The linear-Gaussian counterpart of :class:filterax.EnKS and
:class:filterax.EnsembleRTS: a state observed at times
t_1 < … < t_T evolves as a Gauss–Markov process between them,
with x_1 ~ 𝒩(μ_x, Σ_x) at the first observation time, and
:meth:smooth returns the exact marginals p(x_k | y_1:k) and
p(x_k | y_1:T). F_k and Σ_η,k act over the actual gap
t_k − t_{k−1}, so irregular times need no regridding;
:meth:from_sde builds them from a gaussx SDE kernel.
Unlike :class:SquareRootKF, whose prior sits one transition before
the first observation, the prior here is the state at t_1, so
there are T − 1 transitions for T observation times.
Algorithm
- Prepend
F_1 = IandΣ_η,1 = 0, so that gaussx's predict-then-update filter observesx_1under the prior𝒩(μ_x, Σ_x)itself. - Make masked observation entries inert (zero row of
H_k, unit variance inΣ_ε,k, zero datum), which equals deleting them; a time with every entry masked only predicts. - Filter forward with :func:
gaussx.kalman_filter. - Smooth backward with :func:
gaussx.rts_smoother,μ_x,k|T = μ_x,k|k + G_k (μ_x,k+1|T − μ_x,k+1|k)with the smoother gainG_k = Σ_x,k|k F_k+1ᵀ Σ_x,k+1|k⁻¹.
Attributes:
| Name | Type | Description |
|---|---|---|
transition |
Float[Array, '*G N N']
|
|
process_noise |
Float[Array, '*G N N']
|
|
obs_model |
Float[Array, '*T M N']
|
|
obs_noise |
Float[Array, '*T M M']
|
|
init_mean |
Float[Array, ' N']
|
Prior mean |
init_cov |
Float[Array, 'N N']
|
Prior covariance |
Complexity: O(T (N³ + M³)), sequential in T.
Examples:
A random walk observed at three times, the middle one missing; the smoother fills the gap from both neighbours:
>>> import jax.numpy as jnp
>>> from filterax import KalmanSmoother
>>> smoother = KalmanSmoother(
... transition=jnp.eye(1),
... process_noise=jnp.eye(1),
... obs_model=jnp.eye(1),
... obs_noise=0.01 * jnp.eye(1),
... init_mean=jnp.zeros(1),
... init_cov=jnp.eye(1),
... )
>>> obs = jnp.array([[1.0], [jnp.nan], [3.0]])
>>> mask = jnp.array([[True], [False], [True]])
>>> result = smoother.smooth(obs, mask)
>>> result.smoothed_means.shape
(3, 1)
>>> bool(jnp.isclose(result.smoothed_means[1, 0], 2.0, atol=0.05))
True
References
Rauch, Tung & Striebel (1965), AIAA Journal 3(8), 1445–1450. Särkkä (2013), Bayesian Filtering and Smoothing, ch. 8.
Source code in src/filterax/_filters/_parametric.py
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 | |
from_sde(sde: gaussx.SDEKernel, times: Float[Array, ' T'], obs_model: Float[Array, '*T M N'], obs_noise: Float[Array, '*T M M'], init_mean: Float[Array, ' N'] | None = None) -> KalmanSmoother
classmethod
¶
Discretise a gaussx SDE kernel over the gaps of times.
F_k = exp(A Δt_k) and Σ_η,k = P_∞ − F_k P_∞ F_kᵀ on
Δt_k = t_k − t_{k−1}, where A is the SDE's drift and
P_∞ its stationary covariance, which is also the prior
covariance of x_1.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sde
|
SDEKernel
|
A gaussx SDE kernel, e.g. |
required |
times
|
Float[Array, ' T']
|
Observation times |
required |
obs_model
|
Float[Array, '*T M N']
|
|
required |
obs_noise
|
Float[Array, '*T M M']
|
|
required |
init_mean
|
Float[Array, ' N'] | None
|
Prior mean |
None
|
Returns:
| Type | Description |
|---|---|
KalmanSmoother
|
The smoother for those times. |
Source code in src/filterax/_filters/_parametric.py
smooth(observations: Float[Array, 'T M'], mask: Bool[Array, 'T M'] | None = None) -> KalmanSmoothingResult
¶
Run the filter forward and the RTS smoother backward.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
observations
|
Float[Array, 'T M']
|
|
required |
mask
|
Bool[Array, 'T M'] | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
KalmanSmoothingResult
|
class: |
KalmanSmoothingResult
|
smoothed marginals at every time and the log-likelihood. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a stacked field's time axis does not match
|
Source code in src/filterax/_filters/_parametric.py
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 | |
KalmanSmoothingResult
¶
Bases: Module
Output of :meth:KalmanSmoother.smooth.
The Gaussian marginals of the state at every observation time, both filtered (conditioned on the past) and smoothed (conditioned on the whole record), along a leading time axis. Cross-time covariances are not kept.
Attributes:
| Name | Type | Description |
|---|---|---|
filtered_means |
Float[Array, 'T N']
|
|
filtered_covs |
Float[Array, 'T N N']
|
|
smoothed_means |
Float[Array, 'T N']
|
|
smoothed_covs |
Float[Array, 'T N N']
|
|
log_likelihood |
Float[Array, '']
|
Marginal log-likelihood |