research.adeesh.inFoundationsMathGTSAMOMPLPorting

OMPL deep dive

This page goes from the idea to the code. Source: ompl/ompl main @ 5b209a0, version 2.0.2 (ABI 19), C++17.

0. The one-paragraph version

The robot's configuration space \(\mathcal{C}\) is a manifold such as \(\mathbb{R}^n\), \(SE(3)\), or products of these. A black-box predicate splits it into \(\mathcal{C}_{free}\) and \(\mathcal{C}_{obs}\). Given a start \(q_s\) and a goal region \(G\), find a continuous path \(\sigma:[0,1]\to\mathcal{C}_{free}\) with \(\sigma(0)=q_s\) and \(\sigma(1)\in G\), optionally minimizing a cost \(c(\sigma)\). Computing \(\mathcal{C}_{obs}\) explicitly is exponential in dimension, so OMPL never does it. Instead it samples configurations, checks them with the predicate, connects nearby valid samples with local straight-line motions (also checked), and grows a tree or roadmap until start and goal are connected. Everything in OMPL is one of four things: describing the space, describing the problem, a strategy for sampling and connecting (a planner), or post-processing and benchmarking the result.

1. Architecture at a glance

Directory LOC Role
src/ompl/base 34k Abstractions: StateSpace, State, StateSampler, StateValidityChecker, MotionValidator, SpaceInformation, ProblemDefinition, Goal*, OptimizationObjective, Planner, PlannerData, PlannerTerminationCondition
base/spaces 14k Concrete spaces: RealVector, SO2, SO3, SE2, SE3, Dubins, ReedsShepp, Discrete, Time, SpaceTime, Vana/Owen (3D Dubins variants), constraint/ (Projected, Atlas, TangentBundle)
base/samplers 3.8k Valid-state samplers (Uniform, Gaussian, Bridge-test, ObstacleBased, MaxClearance), informed samplers (PathLength direct, rejection), deterministic (Halton, precomputed sequences)
base/objectives, base/goals 2.5k Path length, clearance, mechanical work, state-cost integral, multi-objective; GoalState(s), GoalRegion, GoalSampleableRegion, GoalLazySamples
geometric 62k SimpleSetup, PathGeometric, PathSimplifier, and ~60 geometric planners
control 15k Kinodynamic planning: ControlSpace, Control, StatePropagator, ODESolver, PathControl, and control planners (RRT, KPIECE, EST, PDST, SST, Syclop, LTL, HyRRT/HySST)
multilevel 13k Planning over a hierarchy of simplified spaces (QRRT, QMP)
datastructures 5.4k Nearest neighbors (GNAT, sqrt-approx, linear, FLANN), BinaryHeap, Grid/GridB/GridN, PDF, AdjacencyList, LPAstarOnGraph
tools 8.9k Benchmark, SelfConfig (picks defaults), ParallelPlan, OptimizePlan, Lightning/Thunder (experience DBs), Profiler
util 2.2k RNG, Console (logging), Time, Exception, ClassForward, ProlateHyperspheroid
vamp/ + external/vamp — Adapters for VAMP (SIMD-vectorized collision checking)
py-bindings — nanobind bindings (2.x replaced the old Py++/Boost.Python generator)
tests — Boost.Test unit tests, regression tests, benchmarks
           user code / MoveIt / Python
                       │
              geometric::SimpleSetup  ──── tools::Benchmark, SelfConfig
                       │
      ┌────────────────┼─────────────────────┐
      ▼                ▼                     ▼
 base::Planner   base::ProblemDefinition   base::SpaceInformation
 (RRT, PRM, ...)  start states, Goal,       ├─ StateSpace (+ samplers, projections)
                  OptimizationObjective     ├─ StateValidityChecker   ← YOUR collision code
                                            └─ MotionValidator (discrete subdivision)

2. The problem description layer (ompl/base)

2.1 StateSpace and State: describing the manifold

A State is an opaque, empty base struct. Each space defines its own StateType subclass (RealVectorStateSpace::StateType { double *values; }, SO3StateSpace::StateType { double x,y,z,w; }) and casts with state->as<T>(). CompoundStateSpace / CompoundState hold a State** array of components with weights, so SE3 = RealVector(3) × SO3.

Memory is managed by the space, not by C++ constructors: allocState() / freeState() / copyState(). This lets OMPL allocate polymorphic states without knowing their type, and keeps planners generic.

The StateSpace interface (pure virtuals in bold) is the full set of operations a planner may perform on states:

