A range sensor reports the square root of x² plus y². A Kalman filter only understands straight lines. The Jacobian is the bridge: the best flat approximation of a curved map, rebuilt fresh at every single point.
Say you run a small tracking station. A dish on the roof reports one number about a drone flying nearby: the straight line distance from the dish to the drone. No bearing, no altitude split out, just a single range reading. If the drone sits at position (x, y) in a coordinate frame centered on your dish, that reading is √(x² + y²), the length of the position vector.
You'd like to fuse this reading with a motion model using a Kalman filter, the tool that optimally blends a prediction with a measurement. There's a catch. The Kalman filter's update rule assumes the measurement is a fixed matrix times the state: z = H·x. Plug in √(x² + y²) and ask what matrix H makes that true for every (x, y), and you'll search forever. No such matrix exists. A square root of a sum of squares is not a linear function of x and y, and no amount of clever matrix picking turns it into one.
It gets worse before it gets better. Even if you squint and accept an approximate matrix, the deeper problem is statistical. Your belief about the drone's position isn't a single point, it's a spread, a small cloud of plausible (x, y) values shaped like a Gaussian blob. Push that whole blob through a curved function like a square root, and the output is no longer shaped like a Gaussian at all. It skews, it bunches up, its mean shifts away from the square root of the input's mean. The clean, closed form update equations that make the Kalman filter fast and exact were built assuming Gaussians stay Gaussian. A curved function breaks that assumption outright.
The teal curve is the true range reading as the drone's x position changes (y held fixed at 3). The warm line is a straight tangent line drawn at the operating point. Drag the slider to move the drone and watch the gap between the curve and the line.
Notice something important in that widget. Close to the point where the tangent line touches the curve, the line and the curve are nearly on top of each other. You'd barely notice the difference. Walk far enough away, and the gap opens up fast. This is the entire idea this lesson is built on: any smooth curve, zoomed in far enough, looks straight. The line that best matches it at one particular point is called the tangent line in one dimension, and its generalization to many inputs and many outputs at once is what we're going to build and name the Jacobian.
Here's the actual fix used by every practical filter that has to deal with a curved sensor or a curved motion model, from a phone's GPS chip to a Mars rover's navigation stack. At each timestep, freeze your current best guess of the state. Build the straight line, or in higher dimensions the flat plane, that best matches the true curved function right at that guess. Run the ordinary, linear Kalman filter math using that flat approximation. Then throw the approximation away and rebuild a fresh one at the next timestep, around the new guess. You've met this idea already if you've studied the Extended Kalman Filter, this lesson builds the tool it leans on from the ground up.
Let's build the one dimensional version of the fix properly, with real numbers, before we generalize to many inputs. Take a smooth function f(x). Pick a point a where you know the value f(a) and the slope f′(a). The claim is that near a, the straight line through (a, f(a)) with slope f′(a) is the best possible linear stand in for f.
Read this formula as a sentence. Start at the known height f(a). Walk a distance of (x − a) away from a. For each unit you walk, climb by f′(a), the slope at the starting point. That's the whole idea: start where you know the answer, then extrapolate using the one thing you know about how it's changing.
Take f(x) = √x, and linearize it at a = 4. First, f(4) = √4 = 2. Next, the derivative of √x is 1 / (2√x), so f′(4) = 1 / (2·2) = 0.25. The linear approximation is:
Now push this line past its comfort zone and watch it drift from the truth. The table below computes the true value, the linear guess, and the error at four increasing distances from a = 4.
| x (distance δ from 4) | True √x | Linear guess L(x) | Error |
|---|---|---|---|
| 4.5 (δ = 0.5) | 2.121320 | 2 + 0.25·0.5 = 2.125000 | 0.003680 |
| 5.0 (δ = 1.0) | 2.236068 | 2 + 0.25·1.0 = 2.250000 | 0.013932 |
| 6.0 (δ = 2.0) | 2.449490 | 2 + 0.25·2.0 = 2.500000 | 0.050510 |
| 8.0 (δ = 4.0) | 2.828427 | 2 + 0.25·4.0 = 3.000000 | 0.171573 |
Look at the error column. Doubling δ from 0.5 to 1.0 roughly quadruples the error, from about 0.0037 to about 0.0139. Doubling again to δ = 2.0 roughly quadruples it again, to about 0.0505. This isn't a coincidence, and it isn't specific to square roots. It's the signature of a squared term.
python import math def linearize(f, fprime, a, x): # L(x) = f(a) + f'(a) * (x - a) return f(a) + fprime(a) * (x - a) f = math.sqrt fprime = lambda x: 1 / (2 * math.sqrt(x)) for x in [4.5, 5.0, 6.0, 8.0]: true_val = f(x) approx = linearize(f, fprime, 4.0, x) print(f"x={x} true={true_val:.6f} approx={approx:.6f} err={abs(true_val-approx):.6f}")
The teal curve is f(x) = √x. The warm line is the tangent at a = 4. Drag δ to move the true point away from a, and watch the true value (teal dot), the linear guess (warm dot), and the numeric error track exactly the table above.
The tangent line trick from Chapter 1 handles a function with one input and one output. Our range sensor has two inputs, x and y. We need to generalize "the slope at a point" to a setting where there's more than one direction to walk in.
The tool is the partial derivative, and the rule for building one is refreshingly simple: freeze every variable except the one you're differentiating with respect to, and treat the frozen ones as plain constants. For f(x, y), the partial with respect to x, written ∂f/∂x or fx, holds y fixed and asks how f changes as x alone wiggles. The partial with respect to y does the mirror image.
Stack the two partials into a vector and you get the gradient, written ∇f:
The gradient is a compass. It points in the direction that increases f the fastest, and its length tells you how steep that fastest direction is. Just as one derivative gave us a tangent line in Chapter 1, the gradient gives us a tangent plane in two dimensions: the flattest possible sheet that still touches the surface at one point and leans in exactly the direction the surface leans.
Take f(x, y) = x² + xy, and linearize it at the point (1, 2).
Step 1, the partials. Freeze y, differentiate in x:
Freeze x, differentiate in y:
Step 2, evaluate at (1, 2).
So ∇f(1, 2) = (4, 1), and the tangent plane is:
Step 3, test it nearby. At (1.1, 2.1):
An error of just 0.02 for a point that moved a full 0.1 units in each direction. That's the tangent plane earning its keep: close to (1, 2), it's an excellent stand in for the true curved surface.
Contour rings of g(x, y) = x² + 0.5xy + y². Set the point with the sliders. The warm arrow is the gradient, always perpendicular to the contour ring through that point. The short teal segment is the local flat direction, the tangent to that same ring.
Drag the sliders around the bowl. Notice that wherever you land, the warm gradient arrow always points straight out, perpendicular to the ring you're standing on, toward higher ground. The rings are lines of equal height, so walking along a ring changes nothing, all the change is in the perpendicular direction. That's not a coincidence of this particular bowl, it's true of every smooth surface, and it's why gradient descent, the workhorse of machine learning training, always steps perpendicular to the current loss contour.
The gradient handled a function with many inputs and one output. Our range and bearing sensor has two inputs and, once we add bearing to the mix, two outputs at once. We need one more generalization: a function f: ℝn → ℝm, n numbers in, m numbers out. The tool for linearizing it is the Jacobian matrix, and it's built from a very natural idea: stack a gradient for every output.
Row i of J is exactly the gradient of output fi, the i-th output's own private compass. So J is an m by n matrix: m rows, one per output, n columns, one per input. Entry Jij = ∂fi/∂xj answers a precise question: if I wiggle input j by a tiny amount, how much does output i move? That's it, that's the entire object. Everything else in this lesson is putting that one idea to work.
Here is the single most useful worked example in robotics: converting a polar coordinate, a distance r and an angle θ, into a Cartesian point (x, y). The map is:
Two outputs, x and y. Two inputs, r and θ. The Jacobian is 2 by 2. Let's fill in all four entries with the freeze the other variable rule from Chapter 2, one at a time, showing every step.
| Entry | Freeze, then differentiate | Result |
|---|---|---|
| ∂x/∂r | freeze θ: x = (cosθ)·r, a constant times r | cosθ |
| ∂x/∂θ | freeze r: x = r·cosθ, derivative of cos is −sin | −r·sinθ |
| ∂y/∂r | freeze θ: y = (sinθ)·r, a constant times r | sinθ |
| ∂y/∂θ | freeze r: y = r·sinθ, derivative of sin is cos | r·cosθ |
Now plug in real numbers. Take r = 4, θ = 60° (that's π/3 radians), so cosθ = 0.5 and sinθ ≈ 0.866025.
Read the first column, [0.5, 0.866], as an instruction: increasing r by one unit moves the point 0.5 units in x and 0.866 units in y, exactly the unit direction the point is already sitting in. Read the second column, [−3.4641, 2.0], as a different instruction: increasing θ by one radian sweeps the point sideways, perpendicular to the first column, and four times farther, because sweeping a lever of length r = 4 through an angle covers 4 times as much arc length as sweeping a lever of length 1.
A small circle of nearby (r, θ) values, on the left, maps through the polar to Cartesian Jacobian into an ellipse of nearby (x, y) values, on the right. Set r and θ and watch the ellipse change shape and orientation.
Slide r up. The ellipse stretches wider in the direction perpendicular to the line to the origin, exactly as the "sweeping a longer lever covers more distance" argument predicts. Slide θ around a full circle instead, and the ellipse just spins in place, since neither cosθ nor sinθ changes the size of the stretch, only its direction. This picture, a small circle of uncertainty becoming an ellipse under a Jacobian, is exactly the picture we'll use in Chapter 8 to compare true and linearized sensor noise.
Real systems are rarely one function. A lidar return goes through a polar to Cartesian conversion, then a rotation into the robot's body frame, then a translation into a world map frame, three functions composed end to end. A neural network is dozens of layers composed the same way. How does linearization survive being composed?
Beautifully, it turns out. If y = g(f(x)), meaning you feed x into f, then feed the result into g, the single variable chain rule you already know says the derivative of the composition is the product of the two derivatives: dy/dx = g′(f(x))·f′(x). The matrix version says exactly the same thing, with matrix multiplication standing in for ordinary multiplication:
Read this carefully, because the evaluation points matter. Jf is evaluated at x, the original input. Jg is evaluated at f(x), the output of the first stage, since that's the input g actually sees. Multiply those two matrices together, in that order, and you get the Jacobian of the whole pipeline, no matter how curved each individual stage is.
Reuse Chapter 3's polar to Cartesian map as stage one: u = f(r, θ) = (r·cosθ, r·sinθ), evaluated at r = 4, θ = 60°, giving u = (2, 3.4641) and Jf = [[0.5, −3.4641], [0.8660, 2.0]] exactly as before.
Now bolt on a second, deliberately nonlinear stage: g(u1, u2) = (u1² + u2, u1·u2). Its Jacobian, by the same freeze and differentiate rule as always:
Evaluated at u = (2, 3.4641): Jg = [[4, 1], [3.4641, 2]]. Now multiply, row by column, tracking every number:
| Entry of Jg·Jf | Row of Jg · Column of Jf | Result |
|---|---|---|
| (1,1) | [4, 1]·[0.5, 0.8660] | 4·0.5 + 1·0.8660 = 2.866 |
| (1,2) | [4, 1]·[−3.4641, 2.0] | 4·(−3.4641) + 1·2.0 = −11.856 |
| (2,1) | [3.4641, 2]·[0.5, 0.8660] | 3.4641·0.5 + 2·0.8660 = 3.464 |
| (2,2) | [3.4641, 2]·[−3.4641, 2.0] | 3.4641·(−3.4641) + 2·2.0 = −8.000 |
Four numbers, and they cost nothing more than two smaller Jacobians and one matrix multiply, no matter how the two stages were defined. This is exactly the computation that makes deep learning practical: backpropagation is nothing more than this chain rule, applied layer by layer, from the output back to the input, multiplying local Jacobians as it goes. See the Backpropagation lesson for that story told end to end.
.backward() in a training loop is this exact chain rule, run
mechanically. Each layer records its own local Jacobian during the forward
pass, and autograd multiplies them back from the loss toward the first
layer's weights. No researcher derives these products by hand; the chain
rule in this chapter is precisely the algorithm autograd executes billions
of times over the course of training a large model.
python import numpy as np def Jf(r, theta): return np.array([[np.cos(theta), -r*np.sin(theta)], [np.sin(theta), r*np.cos(theta)]]) def Jg(u1, u2): return np.array([[2*u1, 1], [u2, u1]]) r, theta = 4.0, np.pi/3 u1, u2 = r*np.cos(theta), r*np.sin(theta) # = f(r, theta) J_total = Jg(u1, u2) @ Jf(r, theta) # chain rule: Jg(f(x)) . Jf(x) print(np.round(J_total, 3))
The small circle on the left is the input neighborhood. It passes through Jf to become the middle ellipse, then through Jg to become the final shape on the right. Adjust k, a scale knob buried inside g, and watch only the last step change while Jf's ellipse stays put.
Every Jacobian so far came from algebra: freeze a variable, differentiate, write down the formula. That's the analytic route, and it's exact. But analytic Jacobians are also where real EKF code most often breaks: a dropped minus sign, a forgotten chain rule term buried three functions deep, a copy paste mistake between the x row and the y row. You need a second, independent way to compute the same numbers, one that never touches your formula at all.
That's a finite difference. Instead of differentiating symbolically, nudge one input by a tiny step h, see how much the output moved, and divide.
The central version costs one extra function call per input but is dramatically more accurate for the same h, because it cancels the leading error term that the forward version leaves behind. This is worth understanding precisely, since the size of h is not a free choice, it's a genuine engineering trade off.
Make h too large, and the straight line secant you're drawing cuts across real curvature in f, so you're not really measuring the slope at x anymore, that's truncation error, and it shrinks as h shrinks. Make h too small, and you run into a completely different problem: computers store numbers with finite precision, roughly 16 significant digits for a standard float. Subtracting two nearly identical numbers, f(x+h) and f(x), throws away almost all of those digits, and dividing by a tiny h then blows up whatever tiny error was left. That's round off error, and it grows as h shrinks. Somewhere in between sits a sweet spot.
Estimating the derivative of sin(x) at x = 1 (true answer: cos(1) ≈ 0.540302) using real floating point arithmetic. Both axes are logarithmic. Slide the step size h and watch where each method sits on its own error curve.
Drag h from the right side of the plot toward the left. At first, both curves fall as h shrinks, truncation error dominates and smaller steps help. Keep going and the central difference curve, which starts with much smaller error thanks to canceling that leading term, bottoms out and starts climbing again as round off takes over. The forward difference curve does the same thing, but its floor sits many orders of magnitude higher. Somewhere around h = 10−5 to 10−4 is usually a safe, general purpose choice for central differences in double precision arithmetic.
Let's put this into practice on the polar to Cartesian Jacobian from Chapter 3, computed three independent ways: by hand coded analytic formulas, by a from scratch central finite difference in NumPy, and by automatic differentiation in PyTorch, which computes exact derivatives without you writing any formula at all. All three should land on the same numbers we computed by hand: [[0.5, −3.4641], [0.8660, 2.0]].
python · plain, analytic formulas import math def polar_to_cart(r, theta): return (r * math.cos(theta), r * math.sin(theta)) def analytic_jacobian(r, theta): c, s = math.cos(theta), math.sin(theta) return [[c, -r * s], [s, r * c]] r, theta = 4.0, math.pi / 3 print("analytic:", analytic_jacobian(r, theta)) # [[0.5, -3.4641], [0.866, 2.0]]
numpy · central finite differences import numpy as np def polar_to_cart(rt): r, theta = rt return np.array([r * np.cos(theta), r * np.sin(theta)]) def jacobian_fd(f, x, h=1e-5): x = np.asarray(x, dtype=float) n = len(x) J = np.zeros((len(f(x)), n)) for j in range(n): step = np.zeros(n); step[j] = h J[:, j] = (f(x + step) - f(x - step)) / (2 * h) # central diff return J x0 = np.array([4.0, np.pi / 3]) print("finite-diff:", np.round(jacobian_fd(polar_to_cart, x0), 4))
pytorch · automatic differentiation import torch def polar_to_cart(rt): r, theta = rt[0], rt[1] return torch.stack([r * torch.cos(theta), r * torch.sin(theta)]) x0 = torch.tensor([4.0, math.pi / 3]) J = torch.autograd.functional.jacobian(polar_to_cart, x0) print("autograd:", J)
Three completely different code paths, none of which knows about the other two, and all three land on [[0.5, −3.4641], [0.8660, 2.0]] to within rounding. That agreement is exactly the check a real filter's test suite runs before the code ever ships.
Time to solve the actual problem from Chapter 0. Give the tracking station a real sensor: it reports range and bearing to the drone, not just range. If the drone sits at (x, y) with the dish at the origin, the measurement function is:
The first output is range, the second is bearing, the angle from the dish to the drone. This is the Cartesian to polar map, the inverse direction from Chapter 3's worked example. In an Extended Kalman Filter this h is what plugs into the update step, and the matrix we need for the covariance math is its Jacobian, usually written H instead of J when it's specifically the measurement Jacobian:
Freeze y, differentiate range with respect to x: range = (x²+y²)1/2, so ∂range/∂x = x·(x²+y²)−1/2 = x / range. By the same logic, ∂range/∂y = y / range. The bearing partials take one more step of chain rule through arctangent, but the result is clean:
Plug in a concrete drone position: (x, y) = (3, 4), so r = √(9+16) = √25 = 5.
Look closely at the top row, (0.6, 0.8). Its length is √(0.6² + 0.8²) = √(0.36+0.64) = √1 = 1, always, for any (x, y), because that row is just (cosθ, sinθ) for the bearing angle θ, a unit vector by definition. Multiply H by its own transpose, H·HT, and the cross terms between the two rows cancel:
Check it with the numbers above: row one dotted with itself is 0.6² + 0.8² = 1. Row one dotted with row two is 0.6·(−0.16) + 0.8·0.12 = −0.096 + 0.096 = 0, they're exactly perpendicular. Row two dotted with itself is (−0.16)² + 0.12² = 0.0256 + 0.0144 = 0.04 = 1/25 = 1/r². This diagonal, orthogonal structure means range sensitivity and bearing sensitivity point in completely independent directions, and it's the exact formula we'll reuse in Chapter 8 to build the linearized uncertainty ellipse without any extra machinery.
Top: the drone's position relative to the dish at the origin, with the range and bearing lines drawn. Bottom: bearing sensitivity, 1/r, plotted against r, with your current position marked. Drag the drone in close and watch the marker race up the curve.
Sensors aren't the only nonlinear piece. A robot's own motion model usually is too. Consider a simple ground robot with state (x, y, θ), position plus heading, driven forward at speed v with turn rate ω for a short time step dt:
Position updates depend on the heading through sine and cosine, curved functions of θ, so this motion model needs the same linearization treatment as any sensor. The Jacobian here is usually called F, the process Jacobian, and because there are three state variables, F is 3 by 3.
Two of the three columns are almost free. Differentiating the x update with respect to x itself gives 1, and with respect to y gives 0, since x doesn't appear in the y update at all, and vice versa. The only genuinely nonlinear column is the θ column, since that's the only place sine and cosine show up:
Plug in v = 2 m/s, θ = 30° (sinθ = 0.5, cosθ ≈ 0.8660), dt = 0.1 s: −v·sinθ·dt = −2·0.5·0.1 = −0.1, and v·cosθ·dt = 2·0.8660·0.1 ≈ 0.1732.
The state estimate itself gets pushed through the true, curved f. But the uncertainty around that estimate, the covariance matrix P, gets pushed through the linearized F instead, using the sandwich formula:
Let's do this by hand too, with a starting covariance P = diag(0.04, 0.04, 0.01), meaning position uncertainty of about 0.2 units in x and y and heading uncertainty of about 0.1 radians, all currently uncorrelated with each other. First, F·P, easy since P is diagonal, it just scales F's columns:
| Row of F·P | Computation | Result |
|---|---|---|
| row 1 | [1·0.04, 0·0.04, −0.1·0.01] | [0.0400, 0, −0.0010] |
| row 2 | [0·0.04, 1·0.04, 0.1732·0.01] | [0, 0.0400, 0.0017] |
| row 3 | [0·0.04, 0·0.04, 1·0.01] | [0, 0, 0.0100] |
Now multiply that result by FT, whose columns are F's rows:
| Entry of Pnew | Row of FP · column of FT | Result |
|---|---|---|
| (1,1) | [0.04, 0, −0.001]·[1, 0, −0.1] | 0.04 + 0.0001 = 0.0401 |
| (1,2) | [0.04, 0, −0.001]·[0, 1, 0.1732] | −0.000173 |
| (1,3) | [0.04, 0, −0.001]·[0, 0, 1] | −0.001 |
| (2,2) | [0, 0.04, 0.0017]·[0, 1, 0.1732] | 0.04 + 0.0003 = 0.0403 |
| (2,3) | [0, 0.04, 0.0017]·[0, 0, 1] | 0.001732 |
| (3,3) | [0, 0, 0.01]·[0, 0, 1] | 0.01 |
The gray circle is a starting position uncertainty, isotropic and uncorrelated. The colored ellipse is that same uncertainty after one FPFT step at the given heading, speed, and time step. Watch it stretch and tilt as you change θ.
Everything in this lesson lands here. On the left, a small tracking dish sits at the origin and a drone sits somewhere nearby, the operating point where we'll build the tangent plane. Drag the drone anywhere you like. Around it hangs a cloud of sample points, real position uncertainty, its spread set by the sigma slider.
On the right, each of those sample points is pushed through the true, curved range and bearing function h(x, y) from Chapter 6, plotted as small dots in range bearing space, the true distribution. On top of that cloud sits an ellipse, drawn from the closed form linearized covariance H·Σ·HT we derived in that same chapter, the linearized approximation.
Left panel: position space, dish at the origin, drone draggable. Right panel: range bearing measurement space, true samples (dots) versus the linearized 1σ ellipse (outline).
Start with the drone far from the dish, several units out. The cloud and the ellipse sit almost exactly on top of each other; the linear approximation is excellent here, exactly what Chapter 1's error grows with δ² result predicts when your uncertainty σ is small relative to how fast the curvature changes. Now click "Very close" and watch the mismatch appear. The true cloud bends into a curved, banana shaped smear as bearing wraps around near the dish, while the linearized ellipse, forced by construction to be a perfect axis aligned oval, cannot bend at all. It captures the right overall size but completely misses the true shape.
Try "Straight ahead," drone directly in front of the dish along the y axis. Even at a moderate range, notice the true cloud is slightly skewed relative to the symmetric ellipse, a small foretaste of the same nonlinearity that becomes dramatic up close. There is no quiz for this chapter. If you can predict, before dragging, whether the ellipse will hug the cloud or badly miss it, purely from how close the drone is to the dish, you understand linearization.
Four objects showed up across this lesson, and it's easy to blur them together. They're related, but each answers a different question and each has a different shape. Here's the map.
| Tool | Shape | Core question | Reach for it when |
|---|---|---|---|
| Gradient | vector, length n | Which single direction increases this one number fastest? | Optimizing a scalar loss or cost, one output, many inputs |
| Jacobian | matrix, m by n | How does each of several outputs change with each input? | Linearizing a vector valued map: a sensor model, robot kinematics, a layer in a network |
| Hessian | matrix, n by n | How is the slope itself curving, and in which directions? | Classifying a critical point, running Newton's method, second order optimizers |
| Finite difference | numeric estimate of any of the above | What's the derivative when I don't trust, or don't have, the algebra? | Checking an analytic Jacobian or gradient, differentiating a black box function, hunting an EKF bug |
| Object | Formula |
|---|---|
| Tangent line (1D) | L(x) = f(a) + f′(a)·(x−a) |
| Tangent plane (n inputs, 1 output) | L(x) = f(a) + ∇f(a)·(x−a) |
| Jacobian (n inputs, m outputs) | Jij = ∂fi/∂xj |
| Chain rule | Jg∘f(x) = Jg(f(x))·Jf(x) |
| Central finite difference | ∂f/∂x ≈ (f(x+h) − f(x−h)) / (2h) |
| Measurement Jacobian | H = ∂h/∂x, used in the Kalman gain and the covariance update |
| Process Jacobian | F = ∂f/∂x, used in Pnew = FPFT + Q |
"What I cannot create, I do not understand." Richard Feynman.
You hand computed a Jacobian, checked it two independent ways, linearized a
real sensor, watched it fail exactly where the geometry predicted, and built
the lab that compares truth to approximation. You can now create it.