optax integration¶
EKI, EKS, and UKI are exposed as optax.GradientTransformation pairs
so you can mix derivative-free ensemble updates with the rest of the
optax ecosystem (schedules, gradient clipping, hybrid pipelines with
gradient-based optimisers, …). The three constructors live in
filterax.optax.
Constructors¶
Ensemble Kalman processes exposed as optax gradient transformations.
Each constructor returns an optax.GradientTransformation with the
ensemble (or sigma-point belief) living inside the optax OptState
and the running mean estimate flowing back through apply_updates.
- :func:
eki— Ensemble Kalman Inversion. - :func:
eks— Ensemble Kalman Sampler (posterior sampling). - :func:
uki— Unscented Kalman Inversion (parametric).
Compose with optax.chain for clipping, schedules, hybrid pipelines
with gradient-based optimisers, etc. See
docs/design_docs/features/optax_ekp.md for the contract and
examples.
eki(forward_fn: ForwardFn, obs: Float[Array, ' N_d'], noise_cov: lx.AbstractLinearOperator, *, n_ensemble: int = 50, init_spread: float = 1.0, scheduler: AbstractScheduler | None = None, key: PRNGKeyArray | int = 0) -> optax.GradientTransformation
¶
Ensemble Kalman Inversion as an optax.GradientTransformation.
The grads argument to :meth:update is ignored — the ensemble
provides search directions via the cross-covariance. The increment
returned is the change in the ensemble mean, unflattened back to
the params tree shape so :func:optax.apply_updates works as
usual.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forward_fn
|
ForwardFn
|
|
required |
obs
|
Float[Array, ' N_d']
|
Observation vector |
required |
noise_cov
|
AbstractLinearOperator
|
Observation noise covariance |
required |
n_ensemble
|
int
|
Ensemble size |
50
|
init_spread
|
float
|
Standard deviation of the initial ensemble
perturbation around |
1.0
|
scheduler
|
AbstractScheduler | None
|
Step-size strategy. Defaults to
:class: |
None
|
key
|
PRNGKeyArray | int
|
PRNG seed (int or PRNG array) for the initial ensemble. |
0
|
Returns:
| Type | Description |
|---|---|
GradientTransformation
|
|
GradientTransformation
|
ensemble and returns the per-step mean delta as the optax |
GradientTransformation
|
update. |
Examples:
>>> import jax.numpy as jnp
>>> import lineax as lx
>>> import optax
>>> from filterax import FixedScheduler
>>> from filterax.optax import eki
>>> G = jnp.array([[1.0, 0.5], [-0.3, 1.2]])
>>> transform = eki(
... forward_fn=lambda theta: G @ theta,
... obs=jnp.array([0.5, 0.9]),
... noise_cov=lx.DiagonalLinearOperator(0.1 * jnp.ones(2)),
... n_ensemble=8,
... scheduler=FixedScheduler(dt=1.0),
... )
>>> params = jnp.zeros(2)
>>> state = transform.init(params)
>>> updates, state = transform.update(None, state, params)
>>> params = optax.apply_updates(params, updates)
>>> params.shape
(2,)
Source code in src/filterax/_processes/_optax.py
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 | |
eks(forward_fn: ForwardFn, obs: Float[Array, ' N_d'], noise_cov: lx.AbstractLinearOperator, *, n_ensemble: int = 100, init_spread: float = 1.0, scheduler: AbstractScheduler | None = None, key: PRNGKeyArray | int = 0) -> optax.GradientTransformation
¶
Ensemble Kalman Sampler as an optax.GradientTransformation.
Adds Brownian noise so the ensemble samples the posterior rather
than collapsing. Access state.particles after burn-in for
posterior samples. The PRNG key advances at every step via
:func:jr.fold_in so successive draws are independent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forward_fn
|
ForwardFn
|
|
required |
obs
|
Float[Array, ' N_d']
|
Observation vector. |
required |
noise_cov
|
AbstractLinearOperator
|
Observation noise covariance |
required |
n_ensemble
|
int
|
Ensemble size |
100
|
init_spread
|
float
|
Standard deviation of the initial ensemble
perturbation around |
1.0
|
scheduler
|
AbstractScheduler | None
|
Step-size strategy. Use
:class: |
None
|
key
|
PRNGKeyArray | int
|
PRNG seed. |
0
|
Source code in src/filterax/_processes/_optax.py
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 | |
uki(forward_fn: ForwardFn, obs: Float[Array, ' N_d'], noise_cov: lx.AbstractLinearOperator, *, init_cov: float | Float[Array, 'N_p N_p'] = 1.0, scheduler: AbstractScheduler | None = None, alpha: float = 1.0, beta: float = 2.0, kappa: float = 0.0) -> optax.GradientTransformation
¶
Unscented Kalman Inversion as an optax.GradientTransformation.
Parametric: state carries (μ, Σ) plus the unflatten callback.
Each update regenerates 2 Nₚ + 1 sigma points, evaluates
forward_fn on them, and applies the unscented Kalman update.
Access state.covariance for the posterior covariance estimate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forward_fn
|
ForwardFn
|
|
required |
obs
|
Float[Array, ' N_d']
|
Observation vector. |
required |
noise_cov
|
AbstractLinearOperator
|
Observation noise covariance |
required |
init_cov
|
float | Float[Array, 'N_p N_p']
|
Initial parameter covariance. Scalar → |
1.0
|
scheduler
|
AbstractScheduler | None
|
Step-size strategy. |
None
|
alpha
|
float
|
Sigma-point spread parameter (Wan & van der Merwe 2000). |
1.0
|
beta
|
float
|
Prior-moment weighting ( |
2.0
|
kappa
|
float
|
Secondary scaling parameter; typically |
0.0
|
Source code in src/filterax/_processes/_optax.py
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 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 | |
Contract¶
The grads argument to update(grads, state, params) is ignored —
the ensemble (or sigma-point quadrature) provides search directions.
The returned updates is the change in the ensemble mean, unflattened
back into the original params PyTree shape so optax.apply_updates
works without modification.
The ensemble itself lives in the optax OptState; params only ever
holds the running mean. Pytree-shaped params (nested dicts,
NamedTuples, …) round-trip via jax.flatten_util.ravel_pytree.
Patterns¶
Basic EKI loop¶
import filterax as flx
import optax
transform = flx.optax.eki(
forward_fn=simulator,
obs=observations,
noise_cov=Gamma,
scheduler=flx.DataMisfitController(),
)
params = initial_params
state = transform.init(params)
for _ in range(50):
updates, state = transform.update(None, state, params)
params = optax.apply_updates(params, updates)
Posterior sampling with EKS¶
sampler = flx.optax.eks(
forward_fn=simulator,
obs=observations,
noise_cov=Gamma,
n_ensemble=200,
)
state = sampler.init(params)
samples = []
for step in range(500):
updates, state = sampler.update(None, state, params)
params = optax.apply_updates(params, updates)
if step > burnin:
samples.append(state.particles) # (200, Nₚ) per step
Hybrid: gradient warm-start + EKI refinement¶
# Phase 1 — Adam handles fast initial descent with gradients.
adam = optax.adam(1e-3)
state_a = adam.init(params)
for _ in range(1000):
grads = jax.grad(loss_fn)(params)
updates, state_a = adam.update(grads, state_a, params)
params = optax.apply_updates(params, updates)
# Phase 2 — switch to EKI for gradient-free refinement.
phase2 = flx.optax.eki(
forward_fn=simulator,
obs=observations,
noise_cov=Gamma,
init_spread=0.1, # tight cluster around the warm-started mean
)
state_b = phase2.init(params)
for _ in range(50):
updates, state_b = phase2.update(None, state_b, params)
params = optax.apply_updates(params, updates)
Composition with other optax transforms¶
optimizer = optax.chain(
flx.optax.eki(forward_fn=sim, obs=y, noise_cov=R),
optax.clip_by_global_norm(1.0),
)
state = optimizer.init(params)
updates, state = optimizer.update(None, state, params)
params = optax.apply_updates(params, updates)
Internal state types¶
These are returned from the optax transforms' init and threaded
through update. Inspect them directly for the full ensemble
(EKI / EKS) or parametric belief (UKI) — useful for diagnostics and
posterior sampling.
Ensemble Kalman processes exposed as optax gradient transformations.
Each constructor returns an optax.GradientTransformation with the
ensemble (or sigma-point belief) living inside the optax OptState
and the running mean estimate flowing back through apply_updates.
- :func:
eki— Ensemble Kalman Inversion. - :func:
eks— Ensemble Kalman Sampler (posterior sampling). - :func:
uki— Unscented Kalman Inversion (parametric).
Compose with optax.chain for clipping, schedules, hybrid pipelines
with gradient-based optimisers, etc. See
docs/design_docs/features/optax_ekp.md for the contract and
examples.
EKIOptaxState
¶
Bases: Module
Internal optax state for :func:eki.
Source code in src/filterax/_processes/_optax.py
EKSOptaxState
¶
Bases: Module
Internal optax state for :func:eks.
Source code in src/filterax/_processes/_optax.py
UKIOptaxState
¶
Bases: Module
Internal optax state for :func:uki.