Make CD filters return predicted observation distributions#29
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends the continuous-discrete nonlinear Gaussian filtering APIs to expose predictive emission information via posterior_extras, enabling downstream evaluation of predicted observation distributions (e.g., via scoring rules).
Changes:
- Add per-step predictive emission outputs to EKF/UKF (
y_pred_mean,y_pred_cov) and expose predicted observation ensembles in EnKF (y_ens_pred) viaposterior_extras. - Clarify
output_fields/posterior_extrassemantics in docstrings. - Add regression tests covering
posterior_extrasandoutput_fields=["posterior_extras"]behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
tests/test_nonlinear_gaussian_filter_posterior_extras.py |
Adds regression tests for new posterior_extras outputs and minimal-output mode. |
cd_dynamax/src/continuous_discrete_nonlinear_gaussian_ssm/models.py |
Updates docstrings to describe output_fields and posterior_extras usage. |
cd_dynamax/src/continuous_discrete_nonlinear_gaussian_ssm/inference_ekf.py |
Computes and returns EKF predictive emission moments in posterior_extras. |
cd_dynamax/src/continuous_discrete_nonlinear_gaussian_ssm/inference_ukf.py |
Refactors UKF conditioning to surface predictive emission moments in posterior_extras. |
cd_dynamax/src/continuous_discrete_nonlinear_gaussian_ssm/inference_enkf.py |
Exposes predicted observation ensemble (y_ens_pred) in EnKF posterior_extras. |
cd_dynamax/dynamax/linear_gaussian_ssm/inference.py |
Documents optional predicted-state fields and posterior_extras on PosteriorGSSMFiltered. |
Comments suppressed due to low confidence (1)
cd_dynamax/src/continuous_discrete_nonlinear_gaussian_ssm/inference_ukf.py:466
posterior_extrasonly returnsy_pred_mean/y_pred_cov(noiseless predictive emission moments). For scoring rules against observed data, the observation-level predictive covariance (includingR) is needed; consider also returningy_obs_pred_meanandy_obs_pred_cov = y_pred_cov + Rso callers don’t have to recompute time-varyingRdownstream.
filter_hyperparams,
warn=warn,
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "posterior_extras": { | ||
| "y_pred_mean": y_pred_mean, | ||
| "y_pred_cov": y_pred_cov, | ||
| }, |
| extras = posterior.posterior_extras | ||
| assert extras is not None | ||
| assert extras["y_pred_mean"].shape == (3, 1) | ||
| assert extras["y_pred_cov"].shape == (3, 1, 1) | ||
| assert jnp.all(jnp.isfinite(extras["y_pred_mean"])) | ||
| assert jnp.all(jnp.isfinite(extras["y_pred_cov"])) | ||
|
|
||
| assert jnp.allclose(extras["y_pred_mean"][0], params.initial.mean.f()) | ||
| assert jnp.allclose(extras["y_pred_cov"][0], params.initial.cov.f()) | ||
| assert jnp.allclose(extras["y_pred_mean"][1:], posterior.predicted_means[:-1]) | ||
| assert jnp.allclose(extras["y_pred_cov"][1:], posterior.predicted_covariances[:-1]) |
| output_fields: list of top-level posterior fields to return. | ||
| These can include "filtered_means", "filtered_covariances", | ||
| "predicted_means", "predicted_covariances", "marginal_loglik", | ||
| and "posterior_extras". For EKF/UKF/EnKF, `posterior_extras` | ||
| contains filter-specific per-step diagnostics such as predictive | ||
| emission moments. |
|
@copilot review again (new commit posted) |
Addressed all four review points (now in commits
|
b9575de to
c0a800c
Compare
| :param filtered_covariances: array of filtered covariances $\mathrm{Cov}[z_t \mid y_{1:t}, u_{1:t}]$ | ||
| :param predicted_means: optional array of one-step-ahead state means $\mathbb{E}[z_{t+1} \mid y_{1:t}, u_{1:t}]$ | ||
| :param predicted_covariances: optional array of one-step-ahead state covariances $\mathrm{Cov}[z_{t+1} \mid y_{1:t}, u_{1:t}]$ | ||
| :param posterior_extras: optional dictionary of filter-specific per-step diagnostics |
There was a problem hiding this comment.
param posterior_extras: stores per-step predictive emission, based on one-step-ahead state predictions z_{t+1}:
moments y_pred_mean & y_pred_cov:
moments y_obs_pred_mean & y_obs_pred_cov, where y_obs_pred_cov = y_pred_cov + R.
| - S: innovation covariance used for assimilation. This equals the | ||
| covariance of the predictive observation ensemble used in the | ||
| update, plus `R`. When inflation is enabled, that covariance is | ||
| computed from the inflated ensemble. |
There was a problem hiding this comment.
I would add the tuple "y_pred_mean", "y_pred_cov", "y_obs_pred_mean", "y_obs_pred_cov"
iurteaga
left a comment
There was a problem hiding this comment.
I left some comments and edit suggestions, mostly on having a consistent API for posterior extras.
Also, maybe worth revising the docstrings to specifically indicate that this predicted emissions are conditioned on predicted state z_{t+1}
|
Thanks! Do you think these should all just become (optional) parts of the filtered posterior itself? This way a user can decide whether to return some/all of them. Currently, you have to take or leave the entire posterior_extras field, which I don't love. |
…s (and their ensembles/statistics)
|
@iurteaga I updated things quite a bit to expand the |
DanWaxman
left a comment
There was a problem hiding this comment.
This all looks reasonable to me. I have two main comments:
-
Should we be also updated filtered dataclasses? E.g., we are often actually returning something like a
PosteriorGSSMFiltered:
cd_dynamax/cd_dynamax/dynamax/linear_gaussian_ssm/inference.py
Lines 112 to 127 in df24b56
This does not currently have all possible fields listed. -
From a design perspective, I find it slightly unfortunate we're passing around a bunch of dictionaries with possibly missing keys or empty values. I think it would be beneficial to have class
FilterOutput(or even pass around partial initializations ofPosteriorGSSMFiltered). Though I acknowledge this may be better served as part of The Great Refactor.
|
Wow, TIL that if you use the GitHub magic links feature it will just randomly throw you back to main when you try to follow a code reference... GitHub never fails to amaze me :D
Fair enough -- I think this looks good me then! |
Co-authored-by: Iñigo Urteaga <urteaga@gmail.com>
mattlevine22
left a comment
There was a problem hiding this comment.
Perfect, made those changes and will merge!
Summary
This PR adds predictive emission information to the continuous-discrete Gaussian filters so downstream code can evaluate both latent signal uncertainty and observation-level uncertainty from filter outputs.
Why? We want to evaluate filters using scoring rules that compare predicted observation distributions versus the observed data; this will allow us to not only evaluate, but also tune filters w.r.t. various metrics.
Specifically:
KF,EKF,UKF, andEnKFnow return predictive emission fields inposterior_extrasoutput_fieldsand predictive output semanticsExternal dependency
There is an open Dynestyx PR for filter-scoring that relies on this PR here.
What’s included
KF / EKF / UKF
These filters now expose both of the following in
posterior_extras:y_pred_meany_pred_covy_obs_pred_meany_obs_pred_covThe intended distinction is:
y_pred_*: predictive moments in observation space before adding observation noisey_obs_pred_*: predictive moments for the actual observed dataIn other words:
y_pred_mean = E[h(x_t) | y_{1:t-1}]y_pred_cov = Cov[h(x_t) | y_{1:t-1}]y_obs_pred_mean = y_pred_meany_obs_pred_cov = y_pred_cov + RThis is useful because different downstream tasks want different objects:
y_pred_*[OPTIONAL]y_obs_pred_*[ESSENTIAL TO THIS PR]EnKF
For EnKF, this PR newly exposes in
posterior_extras:y_ens_pred: predicted observation ensembleUnlike KF/EKF/UKF, EnKF does not return explicit
y_pred_mean/y_pred_covfields. Instead:y_ens_predSWe now document
Smore clearly as the observation-predictive covariance used for assimilation and data scoring, i.e. the ensemble predictive covariance plus observation noiseR.This keeps EnKF lightweight while still exposing the important predictive information.
Why
y_pred_*vsy_obs_pred_*mattersThere’s an important modeling distinction here:
y_pred_covreflects uncertainty from the latent state propagated into observation spacey_obs_pred_covreflects uncertainty in the actual measured observation, so it includesRIf we want to evaluate the predictive distribution of the observed data, the correct object is
y_obs_pred_cov, noty_pred_cov.That distinction is now explicit in the API instead of being left implicit in downstream code.
Why DPF is not included
This PR does not extend
DPF, as the predicted observations are not computed explicitly (hence, a bit more thought and work is needed for how to do this appropriately).Testing
Added regression coverage for:
y_pred_*and state predictive moments for KF/EKF/UKFy_obs_pred_cov = y_pred_cov + Routput_fields=["posterior_extras"]behaviory_ens_predand documentedS