Part III: State Estimation and Classical Inference
Chapter 11: Factor Graphs and Smoothing

MAP estimation and nonlinear least squares

"You asked me for the most probable trajectory. I quietly turned that into a pile of squared residuals, because that is the only shape of problem I actually know how to minimize."

A Pragmatic AI Agent

The Big Picture

Section 11.1 drew the factor graph: variables for the states you want to estimate, factors for every constraint a sensor or a motion model imposes on them. A graph is a picture, not an answer. This section is where the picture becomes a number. The goal is the maximum a posteriori (MAP) estimate: the single configuration of all states that is most probable given every measurement at once. The decisive move, and the one idea to carry out of this section, is that maximizing a product of Gaussian factors is exactly the same as minimizing a sum of squared, noise-weighted residuals. That equivalence converts "find the most likely trajectory" into "solve a nonlinear least squares problem," and nonlinear least squares is a solved discipline with fast, sparse, well-understood algorithms. Every modern smoother, from the pose graph in a warehouse robot to the visual-inertial odometry in a headset, is running the loop you are about to build: linearize, solve a sparse linear system, step, repeat.

This section assumes you are comfortable with priors, likelihoods, and posteriors from Chapter 4, with the measurement-model and sensor-noise language of Chapter 2, and with the factor-graph vocabulary of Section 11.1. Unlike the recursive filters of Chapter 9, which touch one state at a time, MAP estimation optimizes the whole trajectory jointly.

From posterior to a sum of squares

Collect every state into one stacked vector \(X = \{x_1, \dots, x_n\}\) and write \(Z\) for all measurements. The factor graph says the posterior factors into a product over the graph's factors,

$$p(X \mid Z) \;\propto\; \prod_{i} \phi_i(X_i),$$

where each \(\phi_i\) touches only its local subset of variables \(X_i\). The MAP estimate is the maximizer, \(X^\star = \arg\max_X \prod_i \phi_i(X_i)\). Products are awkward to optimize, so we take the negative logarithm, which turns the product into a sum and the maximization into a minimization. When each factor is Gaussian, \(\phi_i(X_i) \propto \exp\!\big(-\tfrac12 \lVert h_i(X_i) - z_i \rVert^2_{\Sigma_i}\big)\), the negative log of each factor is precisely a squared, covariance-weighted error term. The whole problem becomes

$$X^\star \;=\; \arg\min_X \; \sum_i \big\lVert h_i(X_i) - z_i \big\rVert^2_{\Sigma_i}, \qquad \lVert e \rVert^2_{\Sigma} = e^\top \Sigma^{-1} e.$$

Here \(h_i\) is the measurement function (what the sensor would read if the state were \(X_i\)), \(z_i\) is what it actually read, and the residual \(e_i = h_i(X_i) - z_i\) is the disagreement. The norm \(\lVert \cdot \rVert_\Sigma\) is the Mahalanobis distance: it divides each residual by its uncertainty, so a millimeter of disagreement from a precise lidar counts far more than a meter from a noisy GNSS fix. This is where sensor noise models from Chapter 2 earn their keep: \(\Sigma_i\) is the currency in which competing sensors bid for influence.

Key Insight

MAP estimation over a Gaussian factor graph is weighted nonlinear least squares: not two techniques that happen to agree, but one objective wearing two different notations. This is why the entire smoothing literature borrows its machinery, Gauss-Newton, Levenberg-Marquardt, sparse Cholesky, directly from numerical optimization rather than inventing anything probabilistic. A prior on a state (a belief before any measurement) is just one more factor and one more squared term; a maximum-likelihood estimate is the same objective with the prior removed. Nothing in the solver knows or cares which terms came from priors, motion models, or sensors.

Linearize, solve, repeat: Gauss-Newton

The catch is that \(h_i\) is almost never linear. A range sensor involves a square root; a camera projection divides by depth; a rotation is trigonometric. Stack all residuals into one vector \(r(X)\), weighted so the objective is simply \(\tfrac12 \lVert r(X) \rVert^2\). We cannot minimize this in closed form, but we can linearize it around the current estimate \(X\). A first-order Taylor expansion replaces the residual with \(r(X + \Delta) \approx r(X) + J\,\Delta\), where \(J = \partial r / \partial X\) is the Jacobian stacking every factor's local derivative. Substituting and setting the gradient to zero gives the normal equations:

$$J^\top J \, \Delta \;=\; -\,J^\top r(X).$$

