This page is for the reader who wants the machinery. The implementation is clocks.inference.ParticleFilter — a few hundred lines, and everything below maps onto it directly.
State
\(N\) particles, each a full hypothesis for the unknown parameters (e.g. \((x, M)\) in 1D), with weights summing to one. Initialization draws the cloud from the prior — typically uniform over the position and mass ranges.
Update
Per observation, each particle’s log-weight gains the Gaussian log-likelihood of the observed rates given that particle’s predicted rates:
plus a log-prior that is \(-\infty\) outside the bounds (negative mass, position out of range) — invalid particles die instantly rather than being clamped. Normalization happens in log space (log-sum-exp), because with \(\sigma = 0.005\) the raw likelihoods underflow immediately.
Resampling
A few sharp observations leave most particles with negligible weight. The effective sample size\(\mathrm{ESS} = 1/\sum_i w_i^2\) measures the collapse; when it drops below resample_threshold · N, the filter redraws \(N\) particles in proportion to weight and perturbs the clones with jitter so they don’t sit on top of each other. Three resampling schemes are available: systematic (the default) walks one evenly-spaced comb with a single random offset through the cumulative weights, giving minimal variance; stratified draws one uniform sample per equal-probability stratum, trading a little variance for independence between strata; residual deterministically copies each particle \(\lfloor N w_i \rfloor\) times and only randomizes the remainder. Two jitter modes:
fixed — isotropic Gaussian noise with absolute scale jitter_std.
covariance — noise shaped by the weighted empirical covariance of the cloud, scaled by jitter_std. When the posterior is a thin diagonal ridge (a “banana”), isotropic jitter pushes particles off the ridge; covariance jitter slides them along it.
The covariance mode has a guard worth knowing about: with fewer than two effective particles the weighted covariance is mathematically undefined (np.cov’s normalization \(1 - \sum w_i^2\) hits zero), so the filter falls back to isotropic jitter for that resample rather than crashing or freezing.
Evidence
Each update’s increment \(\log \sum_i w_i L_i\) is the marginal likelihood of that observation under the model — how well the model predicted the data before seeing it. Summed over observations this is the log-evidence, the quantity model comparison ranks.
Effective sample size over 25 observations. Sharp likelihoods crash it; resampling (below the dashed threshold) resets it to N.
WarningFailure modes
Three ways this machinery can lie to you, and what the library does about each:
Weight collapse. Every particle lands at \(-\infty\) log-weight (prior and data contradict). The filter raises RuntimeError instead of propagating NaN.
Particle impoverishment. After resampling, the cloud consists of near-identical clones; if the jitter is too weak the posterior freezes and reports false certainty. This is the failure that motivated the demos shipping with fixed jitter.
Label switching. With multiple masses, the posterior is symmetric under relabeling; the sorting constraint breaks it, at a known cost near the sort boundary.