Layer 1 — Component Examples¶
Usage patterns for building filters, processes, and custom protocol implementations.
Building an ETKF Analysis Step¶
Defining dynamics and observation operator, running a single analysis¶
from __future__ import annotations
import jax.numpy as jnp
from jaxtyping import Float, Array
import equinox as eqx
import somax
import ekalmx
# Define observation operator
class LinearObs(ekalmx.AbstractObsOperator):
H: Float[Array, "N_y N_x"]
def __call__(self, state):
return self.H @ state
# Define dynamics (wrapping a somax model)
class SWMDynamics(ekalmx.AbstractDynamics):
model: somax.ShallowWaterModel
dt: float = eqx.field(static=True)
def __call__(self, state, t0, t1):
sol = self.model.integrate(state, t0=t0, t1=t1, dt=self.dt)
return sol.ys[-1]
# Instantiate dynamics and observation operator
dynamics = SWMDynamics(model=somax.ShallowWaterModel(grid=grid, params=params), dt=60.0)
obs_op = LinearObs(H=H_matrix)
# Use the ETKF filter component directly
etkf = ekalmx.filters.ETKF()
# Forecast: vmap dynamics over ensemble members
forecast = eqx.filter_vmap(dynamics)(particles, t0, t1)
# Analysis: single step
result = etkf.analysis(forecast, obs, obs_op, obs_noise=R)
# result.particles: (N_e, N_x) — updated ensemble
Building an EKI Process Step¶
Initializing and iterating an Ensemble Kalman Inversion process¶
import ekalmx
# Define forward model (black-box simulator)
def simulator(params):
"""Expensive forward model: params → predictions."""
return run_climate_model(params)
# Initialize EKI process
eki = ekalmx.processes.EKI(
scheduler=ekalmx.FixedScheduler(dt=1.0),
)
state = eki.init(
particles=init_ensemble, # (J, N_p) — 100 particles, 50 params
obs=observations, # (N_d,)
noise_cov=R, # lineax operator
)
# Iterate
for step in range(n_iterations):
# Caller evaluates forward model (possibly expensive, non-JAX)
G_ensemble = eqx.filter_vmap(simulator)(state.particles) # (J, N_d)
state = eki.update(state, forward_evals=G_ensemble)
# Extract calibrated parameters
best_params = ekalmx.ensemble_mean(state.particles)
Custom Localizer¶
Adaptive distance-based localization with per-variable radius¶
import ekalmx
class AdaptiveLocalizer(ekalmx.AbstractLocalizer):
"""Distance-based localization with per-variable radius."""
radii: Float[Array, "N_x"]
coords: Float[Array, "N_x 2"] # (lat, lon) per state element
def __call__(self, cov, coords):
distances = self._compute_distances(coords)
# Per-variable Gaspari-Cohn with different radii
taper = eqx.filter_vmap(ekalmx.gaspari_cohn, in_axes=(0, 0))(
distances, self.radii
)
return ekalmx.localize(cov, taper)
Custom Inflator¶
Anderson (2007) adaptive multiplicative inflation¶
import ekalmx
class AdaptiveInflator(ekalmx.AbstractInflator):
"""Anderson (2007) adaptive multiplicative inflation."""
min_factor: float = eqx.field(static=True, default=1.0)
max_factor: float = eqx.field(static=True, default=1.2)
def __call__(self, particles, obs=None, obs_op=None, obs_noise=None):
# Estimate optimal inflation from innovation statistics
factor = self._estimate_factor(particles, obs, obs_op, obs_noise)
factor = jnp.clip(factor, self.min_factor, self.max_factor)
return ekalmx.inflate_multiplicative(particles, factor)
Ensemble Smoother (EnKS)¶
Forward filter followed by backward smoother pass¶
import ekalmx
# Run forward filter first
filter = ekalmx.filters.ETKF()
filter_results = []
state = ekalmx.FilterState(particles=ensemble, step=0)
for t, obs in enumerate(observations):
forecast = eqx.filter_vmap(dynamics)(state.particles, t, t + dt)
result = filter.analysis(forecast, obs, obs_op, R)
filter_results.append(result)
state = ekalmx.FilterState(particles=result.particles, step=t + 1)
# Backward smoother pass
smoother = ekalmx.smoothers.EnKS()
smoothed = smoother.smooth(filter_results)
# smoothed: list of AnalysisResult with improved state estimates