cuthbertlib 0.0.13__tar.gz → 0.0.15__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/PKG-INFO +2 -2
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/__init__.py +1 -1
- cuthbertlib-0.0.15/cuthbertlib/ensemble_kalman/README.md +42 -0
- cuthbertlib-0.0.15/cuthbertlib/ensemble_kalman/__init__.py +15 -0
- {cuthbertlib-0.0.13/cuthbertlib/enkf → cuthbertlib-0.0.15/cuthbertlib/ensemble_kalman}/filtering.py +61 -12
- cuthbertlib-0.0.15/cuthbertlib/ensemble_kalman/localization.py +89 -0
- cuthbertlib-0.0.15/cuthbertlib/ensemble_kalman/smoothing.py +36 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/linearize/moments.py +2 -2
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/resampling/protocols.py +3 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/pyproject.toml +1 -1
- cuthbertlib-0.0.13/cuthbertlib/enkf/README.md +0 -14
- cuthbertlib-0.0.13/cuthbertlib/enkf/__init__.py +0 -1
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/.gitignore +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/README.md +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/README.md +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/discrete/__init__.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/discrete/filtering.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/discrete/smoothing.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/kalman/README.md +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/kalman/__init__.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/kalman/filtering.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/kalman/generate.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/kalman/sampling.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/kalman/smoothing.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/linalg/README.md +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/linalg/__init__.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/linalg/collect_nans_chol.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/linalg/marginal_sqrt_cov.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/linalg/symmetric_inv_sqrt.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/linalg/tria.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/linearize/README.md +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/linearize/__init__.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/linearize/log_density.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/linearize/taylor.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/quadrature/README.md +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/quadrature/__init__.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/quadrature/common.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/quadrature/cubature.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/quadrature/gauss_hermite.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/quadrature/linearize.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/quadrature/unscented.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/quadrature/utils.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/resampling/README.md +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/resampling/__init__.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/resampling/adaptive.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/resampling/autodiff.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/resampling/killing.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/resampling/multinomial.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/resampling/no_resampling.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/resampling/systematic.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/resampling/utils.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/smc/README.md +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/smc/__init__.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/smc/ess.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/smc/smoothing/__init__.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/smc/smoothing/exact_sampling.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/smc/smoothing/mcmc.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/smc/smoothing/protocols.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/smc/smoothing/tracing.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/stats/README.md +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/stats/__init__.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/stats/multivariate_normal.py +0 -0
- {cuthbertlib-0.0.13 → cuthbertlib-0.0.15}/cuthbertlib/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: cuthbertlib
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.15
|
|
4
4
|
Summary: Atomic building blocks for state-space model inference with JAX
|
|
5
5
|
Author-email: Sam Duffield <s@mduffield.com>, Sahel Iqbal <sahel13miqbal@proton.me>, Adrien Corenflos <adrien.corenflos.stats@gmail.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Ensemble Kalman
|
|
2
|
+
|
|
3
|
+
The high-level interfaces built on these functions are in
|
|
4
|
+
[`cuthbert.ensemble_kalman.ensemble_kalman_filter`](../../cuthbert/ensemble_kalman/ensemble_kalman_filter.py)
|
|
5
|
+
and
|
|
6
|
+
[`cuthbert.ensemble_kalman.ensemble_rts_smoother`](../../cuthbert/ensemble_kalman/ensemble_rts_smoother.py).
|
|
7
|
+
|
|
8
|
+
<!-- --8<-- [start:overview] -->
|
|
9
|
+
This sub-repository provides modular functions for ensemble Kalman methods, including the ensemble Kalman filter (EnKF) and ensemble RTS smoother (EnRTS smoother).
|
|
10
|
+
|
|
11
|
+
The core functions are:
|
|
12
|
+
|
|
13
|
+
- `predict`: Propagate ensemble members through nonlinear dynamics with additive Gaussian noise.
|
|
14
|
+
- `filter_update`: Update ensemble members with an observation using the EnKF update equation.
|
|
15
|
+
- `smoother_update`: Apply one Ensemble Rauch-Tung-Striebel smoother update.
|
|
16
|
+
- `construct_tapered_chol_innovation_covariance`: Construct a tapered innovation covariance factor without forming the covariance densely.
|
|
17
|
+
- `gaspari_cohn`: Construct covariance tapers with the Gaspari-Cohn correlation function.
|
|
18
|
+
- `gaussian`: Construct smooth, non-compact covariance tapers with the Gaussian correlation function.
|
|
19
|
+
<!-- --8<-- [end:overview] -->
|
|
20
|
+
|
|
21
|
+
## Ensemble Kalman filtering
|
|
22
|
+
|
|
23
|
+
<!-- --8<-- [start:filtering] -->
|
|
24
|
+
Together, `predict` and `filter_update` can be used to perform an online EnKF filtering step.
|
|
25
|
+
|
|
26
|
+
The EnKF uses an ensemble of particles with a Kalman-style measurement update based on empirical covariances. Unlike the EKF, it does not require Jacobians, while naturally handling nonlinear dynamics.
|
|
27
|
+
<!-- --8<-- [end:filtering] -->
|
|
28
|
+
|
|
29
|
+
## Ensemble Rauch-Tung-Striebel smoothing
|
|
30
|
+
|
|
31
|
+
<!-- --8<-- [start:smoothing] -->
|
|
32
|
+
The EnRTS algorithm provides a smoothing counterpart to the EnKF, based on the RTS smoother. It makes an empirical approximation of the RTS smoother gain, which is applied to the ensemble in a backwards pass to obtain a smoothing distribution. Note that, based on the outputs of the EnKF, the EnRTS step is entirely deterministic.
|
|
33
|
+
<!-- --8<-- [end:smoothing] -->
|
|
34
|
+
|
|
35
|
+
## Covariance localization
|
|
36
|
+
|
|
37
|
+
<!-- --8<-- [start:localization] -->
|
|
38
|
+
`filter_update` accepts two independent low-level hooks. `cross_covariance_modifier` receives one empirical state-observation cross-covariance and returns its modified value; it defaults to `no_covariance_modifier`. For observation-space covariance localization, one may optionally use `construct_chol_innovation_covariance`, which receives normalized observation deviations `Y` and a Cholesky factor of the observation noise covariance, `chol_R`, and returns a generalized Cholesky factor of the localized innovation covariance $C_{yy} + R$. All localization happens before handling of missing data, so localization happens in the original coordinates.
|
|
39
|
+
|
|
40
|
+
The most common form of localization is covariance tapering; cuthbertlib provides `gaspari_cohn` and `gaussian` correlation functions for this purpose. The Gaspari-Cohn correlation function is more classical, but has compact support which may cause difficulties in optimizing localization hyperparameters. The `gaussian` correlation function has infinite support, and may therefore have better gradient properties. For obsservation-space tapering, `cuthbertlib` provides a convenience `construct_tapered_chol_innovation_covariance`, which uses a Cholesky factor of the taper to construct a factor of the tapered innovation covariance. This comes at slightly higher cost, due to a larger QR solve.
|
|
41
|
+
|
|
42
|
+
<!-- --8<-- [end:localization] -->
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from cuthbertlib.ensemble_kalman.filtering import (
|
|
2
|
+
ConstructCholInnovationCovariance,
|
|
3
|
+
CrossCovarianceModifier,
|
|
4
|
+
no_covariance_modifier,
|
|
5
|
+
predict,
|
|
6
|
+
)
|
|
7
|
+
from cuthbertlib.ensemble_kalman.filtering import (
|
|
8
|
+
update as filter_update,
|
|
9
|
+
)
|
|
10
|
+
from cuthbertlib.ensemble_kalman.localization import (
|
|
11
|
+
construct_tapered_chol_innovation_covariance,
|
|
12
|
+
gaspari_cohn,
|
|
13
|
+
gaussian,
|
|
14
|
+
)
|
|
15
|
+
from cuthbertlib.ensemble_kalman.smoothing import update as smoother_update
|
{cuthbertlib-0.0.13/cuthbertlib/enkf → cuthbertlib-0.0.15/cuthbertlib/ensemble_kalman}/filtering.py
RENAMED
|
@@ -17,6 +17,23 @@ from cuthbertlib.types import Array, KeyArray, ScalarArray
|
|
|
17
17
|
|
|
18
18
|
ObservationFn = Callable[[Array], Array]
|
|
19
19
|
DynamicsFn = Callable[[Array, KeyArray], Array]
|
|
20
|
+
CrossCovarianceModifier = Callable[[Array], Array]
|
|
21
|
+
ConstructCholInnovationCovariance = Callable[[Array, Array], Array]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def no_covariance_modifier(covariance: Array) -> Array:
|
|
25
|
+
"""Return an empirical covariance unchanged.
|
|
26
|
+
|
|
27
|
+
The identity covariance modifier, used as the default when no modification
|
|
28
|
+
(e.g. localization) is requested.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
covariance: Empirical covariance matrix.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The covariance matrix, unchanged.
|
|
35
|
+
"""
|
|
36
|
+
return covariance
|
|
20
37
|
|
|
21
38
|
|
|
22
39
|
def predict(
|
|
@@ -56,6 +73,9 @@ def update(
|
|
|
56
73
|
chol_R: Array,
|
|
57
74
|
y: Array,
|
|
58
75
|
perturbed_obs: bool = True,
|
|
76
|
+
cross_covariance_modifier: CrossCovarianceModifier = no_covariance_modifier,
|
|
77
|
+
construct_chol_innovation_covariance: ConstructCholInnovationCovariance
|
|
78
|
+
| None = None,
|
|
59
79
|
) -> tuple[Array, ScalarArray]:
|
|
60
80
|
"""Update ensemble members with an observation using the EnKF update.
|
|
61
81
|
|
|
@@ -71,6 +91,16 @@ def update(
|
|
|
71
91
|
y: Observation vector, shape (y_dim,). NaNs indicate missing dimensions.
|
|
72
92
|
perturbed_obs: If True, use perturbed observations (stochastic EnKF).
|
|
73
93
|
If False, use deterministic update.
|
|
94
|
+
cross_covariance_modifier: Function that modifies the empirical
|
|
95
|
+
state-observation cross-covariance, shape (x_dim, y_dim), and returns
|
|
96
|
+
an array with the same shape. Defaults to the identity.
|
|
97
|
+
construct_chol_innovation_covariance: Optional function that
|
|
98
|
+
receives normalized observation deviations with shape (y_dim, N) and
|
|
99
|
+
``chol_R`` with shape (y_dim, y_dim). It must return a generalized
|
|
100
|
+
Cholesky factor of the complete innovation covariance with shape
|
|
101
|
+
(y_dim, y_dim). The deviations have already been divided by
|
|
102
|
+
``sqrt(N - 1)``. Both inputs use the original observation order.
|
|
103
|
+
``None`` uses the standard, unlocalized square-root construction.
|
|
74
104
|
|
|
75
105
|
Returns:
|
|
76
106
|
Tuple of (updated_ensemble, log_likelihood).
|
|
@@ -79,27 +109,46 @@ def update(
|
|
|
79
109
|
|
|
80
110
|
# Map ensemble to observation space
|
|
81
111
|
y_pred = jax.vmap(observation_fn, (0,))(predicted_ensemble)
|
|
112
|
+
x_mean = jnp.mean(predicted_ensemble, axis=0)
|
|
113
|
+
x_dev = predicted_ensemble - x_mean
|
|
114
|
+
|
|
115
|
+
missing = jnp.isnan(y)
|
|
116
|
+
|
|
117
|
+
# Modify or construct covariances before reordering due to NaNs.
|
|
118
|
+
argsort = jnp.argsort(missing, stable=True)
|
|
119
|
+
original_y_dev = y_pred - jnp.mean(y_pred, axis=0)
|
|
120
|
+
normalized_original_y_dev = original_y_dev.T / jnp.sqrt(N - 1)
|
|
121
|
+
|
|
122
|
+
C_xy = x_dev.T @ original_y_dev / (N - 1)
|
|
123
|
+
C_xy = cross_covariance_modifier(C_xy)
|
|
124
|
+
|
|
125
|
+
if construct_chol_innovation_covariance is not None:
|
|
126
|
+
original_chol_S = construct_chol_innovation_covariance(
|
|
127
|
+
normalized_original_y_dev, chol_R
|
|
128
|
+
)
|
|
82
129
|
|
|
83
130
|
# Handle partially-missing observations by reordering and zeroing missing dims.
|
|
84
131
|
# Use y_pred.T because y_pred is (N, y_dim) and we want to reorder along axis 0.
|
|
85
|
-
flag =
|
|
86
|
-
flag, chol_R, y, y_pred = collect_nans_chol(flag, chol_R, y, y_pred.T)
|
|
132
|
+
flag, chol_R, y, y_pred = collect_nans_chol(missing, chol_R, y, y_pred.T)
|
|
87
133
|
y_pred = y_pred.T
|
|
88
134
|
y_dim = y.shape[0]
|
|
89
135
|
|
|
90
|
-
# Ensemble means
|
|
91
|
-
x_mean = jnp.mean(predicted_ensemble, axis=0)
|
|
92
136
|
y_mean = jnp.mean(y_pred, axis=0)
|
|
93
|
-
|
|
94
|
-
# Deviations from ensemble mean
|
|
95
|
-
x_dev = predicted_ensemble - x_mean
|
|
96
137
|
y_dev = y_pred - y_mean
|
|
138
|
+
C_xy = C_xy[:, argsort]
|
|
139
|
+
C_xy = jnp.where(flag[None, :], 0.0, C_xy)
|
|
97
140
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
141
|
+
if construct_chol_innovation_covariance is None:
|
|
142
|
+
chol_S = tria(jnp.concatenate([y_dev.T / jnp.sqrt(N - 1), chol_R], axis=1))
|
|
143
|
+
else:
|
|
144
|
+
# The constructor sees the original indexing. Only collect and refactor its
|
|
145
|
+
# result when dimensions are missing; otherwise preserve its returned factor.
|
|
146
|
+
chol_S = jax.lax.cond(
|
|
147
|
+
jnp.any(missing),
|
|
148
|
+
lambda chol: collect_nans_chol(missing, chol)[1],
|
|
149
|
+
lambda chol: chol,
|
|
150
|
+
original_chol_S,
|
|
151
|
+
)
|
|
103
152
|
|
|
104
153
|
# Kalman gain: K = C_xy @ S^{-1} = C_xy @ cho_solve(chol_S, I)
|
|
105
154
|
K = cho_solve((chol_S, True), C_xy.T).T
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Covariance localization utilities for ensemble Kalman methods."""
|
|
2
|
+
|
|
3
|
+
import jax.numpy as jnp
|
|
4
|
+
|
|
5
|
+
from cuthbertlib.linalg import tria
|
|
6
|
+
from cuthbertlib.types import Array, ArrayLike, ScalarArrayLike
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def construct_tapered_chol_innovation_covariance(
|
|
10
|
+
Y: Array,
|
|
11
|
+
chol_taper: Array,
|
|
12
|
+
chol_R: Array,
|
|
13
|
+
) -> Array:
|
|
14
|
+
"""Construct a tapered innovation covariance factor without forming it densely.
|
|
15
|
+
|
|
16
|
+
If ``taper = chol_taper @ chol_taper.T`` and ``Y`` denotes the normalized
|
|
17
|
+
observation deviations, the returned generalized Cholesky factor ``chol_S``
|
|
18
|
+
satisfies ``chol_S @ chol_S.T = taper * (Y @ Y.T) + R``.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
Y: Observation deviations transposed and divided by the square root of one
|
|
22
|
+
less than the ensemble size, shape (y_dim, n_particles).
|
|
23
|
+
chol_taper: Factor of a positive-semidefinite observation-space taper,
|
|
24
|
+
shape (y_dim, y_dim).
|
|
25
|
+
chol_R: Cholesky factor of the observation noise covariance, shape
|
|
26
|
+
(y_dim, y_dim).
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Generalized Cholesky factor of the complete tapered innovation covariance,
|
|
30
|
+
shape (y_dim, y_dim).
|
|
31
|
+
"""
|
|
32
|
+
y_dim = Y.shape[0]
|
|
33
|
+
Y_tilde = (chol_taper[:, :, None] * Y[:, None, :]).reshape(y_dim, -1)
|
|
34
|
+
return tria(jnp.concatenate([Y_tilde, chol_R], axis=1))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def gaussian(
|
|
38
|
+
distances: ArrayLike,
|
|
39
|
+
length_scale: ScalarArrayLike,
|
|
40
|
+
) -> Array:
|
|
41
|
+
r"""Evaluates a Gaussian covariance taper.
|
|
42
|
+
|
|
43
|
+
The taper is the squared-exponential correlation function
|
|
44
|
+
$\rho(d; \ell) = \exp(-\frac{1}{2}(d / \ell)^2)$. This taper
|
|
45
|
+
has infinite support and is differentiable with respect
|
|
46
|
+
to the length scale.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
distances: Distances at which to evaluate the taper.
|
|
50
|
+
length_scale: Positive characteristic distance of the taper.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Taper values with the broadcast shape of the inputs.
|
|
54
|
+
"""
|
|
55
|
+
scaled_distances = jnp.asarray(distances) / length_scale
|
|
56
|
+
return jnp.exp(-0.5 * jnp.square(scaled_distances))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def gaspari_cohn(
|
|
60
|
+
distances: ArrayLike,
|
|
61
|
+
support_radius: ScalarArrayLike,
|
|
62
|
+
) -> Array:
|
|
63
|
+
"""Evaluates the compactly supported fifth-order Gaspari-Cohn taper.
|
|
64
|
+
|
|
65
|
+
This implements Eq. (4.10) of Gaspari and Cohn (1999),
|
|
66
|
+
https://doi.org/10.1002/qj.49712555417.
|
|
67
|
+
|
|
68
|
+
``support_radius`` is the full support radius: taper values are exactly zero
|
|
69
|
+
where the absolute distance is greater than or equal to it.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
distances: Distances at which to evaluate the taper.
|
|
73
|
+
support_radius: Positive distance at which the taper reaches zero.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Taper values with the broadcast shape of the inputs.
|
|
77
|
+
"""
|
|
78
|
+
distances = jnp.abs(distances)
|
|
79
|
+
q = 2 * distances / support_radius
|
|
80
|
+
|
|
81
|
+
inner = 1 - 5 / 3 * q**2 + 5 / 8 * q**3 + 1 / 2 * q**4 - 1 / 4 * q**5
|
|
82
|
+
|
|
83
|
+
# Avoid NaNs in computing the outer branch of the polynomial
|
|
84
|
+
safe_q = jnp.where(q > 0, q, 1)
|
|
85
|
+
# Factored version of Eq. (4.10) for numerical stability
|
|
86
|
+
outer = (2 - q) ** 4 * (2 * q**2 + 4 * q - 1) / (24 * safe_q)
|
|
87
|
+
|
|
88
|
+
within_support = jnp.where(q <= 1, inner, outer)
|
|
89
|
+
return jnp.where(distances < support_radius, within_support, 0)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Implements the Ensemble Rauch-Tung-Striebel (EnRTS) smoother update.
|
|
2
|
+
|
|
3
|
+
Cf. [Raanes (2016)](https://doi.org/10.1002/qj.2728).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import jax.numpy as jnp
|
|
7
|
+
|
|
8
|
+
from cuthbertlib.types import Array
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def update(
|
|
12
|
+
filtered_ensemble: Array,
|
|
13
|
+
predicted_ensemble: Array,
|
|
14
|
+
next_smoothed_ensemble: Array,
|
|
15
|
+
) -> tuple[Array, Array]:
|
|
16
|
+
"""Applies one EnRTS smoother update step.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
filtered_ensemble: Filtered ensemble at time t, shape (N, x_dim).
|
|
20
|
+
predicted_ensemble: Paired forecast ensemble at time t + 1,
|
|
21
|
+
shape (N, next_x_dim).
|
|
22
|
+
next_smoothed_ensemble: Smoothed ensemble at time t + 1,
|
|
23
|
+
shape (N, next_x_dim).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Tuple of the smoothed ensemble at time t and the EnRTS gain.
|
|
27
|
+
"""
|
|
28
|
+
filtered_dev = filtered_ensemble - jnp.mean(filtered_ensemble, axis=0)
|
|
29
|
+
predicted_dev = predicted_ensemble - jnp.mean(predicted_ensemble, axis=0)
|
|
30
|
+
|
|
31
|
+
gain = filtered_dev.T @ jnp.linalg.pinv(predicted_dev.T)
|
|
32
|
+
|
|
33
|
+
smoothed_ensemble = (
|
|
34
|
+
filtered_ensemble + (next_smoothed_ensemble - predicted_ensemble) @ gain.T
|
|
35
|
+
)
|
|
36
|
+
return smoothed_ensemble, gain
|
|
@@ -63,14 +63,14 @@ def linearize_moments(
|
|
|
63
63
|
- [sqrt-parallel-smoothers](https://github.com/EEA-sensors/sqrt-parallel-smoothers/blob/main/parsmooth/linearization/_extended.py)
|
|
64
64
|
"""
|
|
65
65
|
if has_aux:
|
|
66
|
-
|
|
66
|
+
mean_and_chol_cov_function_with_aux = cast(
|
|
67
67
|
MeanAndCholCovFuncAux, mean_and_chol_cov_function
|
|
68
68
|
)
|
|
69
69
|
|
|
70
70
|
def mean_and_chol_cov_function_wrapper_aux(
|
|
71
71
|
x: ArrayLike,
|
|
72
72
|
) -> tuple[Array, tuple[Array, Array, ArrayTree]]:
|
|
73
|
-
mean, chol_cov, aux =
|
|
73
|
+
mean, chol_cov, aux = mean_and_chol_cov_function_with_aux(x)
|
|
74
74
|
return mean, (mean, chol_cov, aux)
|
|
75
75
|
|
|
76
76
|
F, (m, *extra) = jax.jacfwd(
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# ty: ignore[invalid-return-type]
|
|
2
|
+
# f-strings are not supported docstrings and ty is confused about this
|
|
3
|
+
# See https://github.com/state-space-models/cuthbert/issues/261
|
|
1
4
|
"""Shared protocols for resampling algorithms."""
|
|
2
5
|
|
|
3
6
|
from typing import Protocol, runtime_checkable
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Ensemble Kalman Filter (EnKF)
|
|
2
|
-
|
|
3
|
-
This sub-repository provides modular functions for the Ensemble Kalman Filter.
|
|
4
|
-
|
|
5
|
-
The core functions are:
|
|
6
|
-
|
|
7
|
-
- `predict`: Propagate ensemble members through nonlinear dynamics with additive Gaussian noise.
|
|
8
|
-
- `update`: Update ensemble members with an observation using the EnKF update equation.
|
|
9
|
-
|
|
10
|
-
Together, `predict` and `update` can be used to perform an online EnKF filtering step.
|
|
11
|
-
|
|
12
|
-
The EnKF uses an ensemble of particles with a Kalman-style measurement update based on
|
|
13
|
-
empirical covariances. Unlike the EKF, it does not require Jacobians, while naturally
|
|
14
|
-
handling nonlinear dynamics.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from cuthbertlib.enkf.filtering import predict, update
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|