research.adeesh.inFoundationsMathGTSAMOMPLPorting

GTSAM deep dive

This page goes from the idea to the code. Each layer is explained from first principles, then mapped to the files that implement it. Source: borglab/gtsam develop @ a74146d, version 4.3.2.

0. The one-paragraph version

You have unknowns \(X = \{x_1,\dots,x_n\}\) and measurements \(z_i\). Each measurement depends on a small subset of unknowns, \(z_i = h_i(X_i) + \nu_i\) with Gaussian noise \(\nu_i \sim \mathcal{N}(0,\Sigma_i)\). The MAP estimate is

\[ X^{\star} = \arg\min_X \sum_i \tfrac12 \| h_i(X_i) \ominus z_i \|^2_{\Sigma_i} \]

GTSAM represents each term as a factor and the whole sum as a factor graph. It then minimizes the sum by repeating three steps:

  1. Linearize every factor at the current estimate, producing a Gaussian factor graph.
  2. Eliminate that linear graph variable by variable, producing a Bayes net or Bayes tree, i.e. a sparse triangular system.
  3. Back-substitute to get a tangent-space step \(\delta\), then retract each variable onto its manifold.

Everything else in the repo is either a type of variable (geometry), a type of factor (slam/navigation/sfm), a smarter way to do step 2 (orderings, multifrontal, iSAM2), or tooling.

1. Architecture at a glance

Library layers, bottom to top. Each layer only depends on the layers below it.

Layer Directory LOC* Role
Foundation gtsam/base 15.7k Eigen typedefs, manifold/Lie traits, OptionalJacobian, Value type erasure, block matrices, Testable, timing, serialization
Graph theory gtsam/inference 9.3k Keys, Factor, FactorGraph, Ordering, VariableIndex, elimination tree → junction tree → Bayes tree, all templated on factor type
Symbolic gtsam/symbolic 1.4k Inference instantiated with structure-only factors, used to predict fill-in and build trees
Linear gtsam/linear 25.2k JacobianFactor, HessianFactor, NoiseModel, GaussianConditional, VectorValues, dense QR/Cholesky elimination, iterative solvers (PCG, subgraph)
Nonlinear gtsam/nonlinear 26.5k Values, NonlinearFactor, NoiseModelFactorN, Expression autodiff, optimizers (GN, LM, Dogleg, GNC), ISAM2, Marginals, fixed-lag smoothers
Geometry gtsam/geometry 18.9k Rot2/3, Pose2/3, SOn, Unit3, Similarity3, cameras and calibrations, triangulation
Domain factors gtsam/slam, navigation, sfm, sam 37k Between/Prior/Projection/Range/Bearing factors, IMU preintegration, GNSS, smart factors, Shonan averaging
Beyond Gaussian gtsam/discrete, hybrid, constrained, basis, certifiable 30k Discrete factor graphs (decision trees), hybrid discrete-continuous inference, constrained optimization, Chebyshev/spline bases, SDP certification
Experimental gtsam_unstable 26.8k Incubator; things get promoted to gtsam/ with git mv
Bindings wrap, python, matlab 27k .i interface files → pybind11 / MATLAB code generator
Vendored gtsam/3rdparty — Eigen 3.4, CCOLAMD, METIS 5.1, Spectra, SuiteSparse_config, cephes

*Non-test .h/.cpp lines.

        python / matlab (wrap)                   ← generated from *.i
 ┌───────────────────────────────────────────┐
 │ slam  navigation  sfm  sam  hybrid  discrete│  ← factors & variable types
 ├───────────────────────────────────────────┤
 │ nonlinear   (Values, NonlinearFactorGraph,  │
 │              LM/GN/Dogleg, ISAM2, Expression)│
 ├───────────────────────────────────────────┤
 │ linear      (Jacobian/HessianFactor, noise, │
 │              GaussianBayesTree, PCG)        │
 ├───────────────────────────────────────────┤
 │ inference   (templated elimination machinery)│ ← symbolic is its simplest instantiation
 ├───────────────────────────────────────────┤
 │ geometry    base   (Lie/manifold traits)    │
 └───────────────────────────────────────────┘
            Eigen · CCOLAMD · METIS · TBB(opt) · Boost(opt)

2. Layer 1: manifolds without inheritance (gtsam/base)

2.1 The problem

An optimizer needs, for every variable type \(T\):

GTSAM must support types it doesn't own, such as Eigen::Vector3d and double, so it can't require a common base class. The solution is traits.

2.2 traits<T>: the central concept

gtsam/base/Manifold.h defines internal::Manifold<Class>, Lie.h defines internal::LieGroup<Class>, and VectorSpace.h defines internal::VectorSpace<Class>. A type opts in by specializing traits:

template <> struct traits<Pose3> : public internal::LieGroup<Pose3> {};

These helpers forward to the class's own retract/localCoordinates/Expmap/Logmap and expose dimension, TangentVector, and ChartJacobian. Concept checks (GTSAM_CONCEPT_ASSERT(HasManifoldPrereqs<Class>)) fail at compile time if a method is missing. There's also a runtime invariant you should copy into any port's tests:

check_manifold_invariants(a, b):  Local(a,a) ≈ 0   and   Retract(a, Local(a,b)) ≈ b

Hierarchy of structure: manifold_tag ⊂ group_tag ⊂ lie_group_tag ⊂ vector_space_tag. Vector spaces get Retract = + and Local = − for free.

2.3 Lie groups via CRTP

internal::LieGroup<Class, N> in Lie.h is a CRTP base. Given operator*, inverse(), AdjointMap(), and static Expmap/Logmap, it derives compose, between, retract, and localCoordinates, with Jacobians:

\[ \frac{\partial (g\,h)}{\partial g} = \mathrm{Ad}_{h^{-1}},\qquad \frac{\partial (g\,h)}{\partial h} = I,\qquad \frac{\partial\, g^{-1}}{\partial g} = -\mathrm{Ad}_g \]

That's literally the code: if (H1) *H1 = g.inverse().AdjointMap(); in compose, and if (H) *H = -derived().AdjointMap(); in inverse.

Chart conventions are a porting trap

GTSAM uses right-perturbation (\(x \oplus \delta = x \cdot \mathrm{Exp}(\delta)\), i.e. the tangent space is at \(x\), in the body frame). The tangent ordering for Pose3 is [rotation; translation] = \((\omega, v)\), which is the opposite of many other libraries (Sophus and manif use \((v,\omega)\)). Build flags change the retraction itself: GTSAM_POSE3_EXPMAP (full exp map vs. first-order), GTSAM_ROT3_EXPMAP vs. Cayley, GTSAM_USE_QUATERNIONS. A port has to pin these and document them. Any mismatch silently produces wrong Jacobians.

4.3 adds constructed Lie groups: ProductLieGroup<G,H> (G×H), SemidirectLieGroup (G⋉H), and TangentLieGroup (TG). This is how compound states like NavState, \(SE_2(3)\), and Gal3 get their structure without hand-written exp/log maps.

2.4 OptionalJacobian<Rows, Cols>: zero-cost optional outputs

Almost every geometric function has the signature

Point3 Pose3::transformFrom(const Point3& p, OptionalJacobian<3,6> Hself = {},
                                             OptionalJacobian<3,3> Hpoint = {}) const;

OptionalJacobian wraps an Eigen::Map over the caller's matrix memory, or nothing. The callee writes if (H) *H = ...;. This gives:

It uses placement-new to retarget the map (usurp). If you port to a language without this pattern, the closest equivalents are an out-parameter Option<&mut Matrix> (Rust) or nullptr-able spans.

2.5 Value / GenericValue<T>: type erasure for heterogeneous containers

An optimization problem mixes Pose3, Point3, Vector3 (bias), Cal3_S2… in one container. Value is the abstract base with dim(), retract_(Vector), localCoordinates_(Value), clone_(), equals_(), and print(). GenericValue<T> implements it by calling traits<T>. This is the bridge from compile-time generic code (traits) to run-time polymorphic code (factor graphs).

2.6 Block matrices

2.7 Misc. foundations

3. Layer 2: keys, factors, graphs (gtsam/inference)

3.1 Keys

A Key is a uint64_t. A Symbol('x', 3) packs a char into the top 8 bits and an index into the low 56 bits. LabeledSymbol adds a robot-id char, and EdgeKey packs two uint32s. Keys are opaque everywhere, and printing goes through a KeyFormatter.

3.2 Factor and FactorGraph<F>

Factor is just a KeyVector: the variables it touches. FactorGraph<F> is a vector<shared_ptr<F>> that may contain nulls, because removed factors leave holes so that indices stay stable. iSAM2 relies on this.

3.3 Why elimination is the heart of everything

Take a linear factor graph \(\sum_i \|A_i X_i - b_i\|^2\). To eliminate a variable \(x_j\):

  1. Gather every factor that touches \(x_j\), using VariableIndex (variable → factor indices).
  2. Combine them into one dense factor over \(x_j\) and its neighbors \(S_j\) (the separator).
  3. Factorize it into a conditional \(p(x_j \mid S_j)\) (a row block of \(R\)) and a new factor on \(S_j\) (the Schur complement).
  4. Put the new factor back into the graph. This is where fill-in comes from: \(S_j\) are now all connected.

Doing this for every variable in an ordering yields a Bayes net, a directed acyclic graph of conditionals. That's exactly an upper-triangular \(R\) in sparse form, solved by back-substitution. The algorithm is identical for any factor type that can be combined and split. GTSAM therefore implements it once, templated, in inference/, and instantiates it for symbolic, Gaussian, discrete, and hybrid factors.

// The pluggable dense kernel: (factors touching keys, keys) → (conditional, remaining factor)
using Eliminate = std::function<std::pair<shared_ptr<Conditional>, shared_ptr<Factor>>(
                                const FactorGraph&, const Ordering&)>;

For Gaussian graphs, EliminatePreferCholesky is the default (HessianFactor path; it falls back to QR when constrained noise is present). EliminateQR is the Jacobian/Householder path.

3.4 Orderings

Ordering::Colamd (default; constrained variants ColamdConstrainedLast/First force some keys to the end, which iSAM2 uses to keep the newest poses at the root), Ordering::Metis (nested dissection, better for large SfM graphs), Natural, or Custom. The ordering determines fill-in, which determines everything about performance. It is computed on the symbolic structure only.

3.5 From elimination tree to Bayes tree

eliminateSequential produces a BayesNet and eliminateMultifrontal produces a BayesTree. Both are mixed in by EliminateableFactorGraph<FG> (CRTP), so GaussianFactorGraph, SymbolicFactorGraph, DiscreteFactorGraph, and HybridGaussianFactorGraph all get the same API.

3.6 gtsam/symbolic

The same machinery with factors that carry keys only, no numbers. It's used to predict structure, to test the inference templates, and by iSAM2 to rebuild the top of the tree. If you port the inference layer, port symbolic first. It is the cleanest test bed (the testSymbolic* tests check exact tree shapes).

4. Layer 3: Gaussian world (gtsam/linear)

4.1 Noise models: whitening

A measurement with covariance \(\Sigma\) contributes \(\|r\|^2_\Sigma = \|\Sigma^{-1/2} r\|^2\). noiseModel::Base provides whiten(r) = \(\Sigma^{-1/2}r\), and also WhitenSystem(A, b), which whitens Jacobians too, so everything downstream sees unit covariance ("whitened" least squares). Hierarchy:

Model Represents
Gaussian full \(\Sigma\) stored as upper-triangular \(R\) with \(R^\top R = \Sigma^{-1}\)
Diagonal per-row sigmas
Constrained some sigmas = 0 → hard equality. These need the special Constrained::QR (a "staggered" QR), which is why EliminatePreferCholesky falls back to QR for them
Isotropic / Unit scalar σ / σ = 1
Robust wraps a base model plus an mEstimator (Huber, Cauchy, Tukey, GemanMcClure, DCS, L2WithDeadZone…). It is implemented by reweighting the whitened residual (IRLS-style: scale rows by \(\sqrt{w(\|r\|)}\))

4.2 JacobianFactor

This is the linear factor \(\tfrac12\|A X - b\|^2\) stored in a VerticalBlockMatrix (\([A_1 | \dots | A_k | b]\)). Key operations: error, gradient, hessianDiagonal, transposeMultiplyAdd (for iterative solvers), and eliminate. EliminateQR stacks all involved factors into one big Jacobian (columns ordered: frontals first, then separator), runs in-place Householder QR, then splitConditional(nrFrontals) cuts off the top rows as a GaussianConditional and leaves the remainder as a new JacobianFactor on the separator.

4.3 HessianFactor

This is the information form \(\tfrac12 x^\top G x - x^\top g + \tfrac12 f\) stored in a SymmetricBlockMatrix. Combining factors is just adding their Hessian blocks (cheap), and eliminating is partial Cholesky on the frontal block. That's why it is the default: for typical SLAM it's faster than stacking and QR-ing tall Jacobians. QR is more stable when the system is ill-conditioned.

4.4 GaussianConditional, GaussianBayesNet, GaussianBayesTree, VectorValues

4.5 Solvers

5. Layer 4: nonlinear (gtsam/nonlinear)

5.1 Values

std::map<Key, std::unique_ptr<Value>>. at<T>(key) does a checked dynamic_cast to GenericValue<T>. retract(VectorValues) maps each variable with its own manifold retract. localCoordinates does the inverse. Every optimizer iteration is: linearize at Values → solve → Values.retract(delta).

5.2 NonlinearFactor → NoiseModelFactor → NoiseModelFactorN<T1..Tn>

5.3 Expression<T>: reverse-mode autodiff on manifolds

Expression<Point2> e = project(transformTo(pose, landmark)) builds a tree of ExpressionNodes. Evaluating with Jacobians records an ExecutionTrace (forward pass, storing local Jacobians in a stack-allocated CallRecord arena). It then runs reverse accumulation (reverseAD1/2) into a JacobianMap keyed by variable. The templates are heavy because they keep fixed-size Jacobian types through the whole chain. ExpressionFactor<T> turns an expression plus measurement into a factor without writing derivatives. AdaptAutoDiff bridges Ceres-style autodiff functors.

5.4 Optimizers

All inherit NonlinearOptimizer, which has iterate() and optimize() (loop until converged by relative/absolute error decrease or max iterations).

Optimizer Step
GaussNewtonOptimizer solve \(J^\top J\,\delta = -J^\top r\) by elimination
LevenbergMarquardtOptimizer damped GN with a \(\lambda\) schedule (below)
DoglegOptimizer trust region mixing the GN step and the steepest-descent (Cauchy) step
NonlinearConjugateGradientOptimizer gradient-only
GncOptimizer<BaseParams> Graduated Non-Convexity: robust estimation that starts convex and gradually sharpens to TLS/Geman-McClure; wraps LM or GN

How LM damping is actually implemented (internal/LevenbergMarquardtState.h::buildDampedSystem): GTSAM does not modify \(J^\top J\) in place. It adds a prior factor per variable, \(\sqrt{\lambda}\,I\) (or \(\sqrt{\lambda\,\mathrm{diag}(J^\top J)}\) when diagonalDamping), to the linear graph, then eliminates the augmented graph normally. The tryLambda loop:

  1. build the damped system for the current \(\lambda\) and solve for \(\delta\),
  2. compute the linearized cost change (model-predicted decrease) and the actual cost change \(f(x) - f(x \oplus \delta)\),
  3. modelFidelity = actual / predicted; accept if it exceeds minModelFidelity, then decrease \(\lambda\) (Nielsen-style or by lambdaFactor),
  4. otherwise increase \(\lambda\) and retry. Give up at lambdaUpperBound.

Reproducing this exactly matters if you want bit-comparable iteration counts.

5.5 ISAM2: incremental smoothing

The Bayes tree makes incremental inference cheap. A new factor on variables \(K\) only invalidates the cliques containing \(K\) and their ancestors up to the root. ISAM2::update(newFactors, newTheta) (in ISAM2.cpp), in the order the code runs it:

  1. add new variables to \(\Theta\) (addVariables), and new factors to the nonlinear graph and VariableIndex;
  2. optionally evaluate the error before the update;
  3. mark keys involved in new or removed factors;
  4. every relinearizeSkip updates, mark variables whose current delta exceeds relinearizeThreshold (fluid relinearization: only variables that moved a lot get a new linearization point);
  5. findFluid: mark the cliques containing those variables plus their ancestors;
  6. retract \(\Theta_J \leftarrow \Theta_J \oplus \Delta_J\) for relinearized variables;
  7. linearize the new factors;
  8. if fill-in grew past adaptiveReorderThreshold, force a batch reorder (4.3 feature);
  9. recalculate: detach the top of the tree containing marked cliques, keep the orphan subtrees below, re-eliminate the top's factors plus cached boundary factors from the orphans with a constrained COLAMD (new variables last), re-attach the orphans. If more than about 65% of the variables are affected, do a full batch instead.

delta_ is updated lazily (updateDelta, with wildfire threshold propagation, or Dogleg). calculateEstimate() = \(\Theta \oplus \Delta\). Parameters: ISAM2Params (GaussNewton or Dogleg, factorization CHOLESKY/QR, relinearizeThreshold, relinearizeSkip, cacheLinearizedFactors, findUnusedFactorSlots…).

Related: NonlinearISAM (iSAM1, periodic batch relinearization), BatchFixedLagSmoother / IncrementalFixedLagSmoother (marginalize old states, keep a window), ExtendedKalmanFilter (a factor graph with a window of one), and Marginals (covariances from the Bayes tree).

6. Layer 5: geometry (gtsam/geometry)

Type Dim Notes
Point2/Point3 2/3 just Eigen::Vector2d/3d typedefs with vector-space traits
Rot2 1 stored as (cos, sin)
Rot3 3 3×3 matrix by default (GTSAM_USE_QUATERNIONS switches to quaternion). Expmap = Rodrigues; careful small-angle branches in SO3.cpp
Pose2 3 \(SE(2)\); tangent \((v_x, v_y, \omega)\)
Pose3 6 \(SE(3)\); tangent \((\omega, v)\); transformFrom/To, between, range, bearing
SO3, SO4, SOn 3/6/n(n−1)/2 SOn with dynamic dimension (used by Shonan averaging)
Unit3 2 direction on \(S^2\) (tangent basis computed per point; not a group)
Similarity2/3 4/7 Sim(3) for monocular scale
OrientedPlane3, Line3, EssentialMatrix, FundamentalMatrix 3/4/5/7 manifolds, not groups
ExtendedPose3, Gal3, SL4 9/10/15 4.3 additions (\(SE_K(3)\), Galilean group)
Calibrations — Cal3_S2 (5 params), Cal3DS2 (radial-tangential), Cal3Fisheye, Cal3Unified, Cal3Bundler, Cal3f, stereo
Cameras — PinholeCamera<Cal>, PinholePose<Cal> (fixed calibration), StereoCamera, SphericalCamera, CameraSet
triangulation.h — DLT and nonlinear triangulation, TriangulationResult (valid / degenerate / behind camera / outlier)

Each type comes with traits, Expmap/Logmap plus their derivatives (ExpmapDerivative, LogmapDerivative = right Jacobians), AdjointMap, and exhaustive numerical-derivative tests.

7. Layer 6: domain factors

7.1 gtsam/slam

PriorFactor<T>, BetweenFactor<T>, GenericProjectionFactor (pose + landmark → pixel), GeneralSFMFactor (also optimizes calibration), StereoFactor, RangeFactor, BearingFactor, BearingRangeFactor, OrientedPlane3Factor, EssentialMatrixFactor, and smart factors (SmartProjectionPoseFactor, SmartProjectionRigFactor). Smart factors don't add landmarks as variables. They triangulate internally and Schur-complement the landmark out (RegularImplicitSchurFactor, JacobianFactorQ/SVD), which is a big win in VIO. lago.h does linear initialization of planar pose graphs. InitializePose3 does chordal rotation init. dataset.h handles g2o/TORO file I/O (common test data in examples/Data).

7.2 gtsam/navigation: IMU preintegration

Between two keyframes \(i,j\) you have hundreds of IMU samples. Integrating them into the state would put hundreds of variables into the graph. Instead, preintegrate them into a relative motion \(\Delta R_{ij}, \Delta v_{ij}, \Delta p_{ij}\) that doesn't depend on the absolute state at \(i\), plus its covariance and its Jacobian with respect to the bias. When the bias estimate changes slightly, correct to first order instead of re-integrating (Forster et al.).

7.3 gtsam/sfm

SfmData/SfmTrack (BAL format), ShonanAveraging (certifiable rotation averaging by lifting to \(SO(p)\) and doing a Riemannian staircase), TranslationRecovery (1DSfM-style), MFAS (outlier rejection for translation directions), DsfTrackGenerator, TransferFactor (fundamental-matrix transfer), and SfmLevenbergMarquardt (a specialized BA optimizer, with a CUDA path).

8. Beyond Gaussian

9. Tooling, build, bindings, tests

  1. examples/OdometryExample.cpp → Pose2SLAMExample.cpp → PlanarSLAMExample.cpp
  2. base/Manifold.h, base/Lie.h, base/OptionalJacobian.h, geometry/Pose2.{h,cpp}
  3. nonlinear/NonlinearFactor.{h,cpp}, NoiseModelFactorN.h, slam/BetweenFactor.h
  4. linear/NoiseModel.h, JacobianFactor.cpp (EliminateQR, splitConditional), HessianFactor.cpp (EliminateCholesky)
  5. inference/EliminateableFactorGraph-inst.h, EliminationTree-inst.h, JunctionTree-inst.h, BayesTree-inst.h
  6. nonlinear/LevenbergMarquardtOptimizer.cpp + internal/LevenbergMarquardtState.h
  7. nonlinear/ISAM2.cpp + ISAM2-impl.h (alongside the iSAM2 paper)
  8. geometry/Rot3*.cpp, SO3.cpp, Pose3.cpp (exp/log, derivatives)
  9. navigation/PreintegrationBase.cpp, TangentPreintegration.cpp, ImuFactor.cpp (alongside Forster et al.)
  10. nonlinear/Expression*.h + internal/* (last; it's the hardest template code)