Method Meaning
getDimension() manifold dimension
getMaximumExtent() the largest possible distance(); used to scale step sizes
getMeasure() volume; used by RRT*/PRM* connection radii
distance(a,b) a metric. For SO3 it is acos(|⟨q1,q2⟩|), which is half the rotation angle, so SO3's max extent is π/2 (see Math 6)
interpolate(a,b,t,out) the local planner's geodesic (SO3 uses slerp)
enforceBounds / satisfiesBounds clamp or wrap (SO2 wraps to \([-\pi,\pi)\), SO3 normalizes)
equalStates, copyState, allocState, freeState memory and equality
allocDefaultStateSampler() a uniform sampler on the space
validSegmentCount(a,b) how many collision checks along a→b = ceil(distance / longestValidSegment)
getLongestValidSegmentFraction() default 0.01 of max extent. This is the single most important resolution parameter in OMPL.
registerProjections() low-dimensional projections (\(\mathbb{R}^k\)) used by KPIECE/EST/SBL/PDST grids
serialize/deserialize, copyToReals/copyFromReals flatten to double[]
sanityChecks() numeric tests (triangle inequality, interpolation endpoints…); run them on any custom/ported space

Car-like spaces override distance/interpolate with analytic optimal curves: DubinsStateSpace (forward only, 6 word types LSL, RSR…) and ReedsSheppStateSpace (forward/backward, 48 candidate words). These are non-symmetric or non-holonomic, which matters for bidirectional planners (hasSymmetricInterpolate).

2.2 Constrained spaces (base/spaces/constraint)

When valid states lie on an implicit manifold \(F(q)=0\) (holding a tray level, closed chains), you supply a Constraint (\(F\) and its Jacobian). Three wrappers make that manifold look like an ordinary StateSpace:

Any geometric planner then works unchanged. This is a good example of OMPL's design: planners never know.

2.3 SpaceInformation: the planner's view of the world

SpaceInformation bundles a StateSpace with a StateValidityChecker and a MotionValidator and forwards to them: isValid(s), checkMotion(s1,s2), distance, allocState, allocStateSampler, getMotionStates. Planners talk to si_ and nothing else.

2.4 ProblemDefinition, goals, objectives

2.5 Planner, termination, status, data

class Planner {
  virtual PlannerStatus solve(const PlannerTerminationCondition &ptc) = 0;
  virtual void setup();          // read params, allocate NN structures
  virtual void clear();          // drop the tree/roadmap
  virtual void clearQuery();     // keep the roadmap, drop start/goal (multi-query planners)
  virtual void getPlannerData(PlannerData &) const;  // export graph for visualization/storage
  PlannerSpecs specs_;           // recognizedGoal, multithreaded, approximateSolutions, optimizingPaths, directed...
  ParamSet params_;              // string-keyed parameters ("range", "goal_bias") for benchmarking/MoveIt config
};

3. The planners: how they work, and where

3.1 Anatomy: RRT, line by line

geometric/planners/rrt/src/RRT.cpp solve() is the archetype. Paraphrasing:

add all valid start states to nearest-neighbor structure nn_
while !ptc():
    q_rand ← goal sample (prob goalBias_=0.05, if GoalSampleableRegion) else uniform sample
    q_near ← nn_.nearest(q_rand)
    if dist(q_near,q_rand) > range: q_new ← interpolate(q_near, q_rand, range/d) else q_new ← q_rand
    if si.checkMotion(q_near, q_new):
        add Motion{q_new, parent=q_near} to nn_
        if goal.isSatisfied(q_new, &d): solution ← it; break
        track best approximate solution by goal distance d
walk parent pointers back, build PathGeometric, pdef.addSolutionPath(path, approximate, d)

Every tree planner is a variation on these four primitives: sample, nearest, steer, check. range (maxDistance_) defaults to 0.2 × getMaximumExtent() via SelfConfig::configurePlannerRange (magic::MAX_MOTION_LENGTH_AS_SPACE_EXTENT_FRACTION).

3.2 Planner families

Family Planners Idea Properties
RRT RRT, RRTConnect (default), LazyRRT, pRRT, TRRT/BiTRRT (cost-map transition test), VFRRT, TSRRT, LBTRRT, STRRT* (space-time) grow tree(s) toward random samples single-query, probabilistically complete
Optimal RRT RRT*, InformedRRT*, SORRT*, RRT#, RRTX-static, AORRTC/AOXRRTConnect (asymptotically optimal RRT-Connect) rewire within radius \(r(n)\propto(\log n/n)^{1/d}\) or \(k(n)=k_{RRT}\log n\) asymptotically optimal (AO), anytime
Informed / batch BIT*, ABIT*, AIT*, EIT*, EIRM*, BLIT* batches of samples treated as an implicit RRG; graph search (A*/LPA*) ordered by heuristic; informed sampling of an ellipsoid \(\{q: \|q_s-q\|+\|q-q_g\| < c_{best}\}\) AO, usually the strongest optimizers in OMPL
FMT FMT*, BFMT* fast marching over a fixed batch, lazy collision checks AO, single batch
PRM PRM, PRM*, LazyPRM, LazyPRM*, SPARS, SPARS2 build a roadmap (multi-query), answer queries with A* (Boost.Graph astar_search); Lazy defers edge checks until a path is found; SPARS keeps a sparse near-optimal spanner multi-query; PRM* AO
Projection / grid KPIECE1, BKPIECE1, LBKPIECE1, EST/BiEST/ProjEST, SBL/pSBL, PDST, STRIDE project states to a low-D grid; expand from less-explored cells (KPIECE: interior/exterior cell importance) good in high-D and with controls; needs a projection
Sparse / kinodynamic-friendly SST keep only "witness" best nodes; near-optimal with few nodes AO (asymptotically near-optimal)
Experience Lightning, Thunder (geometric/planners/experience, tools/lightning|thunder) retrieve and repair past paths from a database —
Meta CForest (parallel RRT* sharing best paths), AnytimePathShortening (run several planners in threads, hybridize the results), XXL (decompositions for high-D) — —

