research.adeesh.inFoundationsMathGTSAMOMPLPorting

Porting guide

General guidance for porting GTSAM and OMPL, whether to another language (Rust, Python, Julia, TypeScript…), to a new platform (embedded, WASM, GPU, a non-x86 target), or onto a different math or runtime stack. Nothing here is specific to any one org's target. It covers what the libraries are structurally, what will break, and how to prove the port is correct.

1. First decide what "port" means

Strategy What you do When it's right
Bind keep the C++ and expose it via FFI (C ABI, pybind/nanobind, wasm-bindgen over Emscripten) you need the full feature set quickly and the target can run C++
Rebuild for a platform same code, new toolchain/arch (ARM, RTOS, WASM, Android/iOS) the target has a C++17 compiler; the problems are dependencies, alignment, threads, exceptions
Transliterate rewrite file by file, preserving structure you need a native-language library but want traceability back to upstream
Re-architect reimplement the ideas idiomatically (e.g. Rust traits instead of virtual classes, ECS-style state arrays instead of State*) performance/safety goals the C++ structure prevents; you accept divergence

Most real ports are hybrids: transliterate the core abstractions faithfully (so tests port 1:1), and re-architect the leaves. Decide per layer using the dependency maps below.

2. Scope: you don't need all of it

Both repos are large (GTSAM ≈ 160k LOC core + 27k unstable; OMPL ≈ 150k). Most users touch a small fraction. Cut scope by layer.

GTSAM: minimum viable core → extensions

Tier Contents Approx. LOC
T0: math base (traits, OptionalJacobian, Value, block matrices, numericalDerivative, Testable), geometry subset: Point2/3, Rot2/3, Pose2/3, SO3 ~15k
T1: inference core inference (Key, Symbol, Factor, FactorGraph, Ordering, VariableIndex, elimination/junction/Bayes trees), symbolic ~11k
T2: linear NoiseModel (+ robust), JacobianFactor, HessianFactor, GaussianConditional, Bayes net/tree, VectorValues, COLAMD binding ~15k
T3: nonlinear batch Values, NonlinearFactor(N), FactorGraph, GN, LM, Marginals, PriorFactor, BetweenFactor ~10k
T4: incremental ISAM2 (+ params, impl, clique), fixed-lag smoothers ~5k
T5: domains projection factors and cameras/calibrations, smart factors, IMU preintegration, GPS, Expressions ~30k+
T6: optional discrete, hybrid, constrained, basis, sfm/Shonan, certifiable, CUDA, unstable rest

T0–T3 already give you batch pose-graph SLAM, and you can validate it on examples/Data/*.g2o.

OMPL: minimum viable core → extensions

Tier Contents
T0 util (RNG, Console, Time, Exception), datastructures (NearestNeighbors + Linear + GNAT, BinaryHeap)
T1 base: State/StateSpace, RealVector, SO2, SO3, SE2, SE3, Compound; StateSampler; SpaceInformation; StateValidityChecker; DiscreteMotionValidator; ProblemDefinition; Goal{State,Region,SampleableRegion}; PlannerTerminationCondition; Planner; Cost/OptimizationObjective + PathLength
T2 geometric: PathGeometric, PathSimplifier, SimpleSetup, RRT, RRTConnect, RRT*, PRM, and SelfConfig
T3 informed samplers + BIT*/AIT*/EIT*, LazyPRM, KPIECE (+ projections, Grid), Dubins/ReedsShepp
T4 control (ControlSpace, propagators, ODE, control RRT/KPIECE/SST), constrained spaces, Benchmark
T5 multilevel, LTL/Syclop, experience planners, VAMP integration

3. Dependency maps and what to replace

GTSAM

Dependency Used for Port notes
Eigen 3.4 (vendored) everything; fixed-size matrices are pervasive you need fixed-size small-matrix types with no heap allocation, plus dense QR, Cholesky (LLT/LDLT), SVD, and eigendecomposition. Rust: nalgebra (fixed-size const generics) / faer (fast dense/sparse); Python: NumPy (slow for 3×3) or JAX; Julia: StaticArrays + LinearAlgebra
CCOLAMD (vendored C) default ordering small, plain C. Bind it or port it (≈ 3k LOC). Your ordering decides your fill-in, so a different ordering = different performance, and different round-off
METIS (vendored, optional) nested dissection optional; bind it or skip it
TBB (optional) parallel tree traversal in elimination replace with your runtime's task pool (rayon, OpenMP, GCD), or go sequential first
Boost (optional since 4.3) serialization, timing (chrono), some features drop; replace serialization with serde/protobuf/own format
Spectra sparse eigenvalues (Shonan, certification) only for T6
pybind11 + wrap Python/MATLAB bindings not needed for a native port; the .i files are a handy list of the public API surface
CppUnitLite tests trivial to replace

OMPL

Dependency Used for Port notes
Eigen constrained spaces, ProlateHyperspheroid, some samplers small surface
Boost.Graph PRM family, SPARS, PlannerData, LTL product graph, incremental_components (connected components in PRM) replace with your own adjacency list + A* / Dijkstra + union-find; ~1–2k LOC total
Boost.Serialization PlannerDataStorage, experience DBs, state serialization replace with your serialization format
Boost.Odeint control/ODESolver (RK4, Cash-Karp) any ODE lib, or write RK4/RK45 directly
Boost.Math constants, toms748 root finding, binomial distribution std/your lib
Boost.program_options demos/benchmark CLI drop
std::thread / atomics ptc thread, parallel planners, GoalLazySamples need a threading model or a polling-only ptc
FLANN, spot, Triangle, yaml-cpp optional skip initially

4. Design patterns you must translate deliberately

4.1 GTSAM's dual dispatch: traits (static) + Value (dynamic)

Geometry is generic at compile time (traits<T>), while factor graphs are heterogeneous at run time (Values = map<Key, unique_ptr>, factors = vector<shared_ptr>). In a port:

4.2 OptionalJacobian

Port it as "compute value; optionally write Jacobians into caller-provided buffers". Never allocate Jacobians you won't use, because error-only evaluation runs many times per LM iteration. Also have a way to verify every Jacobian numerically (see §6).

4.3 OMPL's opaque State* + space-owned allocation

OMPL avoids per-state virtual objects. A State is raw memory laid out by its space, and every operation goes through the space (space->distance(a,b)). Options:

Whatever you choose, preserve the operations contract (distance is a metric, interpolate(a,b,0)=a and (…,1)=b, enforceBounds idempotent, SO2 wrapping, SO3 quaternion sign ambiguity |⟨q1,q2⟩|). Port StateSpace::sanityChecks() and run it on every space.

4.4 Shared ownership graphs

Both libraries use shared_ptr heavily: Bayes tree cliques with parent pointers (weak), OMPL SpaceInformationPtr shared by planner, objective, sampler, and validity checker. In Rust, prefer arena + indices (cliques in a Vec<Clique>, parents as usize) over Rc<RefCell<>>. iSAM2's detach/reattach of subtrees becomes index bookkeeping, which is simpler and faster.

4.5 Callbacks and cancellation

OMPL's PlannerTerminationCondition is cooperative: a predicate polled in tight loops, optionally driven by a timer thread. Preserve the guarantee that every planner checks ptc at least once per iteration. In async runtimes, map it to a cancellation token plus deadline. User callbacks (isValid) are the hottest call. Avoid crossing language boundaries per call (Python validity checkers are the classic 100× slowdown). Batch them where possible: check a whole edge at once, which is VAMP's idea.

4.6 Build-time switches that change semantics (GTSAM)

Pick one configuration and freeze it. Record it in the port's README:

Generate your golden test data from an upstream build with exactly those flags.

5. Numerical invariants to preserve

Area Invariant Typical bug
SO(3) exp/log correct small-angle Taylor branches near θ≈0 and correct handling near θ≈π NaNs at identity; wrong log near π
Tangent ordering Pose3 = (ω, v); Pose2 = (x, y, θ) Jacobian columns swapped (rotation vs translation)
Perturbation side right-perturbation \(x\,\mathrm{Exp}(\delta)\) every Jacobian differs by an adjoint
Whitening factors linearize to \(\Sigma^{-1/2}(J\delta + r)\) LM converges but covariances wrong by a scale
Constrained noise (σ=0) needs the QR path, not Cholesky division by zero / NaN
Robust losses IRLS weights applied to the whitened residual outliers not down-weighted
LM damping added as prior factors; modelFidelity acceptance test; λ schedule different iteration counts, false divergence
Elimination the same ordering gives the same sparsity; check GaussianBayesTree structure on symbolic tests silent performance cliffs
Gauge freedom pose graphs need a prior/anchor; otherwise indeterminate system IndeterminateSystemException
OMPL distance SO3 distance = acos(|⟨q1,q2⟩|) = half the rotation angle (sign-invariant, max π/2); SE3 = weighted sum of components double cover makes planner wander
OMPL interpolation SO3 slerp along the shortest arc; SO2 wraps; Dubins/RS analytic paths jump through ±π
Motion checking bisection order; resolution = longestValidSegmentFraction × maxExtent (default 1%) ported planner passes through thin walls, or is 10× slower
RNG uniform quaternion sampling (Shoemake); goal bias 0.05; ball/ellipsoid sampling biased coverage of SO(3)

6. Testing strategy: how to know the port is right

  1. Port the test oracles first. For GTSAM that's numericalDerivative.h + factorTesting.h (EXPECT_CORRECT_FACTOR_JACOBIANS) + check_manifold_invariants + chartTesting.h. For OMPL it's StateSpace::sanityChecks, NearestNeighborsLinear as an NN oracle, and the PPM grid-world test problems in tests/resources.
  2. Port the unit tests 1:1 alongside each class (GTSAM: ~3,200 TESTs in 291 files; OMPL: ~100 Boost.Test cases). They're your executable spec. Expected values are hard-coded and come from the C++ build.
  3. Golden data from upstream. Build upstream C++ with your frozen flags and dump inputs and outputs: exp/log of random elements, Jacobians, linearized factors (A, b), eliminated conditionals (R, d) for small graphs, optimizer trajectories (error per iteration), and ISAM2 estimates after each update. Compare with tolerances (1e-9 for geometry, 1e-6–1e-4 for optimizer outputs).
  4. End-to-end datasets (GTSAM): examples/Data has w100.graph, noisyToyGraph.txt, pose3example.txt, sphere2500.txt, Data/dubrovnik-*.txt (BAL), and the IMU example imuAndGPSdata.csv. Match the final error and the per-iteration error curve. Covariances from Marginals are a strong check of the linear layer.
  5. Statistical acceptance (OMPL): exact trees differ (RNG distributions, see the OMPL page). Use ompl::tools::Benchmark-style runs: for each planner × problem, N≥50 runs, and compare success rate, time-to-first-solution, and cost-vs-time curves against upstream within confidence intervals. Keep the log format identical so you can reuse ompl_benchmark_statistics.py and Planner Arena.
  6. Property tests: random manifold elements → Local(a, Retract(a, v)) ≈ v for small v; triangle inequality for distances; returned paths valid at validSegmentCount resolution; PathSimplifier never increases length or validity violations.
  7. Performance baselines: GTSAM timing/ scripts (e.g. timeSFMBAL, timeIncremental), OMPL benchmark configs. Record upstream numbers on the same hardware before you start.

7. Platform-specific pitfalls (if rebuilding C++)

8. A suggested order of work

  1. Freeze upstream: pin commits (GTSAM a74146d, OMPL 5b209a0 were studied here), build them, run their tests, and generate golden data and benchmark baselines.
  2. GTSAM T0 (Lie groups + numerical derivative + manifold invariants), tested exhaustively. Everything else stands on this.
  3. GTSAM T1 symbolic → T2 linear (start with JacobianFactor + QR; add Hessian/Cholesky after) → T3 LM on w100.graph / sphere2500.txt with matching error curves.
  4. OMPL T0–T1 in parallel (it shares no code with GTSAM, but you can share your Lie-group/SO3 code if conventions are reconciled. Note that OMPL stores SO3 as quaternion (x,y,z,w), while GTSAM uses a matrix by default).
  5. OMPL T2: RRTConnect + RRT* + PRM + simplifier on the PPM grid-world tests and a 7-DoF arm problem with a simple sphere-based checker; set up the statistical benchmark harness.
  6. GTSAM T4 (ISAM2), validated update-by-update against upstream on an incremental dataset (examples/VisualISAM2Example, Pose2SLAMExample streaming).
  7. Domain tiers (IMU, cameras, smart factors; informed planners, control) driven by actual consumers.
  8. Keep an upstream-tracking log: both repos are active (GTSAM 4.3 added CUDA, MultifrontalSolver, and constructed Lie groups; OMPL 2.0 moved to nanobind and VAMP and added AORRTC/EIRM*). Record which upstream commits each ported file corresponds to.

9. Checklist