Skip to content

Conversation

@charlesknipp
Copy link
Member

@charlesknipp charlesknipp commented Nov 20, 2025

Type stability is a very non-trivial problem since filtering is a recursive problem where weights aren't initialized until the first step of the algorithm.

To get around this, I initialize particles with weight of false (can be more generic) using a basic implementation of UnweightedParticles. I use a Boolean since it's type is generic enough to afford a single type promotion to encourage the correct types. I define a typeless initializer fro unweighted particles that promote to the correct type when weights are eventually computed in step 1.

Auxiliary Filter

The APF was somewhat of a challenge to get right. The code not only relied on updating particle weights, but required a sequence of marginalizing constants to keep canonical likelihoods. To remedy it's shortcomings, I created a custom resampler AuxiliaryResampler which handles all of the code previously in particles.jl in a type stable fashion. This was made possible by a cacophony of changes to the resampling behavior.

NOTE: I'm still a little shaky on the log likelihood computation here (see here). Intuitively it makes sense, but it seems like way more logsumexps than should be necessary. In particles it seems to be far less work to get the likelihoods.

Resampling

As mentioned, the resampling had quite a makeover. Instead of completely redefining the resample family of functions, I pass the combined weights (set default to get_weights(state)) and auxiliary weights as a keyword argument. This intelligently dispatches to the correct method without the invasive overwriting I had in previous drafts. It should also be much more efficient than previous drafts since we only need to softmax the weights once.

@github-actions
Copy link
Contributor

SSMProblems.jl/SSMProblems documentation for PR #122 is available at:
https://TuringLang.github.io/SSMProblems.jl/SSMProblems/previews/PR122/

@codecov
Copy link

codecov bot commented Nov 20, 2025

Codecov Report

❌ Patch coverage is 79.81651% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.26%. Comparing base (018f554) to head (98b6c2a).

Files with missing lines Patch % Lines
GeneralisedFilters/src/containers.jl 66.66% 13 Missing ⚠️
GeneralisedFilters/src/resamplers.jl 80.95% 8 Missing ⚠️
GeneralisedFilters/src/GFTest/resamplers.jl 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #122      +/-   ##
==========================================
- Coverage   66.30%   65.26%   -1.05%     
==========================================
  Files          29       30       +1     
  Lines        1211     1212       +1     
==========================================
- Hits          803      791      -12     
- Misses        408      421      +13     
Flag Coverage Δ
GeneralisedFilters 66.81% <79.81%> (-1.13%) ⬇️
SSMProblems 43.90% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions
Copy link
Contributor

SSMProblems.jl/GeneralisedFilters documentation for PR #122 is available at:
https://TuringLang.github.io/SSMProblems.jl/GeneralisedFilters/previews/PR122/

charlesknipp and others added 3 commits November 20, 2025 10:54
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@charlesknipp
Copy link
Member Author

charlesknipp commented Nov 20, 2025

FINALLY

Things are looking good. I have been running the variational filter with Float32s and all is now type stable (for the most part). It just needs some cleaning up and optimization to get merged.

Like I said in the description, this is technically slower than before but only because of some obvious cheap tricks I employ in resampling see here and here. There are likely some other issues with the "generics", but a standalone AuxiliaryResampler should address these issues.

The AuxiliaryResampler is now implemented and fully type stable.

@THargreaves I would definitely appreciate some feedback. Also wondering how we could better treat the marginalize! process. This should be the only place in which the mutability of ParticleDistribution / Particle is used, so getting around that would be elite.

@charlesknipp charlesknipp marked this pull request as ready for review November 20, 2025 21:25
Copy link
Member Author

@charlesknipp charlesknipp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I provide a little clarity for my design choices here. Just let me know if there's anything else that jumps out.

Comment on lines +102 to 104
for p in particles
p.log_w -= LSE_after
end
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only issue with this entire PR is this single line of code. If we could get rid of this, we no longer have to rely on the mutation of Particles and ParticleDistributions

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be on the complete wrong page, but what's stopping us from just constructing new particles using the old weights - LSE_after, rather than mutating?

Just some fixes I noticed when going through my review
Copy link
Collaborator

@THargreaves THargreaves left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a really nice PR. Thank you so much for taking the time to go through and make all these changes. Looks like it was a bit of a slog but the result looks great.

I don't have any real comments, just a few points of confusion that you can probably clear-up.

LSE before adding observation weights. For APF with resampling, it includes first-stage
correction terms computed during the APF resampling step.
"""
function marginalise!(state::ParticleDistribution)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for passing in both state and particles. Can we not use particles = state.particles?

Comment on lines +102 to 104
for p in particles
p.log_w -= LSE_after
end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be on the complete wrong page, but what's stopping us from just constructing new particles using the old weights - LSE_after, rather than mutating?

@THargreaves
Copy link
Collaborator

THargreaves commented Nov 27, 2025

I've had to update the trend inflation example to comply with the new PDMat types for covariance matrices.

I've had to write

local_level_model = StateSpaceModel(
    GF.HomogeneousGaussianPrior(zeros(T, 1), PDMat([100.0;;])),
    LocalLevelTrend(),
    OutlierAdjustedObservation(),
)

even though I'd really like something like

local_level_model = StateSpaceModel(
    GF.HomogeneousGaussianPrior(zeros(T, 1), ScalMat(1, 100.0)),
    LocalLevelTrend(),
    OutlierAdjustedObservation(),
)

The reason for this is that if we currently set the prior to be a ScalMat, the covariance will have become a PDMat after one predict-update and so can't be written back into the posterior container.

I'm hoping that this PR will fix this behaviour and we use the second code snippet.

@charlesknipp
Copy link
Member Author

@THargreaves I need you to take a look at the TypelessBaseline and potentially clean it up a bit. What is here currently works, but it fails the Aqua tests because of some strange ambiguity.

@yebai I added a very basic unit test using JET. What I have is very naive and should be iterated on. I have very little experience with JET myself, so if you don't mind pointing me in the right direction (like other repos with JET unit tests) I can swiftly iterate on this.

With those two items out of the way, I think we can safely merge this PR.

@yebai
Copy link
Member

yebai commented Dec 2, 2025

Mooncake uses JET fairly comprehensively to test type stability and allocation, see here.

@yebai
Copy link
Member

yebai commented Dec 3, 2025

The two main JET tests useful are

The docs contains examples. Let me know if anything is unclear.

@THargreaves
Copy link
Collaborator

TypelessZero, TypelessBaseline look good to me. Nice coverage of the important cases.

You just had the promote_rule the wrong way around which caused the Aqua failure (plus I just had to create some constructors—couldn't see a way around that).

Agree the definition of softmax is a bit janky but I think it's probably fine

@THargreaves THargreaves merged commit 2282c15 into main Dec 5, 2025
22 of 25 checks passed
@THargreaves THargreaves deleted the ck/type-stability branch December 5, 2025 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants