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

From filters to factor graphs

"A filter remembers the present and forgets how it got there. A factor graph keeps the receipts. When a late measurement contradicts an old belief, only one of them can revise history."

A Retrospective AI Agent

Prerequisites

This section assumes the recursive predict-correct filter and its Gaussian belief from Chapter 9, and the nonlinear variants (EKF, UKF) from Chapter 10. It leans on Bayes' rule, the joint-versus-marginal distinction, the multivariate Gaussian, and the information (canonical) form of a Gaussian, all developed in Chapter 4 and Appendix A. No graph theory beyond "nodes and edges" is required; the specific structure is built here.

The big picture

Every filter in Chapter 9 and Chapter 10 shares one design choice: at each step it compresses everything the system has ever seen into a single current belief, then throws the raw past away. That is exactly what you want on a microcontroller streaming inertial data, and exactly what you cannot afford when a measurement arrives late, when a robot revisits a place it mapped ten minutes ago, or when a single bad linearization early in the trajectory quietly poisons everything downstream. A factor graph is the alternative representation: instead of one evolving belief, it holds the entire estimation problem as a graph of unknowns and the constraints that tie them together, and it estimates the whole trajectory at once. This section shows what a factor graph is, why the same posterior a filter computes recursively can be written as a product of local factors, and why keeping the graph (smoothing) rather than collapsing it (filtering) is the move that unlocks loop closure, re-linearization, and everything else in this chapter. The rest of Chapter 11 is machinery on top of this one idea.

The cost of forgetting: filtering versus smoothing

A recursive filter answers one question: given all measurements up to and including now, what is the state now? Formally it tracks the filtering distribution \(p(\mathbf{x}_k \mid \mathbf{z}_{1:k})\). To stay recursive it marginalizes out \(\mathbf{x}_{k-1}\) the instant it moves to step \(k\), folding the old state into the process-noise-inflated prior for the new one. That marginalization is the source of both its speed and its amnesia. Once \(\mathbf{x}_{k-1}\) is gone, no future measurement can ever correct it, and no future evidence can revisit the linearization point that was used to fold it in.

Smoothing asks the larger question: given all measurements over some window, what is the entire trajectory \(p(\mathbf{x}_{0:K} \mid \mathbf{z}_{1:K})\)? Here nothing is marginalized away during processing. Every state stays an explicit unknown, so a measurement at step \(K\) can and does sharpen the estimate of \(\mathbf{x}_5\), and every nonlinear term can be re-linearized around the improved estimate rather than the first guess. The price is that you now solve for many states jointly instead of one at a time. The factor graph is the data structure that makes that joint problem tractable, because the joint posterior is not a dense tangle: it factors.

Key insight

Filtering and smoothing are not different algorithms so much as the same estimation problem read at two altitudes. A filter is a smoother that is forced to eliminate each variable the moment the next one appears. Whatever a filter marginalizes, a smoother keeps. Everything a factor graph lets you do that a Kalman filter cannot, correcting the distant past, re-linearizing, closing loops, absorbing out-of-order data, is a direct consequence of that one refusal to forget.

The joint posterior factors into local terms

Why can we hope to solve for hundreds of states at once? Because the measurement model is local. Odometry couples only consecutive poses; a range reading couples one pose to one landmark; a prior touches one state. By Bayes' rule the posterior over the whole trajectory is proportional to the joint, and the joint splits into a product of these local pieces,

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

where \(\mathbf{X}\) collects all unknowns, each factor \(\phi_i\) is a nonnegative function of only the small subset \(\mathbf{X}_i\) of variables it actually constrains, and the constant of proportionality is the evidence we never need. A factor is nothing more mysterious than one term in this product: a prior, a motion constraint, or a sensor likelihood. For the Gaussian noise models of Chapter 2, each factor is \(\phi_i(\mathbf{X}_i) \propto \exp\!\big(-\tfrac12 \lVert h_i(\mathbf{X}_i) - \mathbf{z}_i \rVert^2_{\Sigma_i}\big)\), so taking the negative logarithm turns the product into a sum of squared, noise-weighted residuals. That sum is the objective Section 11.2 minimizes; here we only need to see that it exists and is a sum of local pieces.

