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_ptrfactors = vector<shared_ptr
- Rust: a
LieGroup/Manifoldtrait with associatedconst DIMand typeTangent;ValuesasHashMap<Key, Box<dyn Variable>>withAnydowncasting, or separate typed stores per variable type (faster, ECS-like).NoiseModelFactorNmaps to a trait plus a macro or const-generic arity. - Python/JAX: duck typing plus pytrees. You lose fixed-size performance; vectorize across factors of the same type instead (the "batch factors by type" approach used by GPU solvers, including GTSAM's own
BatchJacobianFactor/CUDA paths). - Keep
Retract/Localas the only way to update variables. If you ever add tangent vectors directly to rotation parameters, the port is wrong.
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:
- Faithful: an opaque handle plus a space vtable (works in C, Rust with
unsafeor arenas, Zig). - Idiomatic:
trait StateSpace { type State; fn distance(&self, a:&State, b:&State) -> f64; ... }(static dispatch) plus an erased wrapper for compound and runtime-configured spaces. - Data-oriented: store all tree states in a flat
Vec<f64>with stride = state size. This is the best performance and SIMD fit (it's what VAMP-style speedups exploit), and easy for NN structures. The cost is that compound spaces become layout descriptors.
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:
GTSAM_POSE3_EXPMAP= ON (default),GTSAM_ROT3_EXPMAP= ON,GTSAM_USE_QUATERNIONS= OFFGTSAM_TANGENT_PREINTEGRATION= ON,GTSAM_LIEGROUP_PREINTEGRATION= OFFGTSAM_SLOW_BUT_CORRECT_BETWEENFACTOR/_EXPMAPGTSAM_THROW_CHEIRALITY_EXCEPTION= ON (behavior on points behind cameras)GTSAM_DT_MERGING(discrete decision-tree leaf merging)
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
- 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'sStateSpace::sanityChecks,NearestNeighborsLinearas an NN oracle, and the PPM grid-world test problems intests/resources. - 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. - 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).
- End-to-end datasets (GTSAM):
examples/Datahasw100.graph,noisyToyGraph.txt,pose3example.txt,sphere2500.txt,Data/dubrovnik-*.txt(BAL), and the IMU exampleimuAndGPSdata.csv. Match the final error and the per-iteration error curve. Covariances fromMarginalsare a strong check of the linear layer. - 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 reuseompl_benchmark_statistics.pyand Planner Arena. - Property tests: random manifold elements →
Local(a, Retract(a, v)) ≈ vfor small v; triangle inequality for distances; returned paths valid atvalidSegmentCountresolution;PathSimplifiernever increases length or validity violations. - 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++)
- Eigen alignment. Fixed-size vectorizable members need
EIGEN_MAKE_ALIGNED_OPERATOR_NEW(pre-C++17) or C++17 aligned new. Mixing-march=nativeGTSAM with a non-native consumer, or differentEIGEN_MAX_ALIGN_BYTES, causes crashes instd::vector<Pose3>. TurnGTSAM_BUILD_WITH_MARCH_NATIVEoff for distributable builds. - Exceptions/RTTI. GTSAM uses both (exceptions for indeterminate systems and cheirality;
dynamic_castinValues::at<T>). OMPL usesdynamic_castfor goal types andompl::Exception. Embedded-fno-exceptions -fno-rttitargets need code changes. - Threads. OMPL's timed ptc spawns a thread; WASM without threads, or bare-metal, needs a polling deadline instead. GTSAM: build without TBB.
- Windows DLL export. GTSAM
GTSAM_EXPORTon templates; followUsing-GTSAM-EXPORT.md. - Static vs shared. OMPL defaults to static unless
OMPL_BUILD_SHARED. - Floating point.
-ffast-mathbreaks GTSAM's small-angle branches and NaN checks. Don't use it. - Packaging. Both ship
package.xml(ROS),vcpkg.json, and conda/pixi; GTSAM haspixi.toml. Reuse them for dependency pinning.
8. A suggested order of work
- Freeze upstream: pin commits (GTSAM
a74146d, OMPL5b209a0were studied here), build them, run their tests, and generate golden data and benchmark baselines. - GTSAM T0 (Lie groups + numerical derivative + manifold invariants), tested exhaustively. Everything else stands on this.
- GTSAM T1 symbolic → T2 linear (start with JacobianFactor + QR; add Hessian/Cholesky after) → T3 LM on
w100.graph/sphere2500.txtwith matching error curves. - 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). - 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.
- GTSAM T4 (ISAM2), validated update-by-update against upstream on an incremental dataset (
examples/VisualISAM2Example,Pose2SLAMExamplestreaming). - Domain tiers (IMU, cameras, smart factors; informed planners, control) driven by actual consumers.
- 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
- Port strategy chosen per layer (bind / rebuild / transliterate / re-architect)
- Upstream commits pinned; build flags frozen and documented
- Linear algebra backend chosen (fixed-size small matrices + dense QR/Cholesky + sparse ordering)
- Conventions documented: Pose3 tangent (ω,v), right perturbation, quaternion order (OMPL x,y,z,w; Eigen stores x,y,z,w but the constructor takes w,x,y,z)
- Oracles ported: numericalDerivative, factor Jacobian check, manifold invariants, StateSpace sanityChecks, NN linear oracle
- Golden data generator against upstream
- Unit tests ported alongside code
- Datasets reproduce upstream error curves (GTSAM)
- Benchmark harness with Planner-Arena-compatible logs (OMPL)
- Cancellation/ptc semantics preserved
- Threading model decided (TBB / ptc thread / parallel planners)
- Serialization format decided (replaces Boost.Serialization in both)
- License notices: both BSD; vendored Eigen (MPL2), METIS (Apache-2.0), CCOLAMD (BSD-3), Spectra (MPL2) keep their own terms