Localization¶
A small ensemble (N_e ≪ N_x) produces a rank-deficient sample covariance
whose long-range entries are dominated by sampling noise: two physically
unrelated variables will show spurious correlation of order 1/√N_e, and the
filter will happily use it to spread observation increments across the whole
domain. Localization suppresses these spurious long-range correlations by
damping covariance entries as a function of physical distance.
The standard mechanism is the Schur (element-wise) product P_loc = ρ ∘ P
with a taper matrix ρ built from a compactly supported correlation
function. By the Schur product theorem, the element-wise product of two
positive semi-definite matrices is positive semi-definite — so as long as the
taper is itself a valid correlation matrix (Gaspari-Cohn and SOAR are; a hard
cutoff is not, in general), localization cannot break the PSD-ness of the
covariance.
gaspari_cohn and
localization_matrix delegate to their
gaussx counterparts with support parameter c = 2 * radius (gaussx
parameterises by the compact-support length, filterax by the half-width);
the distance metrics (euclidean_distance,
haversine_distance) are re-exported from
gaussx unchanged. Wrap these primitives in an
AbstractLocalizer to plug them into the
Layer-2 loops as a drop-in component; the
LETKF instead applies localization in
observation space, per grid point, and
localized_kalman_gain tapers the gain
itself.
Taper functions¶
Map a distance array to weights in [0, 1]. Gaspari-Cohn is the
production default (compactly supported, exactly zero beyond 2 * radius);
Gaussian and SOAR decay smoothly without compact support; the hard cutoff is
a diagnostic tool rather than a valid correlation function.
filterax — differentiable ensemble Kalman filters and processes for JAX.
gaspari_cohn(distances: Float[Array, '...'], radius: float) -> Float[Array, '...']
¶
Gaspari-Cohn 5th-order piecewise polynomial taper.
With z = d / r:
Properties:
- Compact support — exactly zero for
d > 2 r. - C² smoothness — value, first and second derivatives all match
at
z = 1andz = 2. - Positive definite — the gold standard for covariance localization in operational NWP and ocean DA.
Delegates to :func:gaussx.gaspari_cohn, which parameterises by
the compact-support radius c = 2 r and guards the 2/(3 z)
far-branch term so reverse-mode gradients stay finite at d = 0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
distances
|
Float[Array, '...']
|
Non-negative distance array, any shape. |
required |
radius
|
float
|
Positive localization half-width |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '...']
|
Taper weights in |
Examples:
>>> import jax.numpy as jnp
>>> from filterax import gaspari_cohn
>>> d = jnp.array([0.0, 1.0, 2.0, 3.0])
>>> w = gaspari_cohn(d, radius=1.0)
>>> w[0], w[-1] # full weight at zero distance, zero beyond 2r
(Array(1., dtype=float32), Array(0., dtype=float32))
Reference
Gaspari, G. & Cohn, S. E. (1999). Construction of correlation functions in two and three dimensions. Q. J. R. Meteorol. Soc., 125, 723–757.
Source code in src/filterax/_primitives/_localization.py
gaussian_taper(distances: Float[Array, '...'], radius: float) -> Float[Array, '...']
¶
Gaussian taper ρ(d) = exp(−d² / (2 r²)).
Infinitely smooth (C^∞) and positive definite, but not
compactly supported — decays exponentially but never reaches zero.
Useful as a simpler alternative to Gaspari-Cohn when compact support
is not required (e.g., adjoint sensitivity studies).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
distances
|
Float[Array, '...']
|
Non-negative distance array, any shape. |
required |
radius
|
float
|
Positive characteristic length scale |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '...']
|
Taper weights in |
Examples:
>>> import jax.numpy as jnp
>>> from filterax import gaussian_taper
>>> gaussian_taper(jnp.array([0.0, 1.0]), radius=1.0)
Array([1. , 0.60653067], dtype=float32)
Source code in src/filterax/_primitives/_localization.py
soar_taper(distances: Float[Array, '...'], radius: float) -> Float[Array, '...']
¶
Second-Order Auto-Regressive taper (Thiebaux & Pedder 1987).
Properties:
- C¹ smooth — value and first derivative continuous, second is not (compare with Gaspari-Cohn's C²).
- Positive definite — Schur product preserves PSD.
- Approximate compact support — decays to
< 0.01byd ≈ 5 r; finite but exponentially small beyond that.
Often used as a correlation model for background error covariance
in operational variational systems (e.g. Met Office VAR). Simpler
than Gaspari-Cohn while still being positive definite — useful as
a localization taper when the strict compact support of
:func:gaspari_cohn is not required.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
distances
|
Float[Array, '...']
|
Non-negative distance array, any shape. |
required |
radius
|
float
|
Positive characteristic length scale |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '...']
|
Taper weights in |
Examples:
>>> import jax.numpy as jnp
>>> from filterax import soar_taper
>>> soar_taper(jnp.array([0.0, 1.0]), radius=1.0)
Array([1. , 0.7357589], dtype=float32)
Reference
Thiebaux, H. J. & Pedder, M. A. (1987). Spatial Objective Analysis. Academic Press.
Source code in src/filterax/_primitives/_localization.py
hard_cutoff(distances: Float[Array, '...'], radius: float) -> Float[Array, '...']
¶
Binary cutoff ρ(d) = 𝟙{d ≤ r}.
Discontinuous at d = r; not positive definite in general
(the resulting localized matrix may lose PSD-ness, which can cause
filter instability). Provided as a debugging primitive only; prefer
:func:gaspari_cohn or :func:gaussian_taper in production.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
distances
|
Float[Array, '...']
|
Non-negative distance array, any shape. |
required |
radius
|
float
|
Positive cutoff radius |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '...']
|
Indicator weights ( |
Float[Array, '...']
|
|
Examples:
>>> import jax.numpy as jnp
>>> from filterax import hard_cutoff
>>> hard_cutoff(jnp.array([0.5, 1.0, 1.5]), radius=1.0)
Array([1., 1., 0.], dtype=float32)
Source code in src/filterax/_primitives/_localization.py
Distances & taper matrices¶
localization_matrix fuses a pairwise
distance computation with the Gaspari-Cohn taper, producing the dense
ρ consumed by localize and
localized_kalman_gain. Use
haversine_distance as the metric for
spherical (lat, lon)-in-radians grids.
filterax — differentiable ensemble Kalman filters and processes for JAX.
localization_matrix(coords_a: Float[Array, 'N_a D'], coords_b: Float[Array, 'N_b D'], radius: float, *, metric: Callable[[Float[Array, 'N_a D'], Float[Array, 'N_b D']], Float[Array, 'N_a N_b']] = euclidean_distance) -> Float[Array, 'N_a N_b']
¶
Pairwise Gaspari-Cohn taper matrix ρᵢⱼ = ρ(d(aᵢ, bⱼ); r).
Builds the dense localization matrix consumed by
:func:filterax.localize and :func:filterax.localized_kalman_gain
directly from coordinates, fusing the distance computation with the
taper. Delegates to :func:gaussx.localization_matrix using
filterax's half-width convention (compact support at 2 r), so
localization_matrix(a, b, r) matches
gaspari_cohn(metric(a, b), r) entry for entry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coords_a
|
Float[Array, 'N_a D']
|
First coordinate set, shape |
required |
coords_b
|
Float[Array, 'N_b D']
|
Second coordinate set, shape |
required |
radius
|
float
|
Positive localization half-width |
required |
metric
|
Callable[[Float[Array, 'N_a D'], Float[Array, 'N_b D']], Float[Array, 'N_a N_b']]
|
Pairwise distance function
|
euclidean_distance
|
Returns:
| Type | Description |
|---|---|
Float[Array, 'N_a N_b']
|
Taper matrix in |
Examples:
>>> import jax.numpy as jnp
>>> from filterax import localization_matrix
>>> grid = jnp.arange(3.0)[:, None] # three points on a line
>>> rho = localization_matrix(grid, grid, radius=1.0)
>>> rho.shape, float(rho[0, 0]), float(rho[0, 2])
((3, 3), 1.0, 0.0)
Source code in src/filterax/_primitives/_localization.py
euclidean_distance(coords_a: Float[Array, 'Na D'], coords_b: Float[Array, 'Nb D']) -> Float[Array, 'Na Nb']
¶
Pairwise Euclidean distances ||a_i - b_j||.
A default metric for localization_matrix. Builds on
stable_squared_distances and takes a gradient-safe square root so
zero distances (e.g. the diagonal of a self-distance matrix) do not produce
NaN gradients.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coords_a
|
Float[Array, 'Na D']
|
First set of points, shape |
required |
coords_b
|
Float[Array, 'Nb D']
|
Second set of points, shape |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'Na Nb']
|
Distance matrix of shape |
Source code in .venv/lib/python3.12/site-packages/gaussx/_inference/_localization.py
haversine_distance(coords_a: Float[Array, 'Na 2'], coords_b: Float[Array, 'Nb 2'], radius: float = 6371000.0) -> Float[Array, 'Na Nb']
¶
Pairwise great-circle (haversine) distances on a sphere.
A metric for localization_matrix on geophysical grids.
Coordinates are (latitude, longitude) in radians.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
coords_a
|
Float[Array, 'Na 2']
|
First set of points |
required |
coords_b
|
Float[Array, 'Nb 2']
|
Second set of points |
required |
radius
|
float
|
Sphere radius in the units of the returned distance (default the
Earth mean radius, |
6371000.0
|
Returns:
| Type | Description |
|---|---|
Float[Array, 'Na Nb']
|
Great-circle distance matrix of shape |
Source code in .venv/lib/python3.12/site-packages/gaussx/_inference/_localization.py
Applying localization¶
localize is the Schur product itself;
adaptive_localization builds the weight
matrix from the ensemble's own sampling-error statistics (Anderson 2007)
instead of a prescribed distance taper — useful when no meaningful distance
metric exists (e.g. parameter spaces).
filterax — differentiable ensemble Kalman filters and processes for JAX.
localize(cov: Float[Array, 'M N'], taper: Float[Array, 'M N']) -> Float[Array, 'M N']
¶
Apply localization via Schur (element-wise) product.
Computes P_loc = ρ ∘ P where ρ ∘ P denotes
elementwise multiplication. Works on covariance, gain, or any other
dense (M, N) matrix; for structured operators apply localization
closer to where the matrix is consumed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cov
|
Float[Array, 'M N']
|
Dense covariance or gain matrix of shape |
required |
taper
|
Float[Array, 'M N']
|
Taper matrix of the same shape; elementwise multipliers
in |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, 'M N']
|
Localized matrix of shape |
Examples:
>>> import jax.numpy as jnp
>>> from filterax import localize
>>> cov = jnp.array([[1.0, 0.5], [0.5, 1.0]])
>>> taper = jnp.array([[1.0, 0.0], [0.0, 1.0]])
>>> localize(cov, taper) # off-diagonal entries suppressed
Array([[1., 0.],
[0., 1.]], dtype=float32)
Source code in src/filterax/_primitives/_localization.py
localized_kalman_gain(particles: Float[Array, 'N_e N_x'], obs_particles: Float[Array, 'N_e N_y'], obs_noise: lx.AbstractLinearOperator, rho_xy: Float[Array, 'N_x N_y'], rho_yy: Float[Array, 'N_y N_y'], *, solver: gaussx.AbstractSolverStrategy | None = None) -> Float[Array, 'N_x N_y']
¶
Kalman gain with Schur-product (Hadamard) localization.
Thin wrapper over :func:gaussx.localized_kalman_gain with the EnKF
Bessel convention. Computes
Tapering both covariances with the Schur
(element-wise) product suppresses spurious long-range correlations;
by the Schur product theorem the tapered innovation covariance stays
PSD when ρʸʸ is (use :func:filterax.localization_matrix /
:func:filterax.gaspari_cohn to build the tapers).
Unlike :func:filterax.kalman_gain, the Hadamard product destroys
the low-rank structure of Cᴴᴴ, so the innovation covariance is
materialised densely — cost O(Nₑ Nₓ Nᵧ + Nᵧ³). With
ρ ≡ 1 this reduces exactly to the unlocalized gain.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
particles
|
Float[Array, 'N_e N_x']
|
Prior ensemble in state space, shape |
required |
obs_particles
|
Float[Array, 'N_e N_y']
|
|
required |
obs_noise
|
AbstractLinearOperator
|
Observation error covariance |
required |
rho_xy
|
Float[Array, 'N_x N_y']
|
State-observation taper, shape |
required |
rho_yy
|
Float[Array, 'N_y N_y']
|
Observation-observation taper, shape |
required |
solver
|
AbstractSolverStrategy | None
|
Optional :class: |
None
|
Returns:
| Type | Description |
|---|---|
Float[Array, 'N_x N_y']
|
Dense localized Kalman gain of shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Examples:
>>> import jax.numpy as jnp
>>> import lineax as lx
>>> from filterax import kalman_gain, localized_kalman_gain
>>> particles = jnp.array([[0.0, 0.0], [1.0, 1.0], [2.0, 0.0]])
>>> obs_particles = particles[:, :1]
>>> R = lx.DiagonalLinearOperator(0.5 * jnp.ones(1))
>>> ones = jnp.ones((2, 1)), jnp.ones((1, 1))
>>> K_loc = localized_kalman_gain(particles, obs_particles, R, *ones)
>>> bool(jnp.allclose(K_loc, kalman_gain(particles, obs_particles, R)))
True
Source code in src/filterax/_primitives/_gain.py
adaptive_localization(state_particles: Float[Array, 'N_e N_x'], obs_particles: Float[Array, 'N_e N_y'], *, significance: float = 1.0) -> Float[Array, 'N_x N_y']
¶
Adaptive localization weights from ensemble correlations (Anderson 2007).
Estimates a per-pair localization weight on the (Nₓ, Nᵧ) Kalman
gain by checking whether each sample correlation
r_{ij} = Cˣᴴ_{ij} / (σ_xᵢ σ_yⱼ) exceeds its sampling
uncertainty:
Correlations smaller than significance · se(r) are zeroed.
Correlations above that threshold are kept at unit weight (a
hard-mask variant). The resulting weight matrix is multiplied into
the Kalman gain (Schur product) to suppress spurious long-range
correlations driven by sampling noise rather than physical
structure.
Unlike a distance-based taper, this localizer is data-driven —
no radius to tune. The trade-off: it requires enough ensemble
members for the correlation noise floor to be informative
(typically Nₑ ≥ 20) and an extra O(Nₑ Nₓ Nᵧ) work per
cycle.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state_particles
|
Float[Array, 'N_e N_x']
|
Prior ensemble in state space, |
required |
obs_particles
|
Float[Array, 'N_e N_y']
|
Prior ensemble in obs space |
required |
significance
|
float
|
Threshold multiplier on |
1.0
|
Returns:
| Type | Description |
|---|---|
Float[Array, 'N_x N_y']
|
Weight matrix |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Examples:
>>> import jax
>>> from filterax import adaptive_localization
>>> state = jax.random.normal(jax.random.key(0), (20, 2))
>>> obs_p = state[:, :1] # obs perfectly correlated with dim 0
>>> w = adaptive_localization(state, obs_p)
>>> w.shape
(2, 1)
>>> w[0, 0] # significant correlation kept at unit weight
Array(1., dtype=float32)
Reference
Anderson, J. L. (2007). Exploring the need for localization in ensemble data assimilation using a hierarchical ensemble filter. Physica D, 230, 99-111.
Source code in src/filterax/_primitives/_localization.py
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 | |