Mathematical Foundations

Jacobians & Linearization

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.

Prerequisites: single variable derivatives + matrix vector multiplication. That's it. Everything else we build.
10
Chapters
9+
Simulations
0
Assumed Knowledge

Chapter 0: The Range Sensor Problem

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.

The trap: it's tempting to think the fix is "use a bigger matrix" or "add a correction term." It isn't. The function √(x² + y²) is curved everywhere, and a matrix multiply is flat everywhere. You cannot bend a flat sheet of paper into a dome by picking different numbers on it, no matter how many numbers you're allowed.

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.

A curved sensor, zoomed in

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.

drone x 0.0

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.

Where this is going: by the end of this lesson you will hand compute the Jacobian of the classic polar to Cartesian map, check an analytic Jacobian two different ways, linearize a real bearing and range sensor, watch it fail exactly where the geometry says it should, and run a full lab that compares a true, sampled uncertainty cloud against its linearized ellipse.
Why can't you find a fixed matrix H so that range = H·[x, y] for every (x, y)?

Chapter 1: The Best Linear Approximation

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.

L(x) = f(a) + f′(a)·(x − a)

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.

A hand computed worked example

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:

L(x) = 2 + 0.25·(x − 4)

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 √xLinear guess L(x)Error
4.5 (δ = 0.5)2.1213202 + 0.25·0.5 = 2.1250000.003680
5.0 (δ = 1.0)2.2360682 + 0.25·1.0 = 2.2500000.013932
6.0 (δ = 2.0)2.4494902 + 0.25·2.0 = 2.5000000.050510
8.0 (δ = 4.0)2.8284272 + 0.25·4.0 = 3.0000000.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.

Where the δ² comes from. Taylor's theorem says the true gap between f(x) and its tangent line is approximately ½·f″(a)·δ², the next term in the expansion after the linear one. For f(x) = √x, f″(x) = −¼x−3/2, so f″(4) = −1/32. The predicted error size is ½·(1/32)·δ² = δ²/64. At δ = 1 that's 1/64 ≈ 0.0156, close to the measured 0.0139. At δ = 2 it predicts 4/64 = 0.0625, in the same ballpark as the measured 0.0505. The match isn't exact for large δ because even higher order terms start contributing, but the δ² scaling is unmistakable and it's the reason linearization only works locally.
When to reach for it: a thermistor's firmware. A cheap temperature sensor's resistance to temperature curve is sharply nonlinear across its full range. Embedded firmware avoids an expensive log calculation on every reading by linearizing that curve once, around the expected operating temperature, then evaluating a one line linear formula in the sensor's inner loop thousands of times a second. It works because the operating range is narrow, exactly the small δ condition from this chapter; the same firmware reads badly wrong on a day far outside its calibrated range.

Code: the same computation

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}")
Watch the gap grow away from the tangent point

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.

δ (distance from a=4) 1.0
true = 2.236068, approx = 2.250000, error = 0.013932
The trap: beginners assume linearizing once is good enough for a whole flight, or a whole tracking session. It isn't. The moment the true state drifts more than a small δ away from wherever you built the line, the error grows with the square of that drift, not the drift itself. A filter that linearizes once and never updates its operating point will look fine for a few steps and then diverge fast. This is exactly why the EKF relinearizes at every single timestep instead of trusting one line forever.
You double the distance δ from the point where you built a tangent line. What happens to the approximation error, roughly?

Chapter 2: Partials and the Gradient

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:

∇f(x, y) = ( fx(x, y),   fy(x, y) )

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.

L(x, y) = f(a, b) + fx(a, b)·(x − a) + fy(a, b)·(y − b)

A hand computed worked example

Take f(x, y) = x² + xy, and linearize it at the point (1, 2).

Step 1, the partials. Freeze y, differentiate in x:

fx = ∂/∂x (x² + xy) = 2x + y

Freeze x, differentiate in y:

fy = ∂/∂y (x² + xy) = x

Step 2, evaluate at (1, 2).

f(1, 2) = 1² + 1·2 = 1 + 2 = 3
fx(1, 2) = 2·1 + 2 = 4      fy(1, 2) = 1

So ∇f(1, 2) = (4, 1), and the tangent plane is:

L(x, y) = 3 + 4·(x − 1) + 1·(y − 2)

Step 3, test it nearby. At (1.1, 2.1):

