Add Cuthbert EnKF#209
Conversation
There is now an EnKF in cuthbert, as of v0.0.10. This commit adds integrations to this filter, including: - Upgrading the required cuthbert version - Updating the corresponding filter_config information - Make EnKF the default discrete-time filters - Add EnKF integration code - Update tests accordingly - Add to documentation (including discrete-time LTI profile likelihood notebook)
There was a problem hiding this comment.
Pull request overview
This PR integrates the new cuthbert-backed discrete-time Ensemble Kalman Filter (EnKF) into dynestyx, makes it the default discrete-time filter, and updates configs/tests/docs accordingly.
Changes:
- Bump
cuthbert/cuthbertlibminimum versions to0.0.10and add EnKF dispatch/integration in the cuthbert discrete backend. - Make
EnKFConfig()the default discrete-timeFilter()configuration and update config documentation accordingly. - Extend/adjust tests to cover EnKF behavior (shapes, observation callable support, deterministic CRN behavior), and update docs/navigation.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
pyproject.toml |
Updates required cuthbert versions to pick up EnKF support. |
dynestyx/inference/filter_configs.py |
Updates EnKFConfig docs/default backend (cuthbert) and clarifies default filter role. |
dynestyx/inference/integrations/cuthbert/discrete.py |
Adds EnKF backend integration and unifies Gaussian-site recording helper. |
dynestyx/inference/integrations/utils.py |
Adds covariance_from_cholesky helper used across integrations. |
dynestyx/inference/filters.py |
Switches default discrete-time filter to EnKFConfig() and uses shared covariance helper. |
tests/fixtures.py |
Extends fixtures to include EnKF configs and parametrizations. |
tests/test_filters.py |
Adds EnKF-specific tests and a default-filter smoke test. |
docs/index.md |
Updates external library description to mention EnKF support in Cuthbert. |
docs/faq.md |
Updates guidance to reflect EnKF default and links to a new deep dive notebook. |
docs/api_reference/public/inference/filter_configs.md |
Updates the filter config summary table to reflect EnKF as default discrete filter. |
mkdocs.yml |
Adds a new deep dive notebook to the docs nav. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Agent-Logs-Url: https://github.com/BasisResearch/dynestyx/sessions/804f2085-9a89-43cc-890b-77758f2f48ab Co-authored-by: DanWaxman <11810745+DanWaxman@users.noreply.github.com>
| if isinstance(filter_config, PFConfig): | ||
| _add_sites_pf(name, states, record_kwargs) | ||
| particles = states.particles | ||
| particles = states.particles[1:] |
There was a problem hiding this comment.
is this [1:] (and subsequents) catching a previous off-by-one error??
There was a problem hiding this comment.
If so, it seems like this problem should be managed upstream, e.g. in compute_cuthbert_filter or even cuthbert_filter.
Currently, the batched filter computations would not pass through the above changes (but does use compute_cuthbert_filter).
There was a problem hiding this comment.
And is there a test that would have failed before but now can pass? Wondering how we missed this...
There was a problem hiding this comment.
And is there a test that would have failed before but now can pass? Wondering how we missed this...
Yup, added a test for this test_filters.py::test_cuthbert_filtered_distribution_shapes_match_observations. The error was mostly innocuous, which is why we didn't see it before. This only gets passed forward as filtered_dists, and it didn't affect the prediction behavior at all because of how scans work.
If so, it seems like this problem should be managed upstream, e.g. in compute_cuthbert_filter or even cuthbert_filter.
Yeah, that seems fair.
|
Oops yes, sorry meant to write that in the description. Notably, things are
already sliced when adding sites.
…On Wed, Apr 22, 2026 at 4:33 PM Matt Levine ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In dynestyx/inference/integrations/cuthbert/discrete.py
<#209?email_source=notifications&email_token=AC2DPOIMGJ3ZMCQOYJBLEQT4XET73A5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTIMJVG43TOMBUG43KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K64DSL5ZGK5TJMV3V6Y3MNFRWW#discussion_r3126750278>
:
> @@ -158,17 +229,18 @@ def run_discrete_filter(
if isinstance(filter_config, PFConfig):
_add_sites_pf(name, states, record_kwargs)
- particles = states.particles
+ particles = states.particles[1:]
is this [1:] (and subsequents) catching a previous off-by-one error??
—
Reply to this email directly, view it on GitHub
<#209?email_source=notifications&email_token=AC2DPONOG2LJ5JQUKSPXOJL4XET73A5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTIMJVG43TOMBUG43KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2L24DSL5ZGK5TJMV3V63TPORUWM2LDMF2GS33OONPWG3DJMNVQ#pullrequestreview-4157770476>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AC2DPOOG6WZ46ULTCQHHXJD4XET73AVCNFSM6AAAAACYCXIENWVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHM2DCNJXG43TANBXGY>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
| if not isinstance(obs_model, (LinearGaussianObservation, GaussianObservation)): | ||
| probe_u = jnp.zeros(()) | ||
| probe_t = jnp.zeros(()) | ||
| try: | ||
| probe_d0 = obs_model(jnp.zeros((state_dim,)), probe_u, probe_t) | ||
| probe_d1 = obs_model(jnp.ones((state_dim,)), probe_u, probe_t) | ||
| except Exception: | ||
| probe_d0 = probe_d1 = None | ||
| if probe_d0 is not None and probe_d1 is not None: | ||
| chol0 = _extract_gaussian_chol(probe_d0, obs_dim) | ||
| _check_state_independent_noise(chol0, probe_d1, obs_dim) |
There was a problem hiding this comment.
would be more readable as a utility function.
| filtered_cov = covariance_from_cholesky(chol_cov) | ||
|
|
||
| if add_filtered_states_cov: | ||
| numpyro.deterministic(f"{name}_filtered_states_cov", filtered_cov) |
There was a problem hiding this comment.
At 650 lines and supporting 4 different filters and their own utilities (and some shared), I think this file deserves some refactoring into multiple files.
There was a problem hiding this comment.
Agreed. I started this actually in #192. I think it makes sense to continue the refactoring there.
mattlevine22
left a comment
There was a problem hiding this comment.
Main thoughts are:
- There were some off-by-one fixes
[1:]...
- how did we miss these? Can we have a test to protect us?
- To what extent can this be handled higher up in the Cuthbert integration (e.g.
cuthbert_filter)? - And is this fixed for the batched case as well (different branches of filters it seems)
- The
cuthbert/discrete.pyfile has gotten really long and tough to follow.
- Would highly recommend refactoring into multiple files and some shared utilities.
The issue was mostly innocuous, it just got passed forward to
This is a good point, handled in 7e3c3fc. We don't have access to
Yup, also done by doing this in
Agreed. This is part underway in #192, which I think is a more natural place to undertake this refactoring. |
mattlevine22
left a comment
There was a problem hiding this comment.
Looks good.
- Confirmed that Tutorial docs (NUTS + SVI) produce similar outputs (and speed) with EnKF and EKF
- Confirmed that test_science for discretized L63 produces similar posteriors w/ NUTS + EKF and NUTS + EnKF
There is now an EnKF in cuthbert, as of v0.0.10. This commit adds integrations to this filter, including:
Remaining: