When linear-Gaussian breaks
Three things break the Kalman filter's assumptions: nonlinear tuning (firing that is not a linear function of the state), non-Gaussian noise (Poisson spikes, heavy tails, artifacts), and multimodal posteriors (two plausible movements at once). Three families of filter relax those, at rising computational cost: the extended, the unscented, and the particle filter.
EKF and UKF
The extended Kalman filter keeps the Gaussian machinery but linearizes a nonlinear observation function h(\cdot) around the current estimate, using its Jacobian as a local C. It is cheap and works when the nonlinearity is mild, but the linearization can diverge if the estimate is far from the truth.
\hat{x}_k = \hat{x}_k^{-} + K_k\big(y_k - h(\hat{x}_k^{-})\big), \qquad C_k = \left.\frac{\partial h}{\partial x}\right|_{\hat{x}_k^{-}}EKF: replace the linear C with the Jacobian of the true observation function, evaluated at the prediction.
When the neural-to-state relationship is curved, the plain Kalman filter breaks. The EKF fixes it by linearizing — replacing the constant C with the local slope (Jacobian) of the true function at the current best guess.
- h(\cdot)
- The true nonlinear observation function.
- \hat{x}_k^{-}
- The predicted state, used as the linearization point.
- C_k
- The Jacobian — the local slope of h at the prediction.
- K_k
- The Kalman gain, now built from C_k.
This is the extended Kalman filter; it works well when h is only mildly curved near the estimate.
The unscented Kalman filter avoids Jacobians entirely. It picks a small set of deterministic sigma points, pushes each through the true nonlinearity, and reconstructs a Gaussian from the transformed cloud. For moderate-to-strong nonlinearity it is usually more accurate than the EKF at similar cost.
\mathcal{X}^{(i)} = \hat{x} \pm \Big(\sqrt{(n+\lambda)\,P}\Big)_i, \qquad \hat{y} = \sum_i W_i\, h\big(\mathcal{X}^{(i)}\big)The unscented transform: propagate sigma points through h directly, then reweight — no derivatives needed.
Instead of differentiating, the UKF picks a few clever sample points — sigma points — around the estimate, pushes each through the real nonlinear function, and re-averages. It captures the curvature with no derivatives.
- \mathcal{X}^{(i)}
- The sigma points placed around the current estimate.
- \sqrt{(n+\lambda)\,P}
- The spread of the sigma points, set by the uncertainty P.
- W_i
- The weights used to recombine the transformed points.
- h(\cdot)
- The nonlinear observation function each point passes through.
This is the unscented Kalman filter; it usually beats the EKF when h is strongly curved.
Particle filters
When the posterior is genuinely non-Gaussian or multimodal, represent it not by a mean and covariance but by a swarm of weighted samples (particles). Each particle is propagated through the dynamics, reweighted by how well it explains the new measurement, and the swarm is periodically resampled. This is the particle filter, the most general realization of the guide-1 Bayes recursion.
w_k^{(i)} \propto w_{k-1}^{(i)}\, p\big(y_k \mid x_k^{(i)}\big), \qquad \hat{x}_k = \sum_i w_k^{(i)}\, x_k^{(i)}Sequential importance weighting; the posterior mean is the weighted average of the particles.
Represent the belief with a cloud of guesses (particles). Each round, weight every guess by how well it predicted the new data, and take the weighted average as your estimate. Handles any shape of distribution.
- x_k^{(i)}
- The i-th particle — one candidate state.
- w_k^{(i)}
- That particle's importance weight.
- p(y_k \mid x_k^{(i)})
- How well the particle explains the new measurement.
- \hat{x}_k
- The weighted-average estimate over all particles.
This is the particle filter — flexible but costly, since accuracy grows with the number of particles.
The closed-loop twist
Here is the single most important practical lesson of the track. Every filter above is usually fit on open-loop data — the user passively watches a moving target while you record. But the instant you close the loop and let the user drive, their neural activity changes: they now react to cursor error, correct, and adapt. The training statistics no longer match the test statistics (covariate shift), and a decoder that was statistically optimal offline can feel sluggish, biased, or unstable online.
ReFIT and closed-loop decoder adaptation
The fix is to train on data that reflects the user's intention, not their imperfect open-loop cursor. The insight behind ReFIT is that during calibration the user almost always intends to move straight toward the target. So you take the observed cursor velocities, rotate each to point at the goal (keeping its speed), and refit the Kalman filter on these intention-corrected velocities.
v_k^{\text{intended}} = \lVert v_k \rVert \cdot \frac{p_{\text{target}} - p_k}{\lVert p_{\text{target}} - p_k \rVert}Intention estimation: keep the decoded speed, but redirect it toward the known target before retraining.
A retraining trick: assume the user always meant to head straight for the target, so keep the decoded speed but rotate its direction to point at the goal, then retrain on these corrected intentions. It sharply boosts accuracy.
- \lVert v_k \rVert
- The decoded speed, which is kept as-is.
- p_{\text{target}}
- The known goal position.
- p_k
- The current cursor position.
- v_k^{\text{intended}}
- The velocity redirected straight at the target.
This intention estimation is the heart of ReFIT closed-loop decoder adaptation.
# ReFIT: rotate observed velocity toward the target, keep the speed
def refit_intention(pos, vel, target):
to_target = target - pos
dirn = to_target / (np.linalg.norm(to_target) + 1e-9)
speed = np.linalg.norm(vel)
return speed * dirn # intended velocity used to refit C, Q of the KFGeneralizing ReFIT, closed-loop decoder adaptation (CLDA) repeats this recalibration while the user drives, updating the decoder over seconds to minutes so it converges with the user rather than against them.