Solve this linear system for the update \(\Delta\), take the step \(X \leftarrow X + \Delta\), relinearize at the new point, and repeat until \(\Delta\) is negligible. That is Gauss-Newton. The matrix \(\Lambda = J^\top J\) is the information matrix, and its sparsity is the whole reason smoothing scales: because each factor touches only a few variables, \(\Lambda\) is mostly zeros with structure that mirrors the graph. A sparse Cholesky factorization solves a problem with thousands of poses in milliseconds. That same \(\Lambda\) also delivers the estimate's covariance, the topic of Section 11.6.

Gauss-Newton is fast near a good guess but can overshoot and diverge when the linearization is poor, because the quadratic model it trusts is accurate only close to the current estimate. Levenberg-Marquardt fixes this with a damping term, solving \((\Lambda + \lambda\,\mathrm{diag}(\Lambda))\,\Delta = -J^\top r\). Large \(\lambda\) shrinks the step toward a cautious gradient-descent move; small \(\lambda\) recovers the aggressive Gauss-Newton step. Damping with \(\mathrm{diag}(\Lambda)\) rather than the identity matters: it scales the penalty to each variable's own curvature, so a poorly constrained direction, a beacon geometry that barely pins down one axis, is damped in proportion to how little the data already determines it, rather than every state being treated as equally uncertain. The solver raises \(\lambda\) when a step worsens the error and lowers it when a step succeeds: a trust-region rule, shrinking its trust in the linear model after being misled and expanding it while things go well. That is why Levenberg-Marquardt is the default whenever the initial guess is rough, a pose graph seeded from raw, drifting odometry, while plain Gauss-Newton suffices once the estimate is already close.

Common Misconception: convergence is not correctness

Watching the residual shrink and the step vanish feels like proof that Gauss-Newton found the answer, but a converged solver has only reached a local minimum near wherever it started; it has not proven there is nowhere better to be. Range-only positioning with too few or poorly spread beacons, or a pose graph whose linearization drifted off course early, can converge cleanly to a self-consistent but wrong configuration: two receiver positions predicting nearly identical ranges look identical to a solver that only checks whether the gradient is zero. Practical systems therefore pair MAP estimation with a sane initial guess (odometry, GNSS, a coarse global method) and, in Section 11.5, robust cost functions that make bad convergence targets less attractive to begin with.

Practical Example: a delivery robot squaring up its loop

A warehouse delivery robot drives a long loop and returns to a shelf it already mapped. Wheel odometry has drifted, so the raw trajectory does not close: the estimated end pose sits half a meter from the true start. In a factor-graph smoother, every odometry step is a between-factor and the recognized shelf is a loop-closure factor tying the last pose back to an early one; together they define one MAP objective. Gauss-Newton spreads the half-meter error backward across all the intermediate poses in proportion to each step's covariance, so no single pose absorbs the whole correction, and after three iterations the loop closes cleanly. The robot did not re-drive anything; it re-optimized the whole trajectory at once, which a filter processing one pose at a time cannot do. This is the computational heart of the SLAM systems in Chapter 52.

Watching Gauss-Newton run

The mechanics are clearest on a problem small enough to print. Below, a static receiver at unknown 2D position estimates its location from noisy range measurements to four beacons at known coordinates, a stripped-down version of the positioning problem in Chapter 25. Each range is one nonlinear factor; we build the residual vector and its Jacobian by hand and iterate the normal equations.

import numpy as np

beacons = np.array([[0., 0.], [10., 0.], [0., 10.], [10., 10.]])
true_xy = np.array([3.0, 4.0])
rng = np.random.default_rng(0)
sigma = 0.20                                     # range noise std (meters)
z = np.linalg.norm(beacons - true_xy, axis=1) + rng.normal(0, sigma, 4)

def residual_and_jacobian(x):
    d = x - beacons                              # vectors from each beacon
    rng_pred = np.linalg.norm(d, axis=1)         # predicted ranges h(x)
    r = (rng_pred - z) / sigma                   # whitened residuals
    J = (d / rng_pred[:, None]) / sigma          # d h / d x, also whitened
    return r, J

x = np.array([1.0, 1.0])                         # deliberately poor start
for k in range(6):
    r, J = residual_and_jacobian(x)
    delta = np.linalg.solve(J.T @ J, -J.T @ r)   # normal equations
    x = x + delta
    print(f"iter {k}: x={x.round(3)}  cost={0.5*r@r:8.3f}  step={np.linalg.norm(delta):.4f}")