true: f(1.1, 2.1) = 1.1² + 1.1·2.1 = 1.21 + 2.31 = 3.52
plane: L(1.1, 2.1) = 3 + 4·0.1 + 1·0.1 = 3 + 0.4 + 0.1 = 3.50

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.

Gradient compass on a bowl shaped 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.

x1.2
y-0.8

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.

When to reach for it: a self driving car's local planner. A path planning cost surface scores every nearby (x, y) offset by a blend of "distance to the lane center" and "distance to obstacles." The car doesn't scan every option, it computes the gradient of that cost surface at its current offset and nudges the steering command in the downhill direction. The gradient is the only tool that answers "which single direction improves things fastest right now," using two numbers instead of scanning the whole surface.
The trap: it's easy to think the gradient describes the whole surface. It doesn't. It's a purely local snapshot, exactly one tangent plane at exactly one point. Two points that are close together can have wildly different gradients if the surface curves sharply between them; the gradient alone can't tell you that curvature exists; that's a job for a different object, the Hessian, covered in its own lesson.
For f(x, y) = x² + xy at the point (1, 2), what is ∇f?

Chapter 3: The Jacobian

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.

J =   [   ∂f1/∂x1    ∂f1/∂x2    …   ]
      [   ∂f2/∂x1    ∂f2/∂x2    …   ]
      [      ⋮         ⋮       ⋱    ]

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.

The gradient and the Jacobian are the same idea. When m = 1, a single output, the Jacobian collapses to exactly one row, and that row is the gradient from Chapter 2. The Jacobian isn't a new concept bolted onto the gradient, it's the gradient generalized to handle more than one output at once.

Hand computing the polar to Cartesian Jacobian

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:

x = r·cosθ      y = r·sinθ

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.

EntryFreeze, then differentiateResult
∂x/∂rfreeze θ: x = (cosθ)·r, a constant times rcosθ
∂x/∂θfreeze r: x = r·cosθ, derivative of cos is −sin−r·sinθ
∂y/∂rfreeze θ: y = (sinθ)·r, a constant times rsinθ
∂y/∂θfreeze r: y = r·sinθ, derivative of sin is cosr·cosθ
J =   [   cosθ    −r·sinθ   ]
      [   sinθ      r·cosθ    ]