3.3 RRT* rewiring in practice

RRTstar.cpp: per iteration, the neighborhood is nn_->nearestK(motion, ceil(k_rrt_ * log(n+1))) if useKNearest_ (default), otherwise nearestR with \(r = \min(\text{range}, r_{rrt}(\log(n+1)/(n+1))^{1/d})\). calculateRewiringLowerBounds() computes \(k_{rrt}, r_{rrt}\) from the space dimension and getMeasure() and multiplies by rewire_factor (1.1). The planner chooses the parent minimizing cost-to-come, then rewires neighbors through the new node. Options include delayCC (sort before collision checking), tree pruning, informed sampling, and "new state rejection".

3.4 Kinodynamic planning (ompl/control)

When you can't steer between two states exactly (cars, drones, anything with dynamics), planners can only forward-propagate controls:

3.5 Multilevel (ompl/multilevel)

Plans on a sequence of increasingly detailed spaces \(X_1 \subset \dots \subset X_K\) (e.g. a sphere for the base, then full SE(3), then the arm). Solutions at lower levels bias sampling at higher levels via fiber bundles (QRRT, QRRT*, QMP). It's a research module.

4. Supporting machinery

4.1 Nearest neighbors: the real hot path

NearestNeighbors<T> is an abstract interface: add, remove, nearest, nearestK, nearestR, setDistanceFunction. Implementations:

In a typical RRT run on a cheap collision checker, NN queries are the largest time cost. In an expensive one (meshes), collision checks are. Profile before optimizing a port.

4.2 Path post-processing (geometric/PathSimplifier)

simplifyMax/simplify(ptc) run reduceVertices (random shortcutting: try to connect non-adjacent vertices directly), shortcutPath (shortcuts between random points on edges), collapseCloseVertices, perturbPath (cost-aware), and finally smoothBSpline. PathGeometric::interpolate(n) densifies the result for execution. PathHybridization merges multiple solutions (used by AnytimePathShortening).

4.3 Randomness

ompl::RNG wraps std::mt19937 with std::uniform_real_distribution and std::normal_distribution. Seeds: RNG::setSeed(s) sets the global seed generator (call it before any RNG is constructed), and setLocalSeed sets a per-instance seed. Special samplers: quaternion() (Shoemake's uniform method), uniformInBall, halfNormal, and uniformProlateHyperspheroid (for informed sampling).

Determinism across platforms

mt19937 produces the same raw stream everywhere, but std::uniform_real_distribution and std::normal_distribution are implementation-defined. libstdc++, libc++, and MSVC produce different values from the same seed. A port in another language or stdlib won't reproduce OMPL's exact trees even with a fixed seed. Compare statistics (success rate, path cost distributions), not exact paths, or replace the distributions with hand-written ones in both implementations.

4.4 Tools

4.5 Integration points: who calls OMPL

5. Build, bindings, tests

  1. demos/RigidBodyPlanning.cpp, demos/StateSampling.cpp, demos/OptimalPlanning.cpp
  2. base/State.h, base/StateSpace.h, base/spaces/RealVectorStateSpace.cpp, SO3StateSpace.cpp, SE3StateSpace.cpp (compound)
  3. base/SpaceInformation.{h,cpp}, DiscreteMotionValidator.cpp, StateValidityChecker.h
  4. base/ProblemDefinition.h, Goal*.h, Planner.{h,cpp}, PlannerTerminationCondition.cpp
  5. geometric/planners/rrt/RRT.cpp, then RRTConnect.cpp, then RRTstar.cpp
  6. geometric/planners/prm/PRM.cpp (Boost.Graph usage, multi-query)
  7. datastructures/NearestNeighborsGNAT.h, NearestNeighborsLinear.h
  8. geometric/PathSimplifier.cpp, geometric/SimpleSetup.cpp, tools/config/SelfConfig.cpp
  9. geometric/planners/kpiece/KPIECE1.cpp + Discretization.h (projection/grid family)
  10. control/SpaceInformation.cpp, ODESolver.h, control/planners/sst/SST.cpp
  11. geometric/planners/informedtrees/BITstar.cpp + bitstar/* (largest, most intricate planner)
  12. tools/benchmark/Benchmark.cpp, scripts/ompl_benchmark_statistics.py