print("estimate:", x.round(3), " truth:", true_xy)
Gauss-Newton for range-based positioning. Dividing the residual and Jacobian by sigma applies the Mahalanobis weighting: it is the whitening that turns MAP into ordinary least squares. Watch cost plunge and step shrink toward zero within a few iterations, even from a poor initial guess.

The loop is the entire section in eight lines: form residuals, build the Jacobian, solve \(J^\top J\,\Delta = -J^\top r\), step. The information matrix \(J^\top J\) here is a dense \(2\times 2\) because every factor touches the one variable, but swap in a trajectory of a thousand poses and that matrix becomes banded and sparse, and only the solver call changes.

Right Tool: let the optimizer own the loop

The hand-built loop above is for understanding, not production. scipy.optimize.least_squares collapses the whole iteration, damping schedule, convergence tests, and even a numerical Jacobian if you omit the analytic one, into a single call: pass a function returning the residual vector and get the optimum back. That is roughly fifteen lines of Gauss-Newton and Levenberg-Marquardt bookkeeping replaced by one function call, with a battle-tested trust-region step you did not have to tune. For factor graphs specifically, GTSAM and Ceres go further: you declare factors and variables, and the library discovers the sparsity, orders the elimination, and runs sparse Levenberg-Marquardt, which is the subject of Section 11.3.

What MAP gains, and what it costs

Why prefer this batch optimization to a recursive filter? Two reasons. First, relinearization: an extended Kalman filter linearizes each nonlinearity once, at the instant the measurement arrives, and can never revisit that choice; a smoother relinearizes every factor at every iteration around the improving estimate, so an early bad linearization gets corrected as later evidence arrives. This is why smoothing is markedly more accurate than filtering on strongly nonlinear problems, a limitation first met in Chapter 10. Second, joint consistency: MAP finds the trajectory that best explains all measurements simultaneously, so a loop closure can correct poses hundreds of steps in the past, which no forward filter can do.

The costs are equally concrete. Solving over the whole trajectory costs more than a single filter update, which is why Section 11.3 introduces incremental smoothing to reuse work between solves. And least squares assumes Gaussian noise: a single gross outlier, a mismatched loop closure, a lidar return off a glass wall, enters as a squared term and can drag the entire estimate off; Section 11.5 replaces the squared cost with robust kernels to defend against exactly that. For now, hold the core equivalence: the most probable trajectory minimizes noise-weighted squared error, found by linearizing and solving sparse linear systems until it stops moving.

Research Frontier: differentiable nonlinear least squares

Recent libraries turn the Gauss-Newton loop itself into a differentiable layer inside a neural network. Meta AI's Theseus and the robotics-focused PyPose library expose sparse Levenberg-Marquardt as a PyTorch module, so gradients flow back through every linearize-solve-step iteration and a learned front end, a neural feature detector, a learned motion prior, a learned robust kernel, trains end-to-end against the final MAP estimate instead of being tuned by hand. DROID-SLAM pushes further, replacing hand-derived optical-flow Jacobians with a learned update operator wrapped around the same dense bundle-adjustment core this section builds by hand. The classical algorithm has not changed, normal equations, damping, sparsity; what has changed is where the solver's boundary sits, increasingly one differentiable layer among many rather than a fixed post-processing step.

Exercise

Start from the range-positioning code. (1) Print the information matrix \(J^\top J\) at convergence and relate its two eigenvalues to the beacon geometry; move all four beacons close together on one side and re-run, explaining why the estimate along the beacon-to-receiver direction becomes far less certain (geometric dilution of precision). (2) Corrupt one measurement by adding 5 meters to a single range and re-run; report how far the estimate moves, and argue from the squared-cost structure why one bad factor can dominate. (3) Replace the analytic Jacobian with a finite-difference approximation, confirm the iterates match, and note what you lose in speed and precision.

Self-Check

  1. Explain the exact step that turns "maximize the posterior" into "minimize a sum of squares." Which property of the factors makes the two identical rather than merely similar?
  2. In the normal equations \(J^\top J\,\Delta = -J^\top r\), what does the matrix \(J^\top J\) represent probabilistically, and why is its sparsity the reason smoothing scales to thousands of variables?
  3. Give one concrete advantage of batch MAP smoothing over an extended Kalman filter that comes specifically from relinearizing at every iteration, and one cost you pay for it.

What's Next

In Section 11.3, we make this loop incremental. Re-solving the entire trajectory from scratch every time a new measurement lands is wasteful when only a few variables actually change, so iSAM2 and the GTSAM library reuse the previous factorization, updating just the affected part of the sparse system. That turns the batch optimizer of this section into a smoother fast enough to run online, on a robot, in real time.