Anatomy of a factor graph

A factor graph is the bipartite graph of that factorization. It has two kinds of nodes. Variable nodes are the unknowns you want to estimate: robot poses, landmark positions, sensor biases, a clock offset. Factor nodes are the constraints, one per \(\phi_i\), and each factor node connects by an edge to exactly the variables in its argument \(\mathbf{X}_i\). There are no variable-to-variable or factor-to-factor edges. Reading the graph tells you everything about coupling: a variable's degree is how many measurements see it, and two variables share information only through a common factor. A dead-reckoning chain (Chapter 24) is a line of pose variables linked by odometry factors, with a prior factor pinning the first pose so the whole chain has an absolute reference.

This sparsity is not cosmetic. The negative-log-posterior is quadratic (after linearization), and its Hessian, the information matrix \(\boldsymbol{\Lambda}\), has a nonzero block for two variables exactly when a factor connects them. A chain of poses linked only to their neighbors produces a block-tridiagonal \(\boldsymbol{\Lambda}\): almost entirely zeros. The graph is the sparsity pattern, and that is what lets a smoother scale to thousands of variables instead of choking on a dense inverse. The snippet below assembles the information matrix for a short pose chain and shows the structure directly.

import numpy as np

# Tiny pose chain: 4 scalar states, 1 anchor prior + 3 odometry factors.
# Each factor contributes a rank-1 (or rank-2) block to the information matrix.
n = 4
Lambda = np.zeros((n, n))

def add_prior(Lambda, i, info):          # prior on x_i: touches one variable
    Lambda[i, i] += info

def add_between(Lambda, i, j, info):     # constraint x_j - x_i = odom: touches two
    J = np.zeros(n); J[i], J[j] = -1.0, 1.0
    Lambda += info * np.outer(J, J)

add_prior(Lambda, 0, 100.0)              # anchor the first pose strongly
for k in range(n - 1):                   # odometry links consecutive poses
    add_between(Lambda, k, k + 1, 10.0)

print((Lambda != 0).astype(int))         # sparsity: block-tridiagonal
Building the information matrix of a four-pose chain from its factors. Each factor stamps a small block into Lambda at the rows and columns of the variables it touches; a prior hits one diagonal entry, an odometry factor hits a 2x2 neighbourhood. The printed pattern is tridiagonal, which is the graph's line topology showing up as matrix sparsity. This is the object a smoother solves and a filter would eliminate one variable at a time.

Run it and the nonzero pattern is a narrow band hugging the diagonal, precisely mirroring the chain in the graph. Add one factor linking pose 3 back to pose 0 (a loop closure, the subject of Section 11.4) and two off-diagonal corners light up: the graph edge and the matrix entry are the same fact stated twice.

Filtering is elimination on the same graph

The unifying payoff: a Kalman filter and a batch smoother operate on the identical factor graph. They differ only in the order and aggressiveness of variable elimination. Solving a linear-Gaussian factor graph means eliminating variables one at a time, and eliminating a variable marginalizes it out, which introduces a new factor coupling all of its former neighbours (this induced factor is called a fill-in). A filter eliminates variables in strict time order and, crucially, discards each eliminated variable's node the moment it is gone, so at any instant it stores only the frontier: one belief over the current state. That frontier belief is exactly the Kalman filter's mean and covariance. A smoother runs the same elimination but retains every variable, so it can eliminate in a cheaper order and, when new evidence arrives, un-eliminate and re-solve. The recursive filter you already know is thus a special-purpose graph solver that happens to keep only a moving window of size one.

In practice: a warehouse robot that could not un-see a lie

A logistics team ran an EKF fusing wheel odometry and ceiling-marker sightings for an autonomous forklift. Early in each run, before enough markers were in view, a single mislabeled marker injected a large, wrong correction. The EKF folded that correction into its covariance and marginalized the poses that would have revealed the error, so the filter walked the rest of the aisle with a confident, silently biased pose; the mistake was mathematically unrecoverable because the evidence had already been thrown away. Rebuilding the same problem as a factor graph and smoothing over a sliding window changed the outcome without changing a single sensor: the mislabeled marker became one factor among many, its large residual stood out against the consistent majority (later downweighted by the robust costs of Section 11.5), and re-linearizing the retained early poses pulled the trajectory back onto the true aisle. Same measurements, same noise models; keeping the graph instead of collapsing it was the entire difference.

