Inflation¶
Counteract spread collapse from finite-ensemble, model-error, or localization-side-effect sources. Two flavours of primitive:
- Drop-in inflators —
MultiplicativeInflator,RTPS,RTPP,AdditiveInflatorall implementAbstractInflatorand plug straight into the Layer-2 assimilation loops (filterax.ETKF,LETKF, …). - Helper primitives —
inflate_adaptivereturns a posterior on the multiplicative factorλ(the caller composes it with a freshMultiplicativeInflator(factor=μ_post)on the next cycle);ledoit_wolf_shrinkagereturns a regularised covariance matrix, not an inflated ensemble. Neither is anAbstractInflator, so they are driven from custom loops rather than dropped into L2 models.
The classic trio of functional primitives —
inflate_multiplicative,
inflate_rtps,
inflate_rtpp — delegates to the corresponding
gaussx primitives
(gaussx.inflate_multiplicative, gaussx.inflate_rtps,
gaussx.inflate_rtpp); filterax adds the EnKF-flavoured conventions and the
inflator classes on top.
Picking an inflator¶
| Method | Mean-preserving? | Tunable | Use when |
|---|---|---|---|
MultiplicativeInflator / inflate_multiplicative |
Yes | factor |
Simple scalar inflation; production default |
RTPS / inflate_rtps |
Yes (per-variable spread) | alpha ∈ [0, 1] |
Per-variable adaptive recovery of forecast spread (Whitaker & Hamill 2012) |
RTPP / inflate_rtpp |
Yes | alpha ∈ [0, 1] |
Full-anomaly blend that preserves inter-variable correlation (Zhang et al. 2004) |
AdditiveInflator / inflate_additive |
Yes (after recentring) | Q_add |
Explicit model-error injection (Hamill & Whitaker 2005) |
inflate_adaptive |
(Anderson 2009 prior update) | prior (μ_λ, σ²_λ) |
Self-tuning multiplicative inflation; carry posterior across cycles |
ledoit_wolf_shrinkage |
n/a (returns covariance) | none (analytic optimum) | Regularise the rank-deficient sample covariance when Nₑ ≪ Nₓ |
Inflator classes¶
Drop-in AbstractInflator implementations,
accepted by every Layer-2 assimilation loop via its inflator= argument.
filterax — differentiable ensemble Kalman filters and processes for JAX.
MultiplicativeInflator
¶
Bases: AbstractInflator
Multiplicative inflation X′ ← λ X′.
Ignores forecast_particles and any extra kwargs.
Attributes:
| Name | Type | Description |
|---|---|---|
factor |
float
|
Inflation factor |
Examples:
>>> import jax.numpy as jnp
>>> from filterax import MultiplicativeInflator
>>> inflator = MultiplicativeInflator(factor=2.0)
>>> inflator(jnp.array([[0.0], [2.0]])) # mean 1 preserved
Array([[-1.],
[ 3.]], dtype=float32)
Source code in src/filterax/_primitives/_inflators.py
RTPS
¶
Bases: AbstractInflator
Relaxation to Prior Spread (Whitaker & Hamill 2012).
Requires forecast_particles — raises ValueError when missing
rather than silently no-opping.
Attributes:
| Name | Type | Description |
|---|---|---|
alpha |
float
|
Relaxation coefficient in |
Examples:
>>> import jax.numpy as jnp
>>> from filterax import RTPS
>>> inflator = RTPS(alpha=1.0)
>>> forecast = jnp.array([[-2.0], [2.0]])
>>> analysis = jnp.array([[-1.0], [1.0]])
>>> inflator(analysis, forecast) # full prior spread restored
Array([[-2.],
[ 2.]], dtype=float32)
Source code in src/filterax/_primitives/_inflators.py
RTPP
¶
Bases: AbstractInflator
Relaxation to Prior Perturbations (Zhang et al. 2004).
Requires forecast_particles.
Attributes:
| Name | Type | Description |
|---|---|---|
alpha |
float
|
Relaxation coefficient in |
Examples:
>>> import jax.numpy as jnp
>>> from filterax import RTPP
>>> inflator = RTPP(alpha=0.5)
>>> forecast = jnp.array([[-2.0], [2.0]])
>>> analysis = jnp.array([[-1.0], [1.0]])
>>> inflator(analysis, forecast) # blend anomalies
Array([[-1.5],
[ 1.5]], dtype=float32)
Source code in src/filterax/_primitives/_inflators.py
AdditiveInflator
¶
Bases: AbstractInflator
Additive Gaussian inflation X′ ← X′ + ε, ε ~ 𝒩(0, Q_add).
Wraps :func:filterax.inflate_additive. Per-call independence:
- Inside the L2 run loop (
filterax.ETKF/LETKF/ …): the loop passesstep=through**kwargsand this inflator computeskey = jr.fold_in(self.base_key, step)so successive assimilation windows draw independent Gaussian perturbations. - In your own loop: pass
step=yourself (or pass an explicitkey=kwarg, which takes precedence overbase_key/step). Calling without either reusesself.base_keyand yields the same draw every time — fine for one-off use, but a footgun if you reuse the inflator across cycles.
Attributes:
| Name | Type | Description |
|---|---|---|
noise_cov |
AbstractLinearOperator
|
Model-error covariance |
base_key |
PRNGKeyArray
|
PRNG key seed; folded against the per-call |
Examples:
>>> import jax.numpy as jnp
>>> import jax.random as jr
>>> import lineax as lx
>>> from filterax import AdditiveInflator
>>> inflator = AdditiveInflator(
... noise_cov=lx.DiagonalLinearOperator(0.1 * jnp.ones(2)),
... base_key=jr.key(0),
... )
>>> out = inflator(jnp.zeros((4, 2)), step=0)
>>> out.shape
(4, 2)
>>> bool(jnp.allclose(out.mean(axis=0), 0.0, atol=1e-6)) # mean kept
True
Source code in src/filterax/_primitives/_inflators.py
Functional primitives¶
The pure functions underneath the classes, plus the two helpers
(inflate_adaptive, ledoit_wolf_shrinkage) that have no class form.
filterax — differentiable ensemble Kalman filters and processes for JAX.
inflate_multiplicative(particles: Float[Array, 'N_e N_x'], factor: float) -> Float[Array, 'N_e N_x']
¶
Multiplicative inflation X′ ← λ X′.
For each member x⁽ʲ⁾_inflated = x̄ + λ (x⁽ʲ⁾ − x̄). The inflated
covariance is λ² P; the analysis mean is preserved. Typical
values λ ∈ [1.01, 1.10] for operational NWP. Delegates to
:func:gaussx.inflate_multiplicative.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
particles
|
Float[Array, 'N_e N_x']
|
Ensemble of shape |
required |
factor
|
float
|
Inflation factor |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'N_e N_x']
|
Inflated ensemble of shape |
Examples:
>>> import jax.numpy as jnp
>>> from filterax import inflate_multiplicative
>>> ens = jnp.array([[0.0, 0.0], [2.0, 2.0]])
>>> inflate_multiplicative(ens, 1.5) # mean (1, 1) preserved
Array([[-0.5, -0.5],
[ 2.5, 2.5]], dtype=float32)
Reference
Anderson, J. L. & Anderson, S. L. (1999). A Monte Carlo implementation of the nonlinear filtering problem. Mon. Wea. Rev., 127, 2741–2758.
Source code in src/filterax/_primitives/_inflation.py
inflate_rtps(analysis_particles: Float[Array, 'N_e N_x'], forecast_particles: Float[Array, 'N_e N_x'], alpha: float) -> Float[Array, 'N_e N_x']
¶
Relaxation to Prior Spread (Whitaker & Hamill 2012).
Per-variable target spread is a convex blend of analysis and forecast standard deviations:
Each analysis anomaly is then rescaled so the resulting standard deviation matches the target:
Spatially adaptive (per-variable factor), mean-preserving, and
parameterised by a single coefficient α. α = 0 is the
identity; α = 1 restores the full forecast spread. Delegates to
:func:gaussx.inflate_rtps (which calls the coefficient beta
and writes the equivalent form
scale = (1 − β) + β σᶠ/σᵃ).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
analysis_particles
|
Float[Array, 'N_e N_x']
|
Posterior ensemble |
required |
forecast_particles
|
Float[Array, 'N_e N_x']
|
Prior (forecast) ensemble |
required |
alpha
|
float
|
Relaxation coefficient in |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'N_e N_x']
|
Relaxed analysis ensemble of shape |
Examples:
>>> import jax.numpy as jnp
>>> from filterax import inflate_rtps
>>> forecast = jnp.array([[-2.0], [2.0]])
>>> analysis = jnp.array([[-1.0], [1.0]])
>>> inflate_rtps(analysis, forecast, alpha=1.0) # full prior spread
Array([[-2.],
[ 2.]], dtype=float32)
Reference
Whitaker, J. S. & Hamill, T. M. (2012). Evaluating methods to account for system errors in ensemble data assimilation. Mon. Wea. Rev., 140, 3078–3089.
Source code in src/filterax/_primitives/_inflation.py
inflate_rtpp(analysis_particles: Float[Array, 'N_e N_x'], forecast_particles: Float[Array, 'N_e N_x'], alpha: float) -> Float[Array, 'N_e N_x']
¶
Relaxation to Prior Perturbations (Zhang et al. 2004).
Convex combination of analysis and forecast anomaly matrices:
Unlike RTPS (which acts on per-variable spread), RTPP operates on
the full anomaly matrix and so blends inter-variable correlation
structure between forecast and analysis. Mean-preserving.
Delegates to :func:gaussx.inflate_rtpp.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
analysis_particles
|
Float[Array, 'N_e N_x']
|
Posterior ensemble |
required |
forecast_particles
|
Float[Array, 'N_e N_x']
|
Prior (forecast) ensemble |
required |
alpha
|
float
|
Relaxation coefficient in |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'N_e N_x']
|
Relaxed analysis ensemble of shape |
Examples:
>>> import jax.numpy as jnp
>>> from filterax import inflate_rtpp
>>> forecast = jnp.array([[-2.0], [2.0]])
>>> analysis = jnp.array([[-1.0], [1.0]])
>>> inflate_rtpp(analysis, forecast, alpha=0.5) # blend anomalies
Array([[-1.5],
[ 1.5]], dtype=float32)
Reference
Zhang, F., Snyder, C., & Sun, J. (2004). Impacts of initial estimate and observation availability on convective-scale data assimilation. Mon. Wea. Rev., 132, 1238–1253.
Source code in src/filterax/_primitives/_inflation.py
inflate_additive(key: PRNGKeyArray, particles: Float[Array, 'N_e N_x'], noise_cov: lx.AbstractLinearOperator) -> Float[Array, 'N_e N_x']
¶
Additive inflation X′ ← X′ + ε, ε ~ 𝒩(0, Q_add).
Injects stochastic model-error perturbations into every ensemble
member's anomaly. Unlike multiplicative inflation, the noise
direction is set by Q_add rather than by the ensemble's own
spread — useful when the dominant model-error mode is not
represented by the ensemble (e.g. structurally underdispersive in
a particular subspace). Mean-preserving by construction (we
subtract the empirical mean of the noise).
Common sources for Q_add: climatological variability, lagged
forecast differences, or stochastic-physics tendency variances.
Sampling delegates to :func:filterax.perturbed_observations's
diagonal-aware path, so a diagonal Q_add never materialises.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
PRNGKeyArray
|
PRNG key consumed by this call. |
required |
particles
|
Float[Array, 'N_e N_x']
|
Ensemble of shape |
required |
noise_cov
|
AbstractLinearOperator
|
Additive-noise covariance |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'N_e N_x']
|
Inflated ensemble of shape |
Reference
Hamill, T. M. & Whitaker, J. S. (2005). Accounting for the error due to unresolved scales in ensemble data assimilation: A comparison of different approaches. Mon. Wea. Rev., 133, 3132-3147.
Source code in src/filterax/_primitives/_inflation.py
inflate_adaptive(inflation_mean: Float[Array, ''] | float, inflation_var: Float[Array, ''] | float, innovation: Float[Array, ' N_y'], innovation_cov: Float[Array, 'N_y N_y'], *, min_factor: float = 1.0, max_factor: float = 1.2) -> tuple[Float[Array, ''], Float[Array, '']]
¶
Bayesian update of a multiplicative inflation factor (Anderson 2009).
Treats the inflation factor λ as a random variable with a
Gaussian prior λ ~ 𝒩(μ_λ, σ²_λ) and updates it from the
cycle's normalised innovation. The data-implied factor is the
clamped Mahalanobis ratio
(the simplified Anderson-2009 form — under correct specification
E[χ²/Nᵧ] = 1; under-dispersive ensembles overshoot and pull
λ̂_obs above 1). The posterior on λ is then the Gaussian
product of the prior with an "observation likelihood" centred at
λ̂_obs whose variance is the χ²-distribution variance
σ²_obs = 2 / Nᵧ:
Returns the posterior (μ, σ²) as JAX scalars so callers
can carry the belief through jax.jit / jax.grad / lax.scan
state across assimilation windows. Actual inflation of an ensemble
is one extra call to :func:inflate_multiplicative with μ_post
as the factor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inflation_mean
|
Float[Array, ''] | float
|
Prior mean |
required |
inflation_var
|
Float[Array, ''] | float
|
Prior variance |
required |
innovation
|
Float[Array, ' N_y']
|
Innovation vector |
required |
innovation_cov
|
Float[Array, 'N_y N_y']
|
Dense innovation covariance |
required |
min_factor
|
float
|
Clamp lower bound for the data-implied estimate. |
1.0
|
max_factor
|
float
|
Clamp upper bound. |
1.2
|
Returns:
| Type | Description |
|---|---|
Float[Array, '']
|
|
Float[Array, '']
|
for use as the prior on the next cycle. |
Reference
Anderson, J. L. (2009). Spatially and temporally varying adaptive covariance inflation for ensemble filters. Tellus A, 61, 72-83.
Source code in src/filterax/_primitives/_inflation.py
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 | |
ledoit_wolf_shrinkage(particles: Float[Array, 'N_e N_x']) -> tuple[Float[Array, 'N_x N_x'], Float[Array, '']]
¶
Ledoit-Wolf optimal covariance shrinkage (Ledoit & Wolf 2004).
Replaces the sample covariance with a convex combination toward a scalar-multiple-of-identity target:
where μ = tr(P_sample) / Nₓ is the average sample eigenvalue
and λ* ∈ [0, 1] is the optimal shrinkage intensity that
minimises E ‖P_shrunk − P_true‖²_F. The closed-form
Ledoit-Wolf estimator:
where d² is the sample deviation from the target and b² is
the oracle approximation, capped at d² so λ* ≤ 1.
Useful when Nₑ ≪ Nₓ and the rank-deficient sample covariance
would otherwise contaminate downstream operations. Positive
semidefinite by construction (PSD, not strictly PD — when the
ensemble is collapsed in some direction both the sample covariance
and the target μ I are singular there). Returns a dense
(Nₓ, Nₓ) matrix — only call it when Nₓ is small enough to
materialise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
particles
|
Float[Array, 'N_e N_x']
|
Ensemble of shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'N_x N_x']
|
|
Float[Array, '']
|
shrinkage intensity as a scalar array, so the function composes |
tuple[Float[Array, 'N_x N_x'], Float[Array, '']]
|
with |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Reference
Ledoit, O. & Wolf, M. (2004). A well-conditioned estimator for large-dimensional covariance matrices. J. Multivariate Anal., 88, 365-411.
Source code in src/filterax/_primitives/_inflation.py
Composition pattern¶
The L2 models (filterax.ETKF, filterax.LETKF, …) accept any
AbstractInflator instance. To carry stateful inflation (e.g. adaptive
λ) across cycles, drive the loop yourself with the L1 components:
import filterax as flx
# Anderson 2009: carry (μ_λ, σ²_λ) across cycles.
mu, var = 1.0, 0.01
inflator = flx.MultiplicativeInflator(factor=mu)
for obs, time in observations:
forecast = vmap_dynamics(particles, t_prev, time)
result = flx.filters.ETKF().analysis(forecast, obs, obs_op, R)
# Update the inflation belief from this cycle's innovation.
S = flx.innovation_covariance(vmap(obs_op)(forecast), R).as_matrix()
innov = obs - vmap(obs_op)(forecast).mean(axis=0)
mu, var = flx.inflate_adaptive(mu, var, innov, S)
inflator = flx.MultiplicativeInflator(factor=mu)
particles = inflator(result.particles, forecast)