"The extended filter asked the function for its slope and believed the answer. I prefer to send a small delegation through the function and see who comes back."
A Prudent AI Agent
The Big Picture
The extended Kalman filter of Section 10.1 keeps the Gaussian machinery alive by replacing each nonlinear model with its first-order Taylor expansion at the current mean. That works when the function is gently curved over the width of your uncertainty, and it fails, sometimes silently, when it is not: the Jacobian describes the tangent line, not the function, and a confidently wrong tangent produces a confidently wrong covariance. The unscented Kalman filter (UKF) attacks the same problem from the opposite side. Instead of linearizing the function, it approximates the distribution. It picks a small, carefully chosen set of sample points, the sigma points, pushes each one through the true nonlinear function untouched, and reconstructs a Gaussian from where they land. No derivatives, no tangent lines, no Jacobians to derive or debug. This section builds the unscented transform that does the pushing, wires it into the predict-correct recursion, and shows why "propagate points, then refit" beats "differentiate, then propagate" for the moderately nonlinear models that fill real sensor systems.
This section assumes the linear Kalman recursion and the meaning of the state mean and covariance from Chapter 9, the linearization idea and Jacobian notation from Section 10.1, and comfort with Gaussians, covariance, and matrix square roots from Chapter 4. Like the EKF, the UKF still assumes the belief is a single Gaussian; when that assumption itself breaks, you reach for the particle filter of Section 10.3.
The idea: approximate the distribution, not the function
Here is the intuition that names the method. It is easier to approximate a probability distribution than an arbitrary nonlinear function. If you want to know what a Gaussian looks like after passing through some curved map \(g\), you do not need to know \(g\) everywhere; you need to know it at a handful of representative places. The unscented transform chooses those places deterministically so that they carry the mean and covariance of the input exactly, propagates them through the real \(g\), and reads the output mean and covariance back off the transformed set. The claim, which the derivation makes precise, is that this captures the true posterior mean and covariance to second order in the Taylor expansion for any nonlinearity, and to third order when the input is Gaussian and the weights are tuned. The EKF, by contrast, is accurate only to first order. You get better statistics for the same Gaussian-in, Gaussian-out contract, and you never touch a derivative.
Choosing the sigma points
Let the state have dimension \(n\), with mean \(\hat{x}\) and covariance \(P\). The unscented transform draws \(2n+1\) sigma points: one at the mean, and a symmetric pair along each of the \(n\) principal axes of the covariance. The spread along axis \(i\) is set by a column of the matrix square root \(\sqrt{(n+\lambda)P}\), usually the lower-triangular Cholesky factor, so the cloud has exactly the covariance \(P\) scaled by \(n+\lambda\):
$$\mathcal{X}_0 = \hat{x}, \qquad \mathcal{X}_i = \hat{x} + \left(\sqrt{(n+\lambda)P}\right)_i, \qquad \mathcal{X}_{i+n} = \hat{x} - \left(\sqrt{(n+\lambda)P}\right)_i,$$for \(i = 1, \dots, n\). Each point carries two weights, one for reconstructing the mean (\(W_i^{m}\)) and one for the covariance (\(W_i^{c}\)):
$$W_0^{m} = \frac{\lambda}{n+\lambda}, \quad W_0^{c} = \frac{\lambda}{n+\lambda} + (1 - \alpha^2 + \beta), \quad W_i^{m} = W_i^{c} = \frac{1}{2(n+\lambda)}.$$The composite scaling parameter is \(\lambda = \alpha^2(n+\kappa) - n\). The three knobs each have a job. \(\alpha\) (small, e.g. \(10^{-3}\)) controls how tightly the sigma points hug the mean, keeping them inside the region where the function is well-behaved and suppressing higher-order sampling error. \(\beta\) folds in prior knowledge of the distribution's shape; \(\beta = 2\) is optimal for a Gaussian and shows up only in the covariance weight of the center point. \(\kappa\) is a secondary spread control, usually \(0\) or \(3-n\). These are the Merwe scaled-sigma-point defaults, and for most sensor work you set \(\alpha, \beta, \kappa\) once and forget them.
Key Insight
The sigma points are not random samples and there are not many of them. They are a deterministic, minimal quadrature rule: \(2n+1\) points chosen precisely so their weighted mean and weighted covariance reproduce \(\hat{x}\) and \(P\) exactly. Because the propagated mean is computed from where the real function sent those points, the transform sees the function's curvature that a single tangent plane at the mean is blind to. This is why the UKF corrects the bias the EKF misses: the EKF evaluates \(g\) at one point, the UKF evaluates it at \(2n+1\) and lets their spread report the nonlinearity.
The unscented transform in code
The whole method is short enough to write once and trust. The function below generates the sigma points and weights, and a second function pushes them through any map and refits a Gaussian. We test it on the classic pain point for linearization: converting a noisy range-and-bearing reading (polar) into a Cartesian position, a map curved enough that evaluating it only at the mean is measurably biased.
import numpy as np
rng = np.random.default_rng(0)
def sigma_points(mean, cov, alpha=1e-3, beta=2.0, kappa=0.0):
n = mean.size
lam = alpha**2 * (n + kappa) - n
S = np.linalg.cholesky((n + lam) * cov) # scaled matrix square root
pts = np.vstack([mean, mean + S.T, mean - S.T]) # 2n+1 sigma points
wm = np.full(2 * n + 1, 1.0 / (2 * (n + lam)))
wc = wm.copy()
wm[0] = lam / (n + lam)
wc[0] = wm[0] + (1 - alpha**2 + beta)
return pts, wm, wc
def unscented_transform(pts, wm, wc, f):
y = np.array([f(p) for p in pts]) # push each point through real f
mean = wm @ y
d = y - mean
cov = (wc[:, None] * d).T @ d
return mean, cov
def polar_to_xy(s): # nonlinear sensor->world map
r, th = s
return np.array([r * np.cos(th), r * np.sin(th)])
mean = np.array([30.0, 0.9]) # range 30 m, bearing 0.9 rad
cov = np.diag([1.0**2, 0.35**2]) # range / bearing noise
pts, wm, wc = sigma_points(mean, cov)
ut_mean, _ = unscented_transform(pts, wm, wc, polar_to_xy)
samples = rng.multivariate_normal(mean, cov, 400_000)
mc_mean = np.array([polar_to_xy(s) for s in samples]).mean(0) # truth
lin_mean = polar_to_xy(mean) # EKF-style: f(mean) only
print("Monte-Carlo mean :", mc_mean.round(3))
print("unscented mean :", ut_mean.round(3))
print("linearized mean :", lin_mean.round(3))
sigma_points builds the deterministic \(2n+1\) point cloud and its two weight vectors; unscented_transform propagates them through the true polar_to_xy map and refits a Gaussian. The printout shows the unscented mean landing on the Monte-Carlo truth while the linearized guess f(mean), the value an EKF would use, sits noticeably off because a wide bearing spread biases the true mean inward.Run it and the unscented mean tracks the 400,000-sample Monte-Carlo truth to a few centimeters, while the linearized f(mean) is off by a visible margin. That gap is exactly the second-order term the EKF drops and the UKF keeps, purchased with nine function evaluations here and zero calculus.
Wiring the transform into the filter
The full UKF is just the recursion of Chapter 9 with every "propagate the Gaussian" step replaced by an unscented transform. Predict: draw sigma points from \((\hat{x}_{t-1}, P_{t-1})\), push them through the motion model \(f\), and refit to get the predicted mean and covariance, then add the process noise \(Q\). Correct: draw fresh sigma points from the predicted Gaussian, push them through the measurement model \(h\) to get predicted measurements, and compute three things from the propagated sets: the innovation covariance \(S = P_{zz} + R\), the state-measurement cross-covariance \(P_{xz}\), and from them the Kalman gain \(K = P_{xz} S^{-1}\). The mean and covariance update, \(\hat{x} = \hat{x}^- + K(z - \hat{z})\) and \(P = P^- - K S K^{\top}\), are algebraically identical to the linear filter. The only structural change from the EKF is that Jacobians \(F\) and \(H\) are gone; the cross-covariance \(P_{xz}\), which the EKF forms as \(P^- H^{\top}\), now falls straight out of the sigma-point spread.
Practical Example: automotive radar tracking a merging vehicle
An automotive radar reports each detection in its own polar frame: range, range-rate, and azimuth. The tracker, though, reasons in the ego vehicle's Cartesian frame, where a constant-velocity motion model is linear and clean. The coupling between them, the measurement model that maps Cartesian state to range-bearing-Doppler, is sharply nonlinear, and it is worst exactly where it matters: a vehicle cutting in from an adjacent lane at close range and wide azimuth, where bearing uncertainty of a few degrees smears across a meter of lateral position. An EKF linearizes that map at the predicted mean and, during the fast lateral transient, produces an innovation covariance that is subtly the wrong shape, so the gate either rejects good detections or admits clutter. Swapping in a UKF changes two functions and deletes the hand-derived Jacobian; the sigma points fan out across the true azimuth arc, the innovation covariance bulges in the correct lateral direction, and the track stays locked through the merge. This is the standard reason radar and lidar stacks (Chapter 44) reach for the UKF before anything heavier.
Right Tool: FilterPy's UnscentedKalmanFilter
Everything above, sigma-point generation, the predict transform, the innovation and cross-covariance in the correct step, the gain and update, is packaged in filterpy.kalman.UnscentedKalmanFilter paired with MerweScaledSigmaPoints. You supply only the two model functions \(f\) and \(h\), the noise covariances \(Q\) and \(R\), and the sigma-point parameters; the library runs the recursion. That replaces roughly sixty to eighty lines of hand-written sigma-point bookkeeping, Cholesky factoring, and weighted refitting with about eight lines of setup and a one-line ukf.predict(); ukf.update(z) loop, and it handles the numerical guards (positive-definite covariance repair) you would otherwise discover the hard way in production. Build the transform once by hand to understand it, as above; ship the library version.
When to choose it, and what it still cannot do
Reach for the UKF when the models are meaningfully nonlinear over the width of your uncertainty, when analytic Jacobians are painful, error-prone, or expensive to derive (attitude and orientation estimation in Chapter 24 is a canonical case, where quaternion kinematics make Jacobians treacherous), or when you simply want a derivative-free drop-in that is more robust than the EKF at essentially the same cost. The compute is \(O(n^3)\) per step from the Cholesky factorization, the same order as the EKF, with a small constant factor from the \(2n+1\) function evaluations; for the low-dimensional states typical of sensor tracking this difference is negligible. The hard limit is inherited, not introduced: the UKF still represents the belief as one Gaussian. If the true posterior is multi-modal, if you are tracking through a data-association ambiguity or a bimodal likelihood, no choice of sigma points saves you, because the output is refit to a single mean and covariance by construction. That wall is where Section 10.3 begins.
Research Frontier
The sigma-point idea generalizes well beyond the original 2004 unscented transform. The broader family of sigma-point Kalman filters swaps in different deterministic quadrature rules: the cubature Kalman filter (CKF) uses a spherical-radial rule with \(2n\) equally weighted points and is numerically more stable in high dimension, and the Gauss-Hermite Kalman filter trades cost for higher-order accuracy. On the software side, filterpy remains the standard teaching and prototyping implementation, while performance-critical robotics stacks increasingly express these filters inside factor-graph optimizers such as GTSAM (Chapter 11), where the sigma-point linearization becomes one factor among many rather than a standalone recursion. The current practical guidance is unchanged: for moderate-dimension, moderately nonlinear, unimodal problems, a square-root UKF or CKF is the robust default before you pay for sampling.
Exercise
Extend the code above into a working scalar-to-2D tracker. (1) Wrap the transform in a predict-correct loop for a 2-D constant-velocity target observed by the polar_to_xy-style range-bearing sensor, and confirm the track holds. (2) Reduce \(\alpha\) from \(10^{-3}\) toward \(1\) and report how the sigma-point spread and the estimated covariance change; explain why a larger spread can hurt when the function is only locally well-behaved. (3) Replace the unscented mean with the linearized mean \(f(\hat{x})\) inside the correct step and measure the tracking error increase, isolating exactly the bias the UKF removes. (4) Push the bearing noise up until the true posterior becomes visibly banana-shaped, and describe, from the printed covariance, how a single-Gaussian filter misrepresents it.
Self-Check
- The EKF and the UKF both output a Gaussian from a Gaussian. What does each one approximate to get there, the function or the distribution, and to what order in the Taylor expansion is each accurate?
- Why does the center sigma point have a different covariance weight \(W_0^c\) from its mean weight \(W_0^m\), and which parameter injects that difference?
- The UKF removes the Jacobians \(F\) and \(H\) entirely. Where does the information those Jacobians carried, specifically the state-measurement cross-covariance \(P_{xz}\), now come from instead?
What's Next
In Section 10.3, we cross the wall the UKF cannot climb. When the belief is multi-modal or the noise is anything but Gaussian, we stop refitting a single Gaussian and instead represent the posterior with a cloud of many weighted samples, the particles, propagated by the same predict-correct logic but weighted by importance sampling. The sigma points you just met were a deterministic handful chosen to match two moments; the particle filter trades that elegance for a stochastic swarm that can chase any shape the posterior takes.