The right tool: declare the graph, not the linear algebra

Assembling the information matrix, choosing an elimination order, factorizing sparsely, and back-substituting is a few hundred lines of careful sparse-matrix code that is easy to get subtly wrong. A factor-graph library lets you declare the variables and factors and owns everything else. In GTSAM (the library this chapter builds toward in Section 11.3), the pose chain above is a handful of lines.

import gtsam
from gtsam import Pose2, BetweenFactorPose2, PriorFactorPose2

graph = gtsam.NonlinearFactorGraph()
prior_noise = gtsam.noiseModel.Isotropic.Sigma(3, 0.1)
odom_noise  = gtsam.noiseModel.Isotropic.Sigma(3, 0.2)

graph.add(PriorFactorPose2(0, Pose2(0, 0, 0), prior_noise))   # anchor pose 0
for k in range(3):                                            # odometry links
    graph.add(BetweenFactorPose2(k, k + 1, Pose2(1, 0, 0), odom_noise))

# graph now encodes the same factorization; solving is one call (Section 11.2)
The four-pose chain as a GTSAM factor graph. Compared with hand-rolling the information matrix, its sparse factorization, and the solve, this is roughly a 10-to-1 line reduction, and the library also handles the manifold structure of Pose2 (rotations do not add like vectors), the elimination ordering, and incremental re-solving. You supply only the variables, the factors, and the noise models.

Research frontier: the graph as the common language of estimation

Factor graphs have become the shared substrate of modern back-end estimation. GTSAM with its incremental smoother iSAM2, Google's Ceres Solver, and the g2o library are the workhorses behind production visual-inertial odometry and SLAM systems such as ORB-SLAM3 and VINS-Fusion (Chapter 52). The active frontier pushes the representation two ways: symbolic-differentiation toolchains like SymForce generate fast, exact factor Jacobians from a high-level spec, and differentiable factor graphs embed the whole solve as a layer inside a neural network so learned front ends and the optimizer train end to end. In every case the factor graph, not the recursive filter, is the abstraction people reach for first.

Exercise

Take the four-pose chain from the code above and treat it as a smoothing problem.

  1. Assemble the full information matrix \(\boldsymbol{\Lambda}\) and confirm by inspection that it is block-tridiagonal. Add a single "loop closure" factor between pose 3 and pose 0 and identify exactly which new entries become nonzero.
  2. Eliminate the variables in time order (0, then 1, then 2), and after each elimination note which remaining variables are now directly coupled. Explain where the fill-in comes from and why a pure filter never has to store it.
  3. Argue, in two or three sentences, why the loop-closure factor from part 1 makes the strict time-ordered elimination of a filter unable to benefit from that constraint without redoing past steps.
Hint

Eliminating a variable marginalizes it and connects all of its former neighbours to each other. In a plain chain each interior pose has two neighbours, so eliminating it links that pair; the loop-closure edge gives pose 0 an extra neighbour far away in time, which is why it survives only if that variable was never thrown away.

Self-check

  1. State the difference between the filtering distribution and the smoothing distribution in terms of which variables are marginalized and when.
  2. In a factor graph, what does an edge mean, and how does a variable's set of incident factors relate to a block of the information matrix?
  3. A colleague says "a Kalman filter and a factor-graph smoother are completely different methods." Correct them: in what precise sense are they the same computation performed differently?

What's Next

In Section 11.2, we turn the graph into an objective and solve it. Taking the negative logarithm of the product of Gaussian factors yields a sum of squared, noise-weighted residuals, and estimating the trajectory becomes a sparse nonlinear least-squares problem. We derive the maximum-a-posteriori estimate, linearize with the same Jacobians from Chapter 10, and see why Gauss-Newton and Levenberg-Marquardt, running on the sparse structure this section exposed, solve for thousands of states faster than intuition suggests.