Now plug in real numbers. Take r = 4, θ = 60° (that's π/3 radians), so cosθ = 0.5 and sinθ ≈ 0.866025.

J(4, 60°) =   [   0.5      −4·0.866025   ] = [   0.5000    −3.4641   ]
           [   0.866025    4·0.5   ]        [   0.8660     2.0000   ]

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 free correctness check. The determinant of this Jacobian is cosθ·(r·cosθ) − (−r·sinθ)·sinθ = r·cos²θ + r·sin²θ = r. Here, det J = 4, matching r = 4 exactly. This is not a coincidence, it's the reason area elements in polar coordinates carry a factor of r: the Jacobian determinant is that scaling factor, the amount a tiny patch of area stretches or shrinks under the map.
The Jacobian as a stretch and rotate map

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.

r4.0
θ (deg)60

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.

When to reach for it: a robot vacuum's lidar front end. A spinning lidar reports each hit as a bearing and range pair, exactly the polar format here. Before that scan can be dropped into an occupancy grid or a SLAM back end, which both expect Cartesian points, the sensor driver applies precisely this map, point by point, tens of thousands of times per second. The Jacobian in this chapter isn't a classroom abstraction, it runs inside real robot firmware on every single lidar return.
The trap: it's tempting to assume the Jacobian of the inverse map, Cartesian to polar, is just this matrix flipped upside down entry by entry. It's not that simple, though it's close: the Jacobian of an inverse map really is the matrix inverse J−1, not merely a relabeling. We'll compute the Cartesian to polar Jacobian directly, from scratch, in Chapter 6, because that's the direction our sensor model actually needs.
In the polar to Cartesian Jacobian, why is the second column, the θ column, scaled by r while the first column, the r column, is not?

Chapter 4: The Chain Rule, in Matrix Form

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:

Jg∘f(x) = Jg(f(x)) · Jf(x)

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.

A hand computed two stage example

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:

Jg =   [   2u1    1   ]
      [   u2     u1   ]

Evaluated at u = (2, 3.4641): Jg = [[4, 1], [3.4641, 2]]. Now multiply, row by column, tracking every number:

Entry of Jg·JfRow of Jg · Column of JfResult
(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
Jg∘f(4, 60°) =   [   2.866    −11.856   ]
           [   3.464     −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.

When to reach for it: PyTorch's autograd engine. Every call to .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))
Composing two stretch maps

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.

k (scale inside g)1.0
The trap: mixing up the order of multiplication. Matrix multiplication doesn't commute, Jg·Jf is not generally equal to Jf·Jg, and the dimensions often won't even match if you flip the order on a longer pipeline. Always build the product outside in, the last stage's Jacobian on the left, evaluated at that stage's own input.
For y = g(f(x)), at what point should you evaluate Jg when computing Jg∘f(x) = Jg(?)·Jf(x)?

Chapter 5: Numerical vs Analytic Jacobians

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.

forward: ∂f/∂x ≈ ( f(x + h) − f(x) ) / h
central: ∂f/∂x ≈ ( f(x + h) − f(x − h) ) / (2h)

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.

The trade off: too big, too small

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.

The classic step size 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.

step size h (10power)-4.0

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.

The EKF's number one bug, and its fix. The single most common way an Extended Kalman Filter silently produces garbage is a wrong analytic Jacobian: it compiles, it runs, the filter doesn't crash, it just quietly converges to the wrong answer or diverges after enough steps. The standard defense, used in real autopilot and SLAM codebases, is a Jacobian check: compute your analytic H or F, compute a central finite difference version of the same matrix at the same point, and assert they agree to several decimal places before ever flying the code. If they disagree, trust the finite difference and go hunting for your algebra mistake.

The same computation, three ways

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.

The trap: using the forward difference for a Jacobian check and calling it good when it only matches to two decimal places. Forward differences carry error proportional to h, central differences carry error proportional to h², a much smaller number once h is small. If your check tolerance is loose because you used a forward difference, you can miss a genuinely broken analytic Jacobian. Always check with central differences.
Why does making the finite difference step size h extremely tiny, say 10−15, make the estimate WORSE, not better?

Chapter 6: Linearizing a Measurement Model

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:

h(x, y) = ( √(x² + y²),   atan2(y, x) )

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:

H = ∂h/∂[x, y]

Computing H by hand

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:

∂bearing/∂x = −y / range²      ∂bearing/∂y = x / range²
H =   [   x/r     y/r   ]
      [   −y/r²   x/r²   ]     where r = √(x²+y²)

Plug in a concrete drone position: (x, y) = (3, 4), so r = √(9+16) = √25 = 5.

H(3, 4) =   [   3/5     4/5   ] = [   0.6    0.8   ]
         [   −4/25   3/25   ]    [   −0.16   0.12   ]

A remarkable hidden structure

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:

H·HT =   [   1    0   ]
         [   0   1/r²   ]

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.

Where it fails: bearing near zero range. Look at the bottom row of H again: it's scaled by 1/r². As the drone flies toward the dish, r shrinks toward zero, and 1/r² explodes toward infinity. A tiny, ordinary position uncertainty in x and y gets amplified into a wildly uncertain bearing estimate right at the point where the drone is closest, precisely where you might expect the sensor to be most confident. Physically this makes sense: standing right on top of the dish, a millimeter of position error corresponds to a huge swing in "which direction is it from here." Mathematically, the linear approximation itself becomes untrustworthy at exactly this point, since the Jacobian is changing violently from one nearby point to the next.
Bearing sensitivity as range shrinks

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.

drone x3.0
drone y4.0
When to reach for it: submarine bearings only tracking. Passive sonar gives a submarine bearing to a contact with no range at all, the classic "bearings only tracking" problem in the estimation literature. Naval tracking filters are built entirely around managing this same 1/r blow up: range uncertainty stays enormous until the observer maneuvers, changing the geometry, because a bearing only measurement's information content collapses exactly when range is poorly known and the target could be near or far along the same line of sight.
As a drone flies directly toward the tracking dish, what happens to the linearized bearing uncertainty, and why?

Chapter 7: Linearizing Dynamics

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:

f(x, y, θ) = ( x + v·cosθ·dt,   y + v·sinθ·dt,   θ + ω·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.

Computing F by hand

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:

∂/∂θ (v·cosθ·dt) = −v·sinθ·dt      ∂/∂θ (v·sinθ·dt) = v·cosθ·dt
F =   [   1   0   −v·sinθ·dt   ]
      [   0   1    v·cosθ·dt    ]
      [   0   0        1          ]

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.

F =   [   1   0   −0.1000   ]
      [   0   1    0.1732    ]
      [   0   0    1.0000    ]

Why we need F: propagating the covariance

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:

Pnew = F·P·FT

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·PComputationResult
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 PnewRow of FP · column of FTResult
(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
Pnew =   [   0.0401   −0.0002   −0.0010   ]
           [   −0.0002    0.0403     0.0017    ]
           [   −0.0010    0.0017     0.0100    ]
The interesting part isn't the diagonal, it's the new off diagonals. We started with a perfectly diagonal P, x, y, and θ all independent. After one FPFT sandwich, position and heading became correlated, the (1,3) and (2,3) entries are no longer zero. That's not a bug, it's exactly what should happen: because heading feeds into how position updates through sine and cosine, uncertainty in heading necessarily spills into uncertainty about where the robot ends up. In a real filter you'd then add a small process noise matrix Q, representing wheel slip and other unmodeled effects, with one more elementwise addition, Pnew + Q.
One motion step, watching the ellipse tilt

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 θ.

heading θ (deg)30
v·dt2.0
When to reach for it: an autonomous car's local motion filter. A self driving car's short horizon localization filter uses exactly this bicycle style motion model between GPS or lidar fixes. Every control cycle, the filter runs an FPFT sandwich to grow the position uncertainty forward in time, and the heading dependent tilt you just saw in the widget is precisely why the car's uncertainty ellipse stretches out along its direction of travel rather than staying a neat circle.
Starting from a diagonal (uncorrelated) covariance P, why does Pnew = FPFT end up with nonzero off diagonal entries between position and heading?

Chapter 8: The Linearization Lab

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.

Drag the drone. Compare the true cloud to the linearized ellipse.

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).

position uncertainty σ0.8

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.

Reading the numbers, not just the picture. Recall from Chapter 6 that the linearized measurement covariance is exactly σ²·diag(1, 1/r²), no eigen decomposition required, since H·HT is already diagonal in range bearing coordinates. That means the ellipse's horizontal half width is always σ, unchanged by position, while its vertical half width is σ/r, growing without bound as the drone approaches the origin. Watch the readout above the plot: as r shrinks, the predicted bearing spread grows exactly like 1/r, matching Chapter 6's formula on the nose.
The trap: trusting the ellipse just because it's smooth and pretty. A Kalman gain computed from a linearized covariance that badly underestimates or misshapes the true uncertainty produces a filter that is either overconfident, trusting a measurement more than it should, or wildly inconsistent, rejecting good measurements because its own uncertainty bookkeeping has drifted from reality. This exact failure mode, an EKF quietly diverging near a sensor singularity, is the textbook motivation for the Unscented Kalman Filter, which propagates a small set of real sample points through the true nonlinear function instead of relying on one linearized ellipse. You just built, by hand, the picture that motivates that entire lesson.

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.

Chapter 9: Choose Your Tool

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.

ToolShapeCore questionReach for it when
Gradientvector, length nWhich single direction increases this one number fastest?Optimizing a scalar loss or cost, one output, many inputs
Jacobianmatrix, m by nHow does each of several outputs change with each input?Linearizing a vector valued map: a sensor model, robot kinematics, a layer in a network
Hessianmatrix, n by nHow is the slope itself curving, and in which directions?Classifying a critical point, running Newton's method, second order optimizers
Finite differencenumeric estimate of any of the aboveWhat'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
The one line summary of the whole lesson. A gradient is a Jacobian with one output row. A Hessian is the Jacobian of the gradient. A finite difference is how you check any of them when you don't trust your own algebra. Every one of these objects exists for a single reason: turning a curved, hard to reason about function into a flat, easy to reason about matrix, valid in a small neighborhood of one point.

Quick cheat sheet

ObjectFormula
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 ruleJg∘f(x) = Jg(f(x))·Jf(x)
Central finite difference∂f/∂x ≈ (f(x+h) − f(x−h)) / (2h)
Measurement JacobianH = ∂h/∂x, used in the Kalman gain and the covariance update
Process JacobianF = ∂f/∂x, used in Pnew = FPFT + Q

Where to go next

The thread of this lesson: nothing in the real world is actually linear, but almost everything looks linear if you zoom in far enough. The derivative, the gradient, and the Jacobian are three sizes of the same idea: the best flat stand in for a curved thing, valid near one point, thrown away and rebuilt at the next.

"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.

Before flying new EKF code, you want to confirm your hand derived measurement Jacobian H is correct. Which tool from this chapter's table do you reach for?