Skip to content

feat(pybind): complete missing Python bindings (base, geometric, control, tools, multilevel)#1442

Open
joao-pm-santos96 wants to merge 17 commits into
ompl:mainfrom
joao-pm-santos96:feat/python-bindings-gap
Open

feat(pybind): complete missing Python bindings (base, geometric, control, tools, multilevel)#1442
joao-pm-santos96 wants to merge 17 commits into
ompl:mainfrom
joao-pm-santos96:feat/python-bindings-gap

Conversation

@joao-pm-santos96

Copy link
Copy Markdown
Contributor

Summary

This PR closes the largest remaining gap between the OMPL C++ public API and the Nanobind-based Python bindings introduced in OMPL 2.x. It adds ~110 new binding translation units, introduces the ompl.multilevel submodule, documents GIL-unsafe APIs we intentionally skip, and adds CI drift checks so future header additions are easier to audit.

Scale: 175 files changed, ~6.7k lines added (mostly new py-bindings/**/*.cpp files auto-discovered by CMake).

Branch is rebased on current ompl/main (merge-base cf7095db, 0 commits behind upstream at open time).


Tooling disclosure (important)

This work was developed with assistance from Cursor (AI-assisted IDE). I (@joao-pm-santos96) reviewed, built, tested, and committed every change on this branch. I am not submitting unreviewed generated code:

  • I wrote/maintained the binding gap plan and acceptance criteria before implementation.
  • I ran pip install ./py-bindings, import smoke tests, and pytest tests/pytests (excluding deprecated) locally on WSL.
  • I fixed issues found during review (lifetime policies, a C++ callback capture bug, missing type casters, etc.) in follow-up commits.
  • I explicitly chose not to bind several threading-heavy C++ APIs (documented below) rather than hiding risk behind Python wrappers.

If reviewers want extra confidence, the commit history is intentionally phase-scoped (13 commits) so each area can be reviewed incrementally.


Motivation

OMPL 2.x ships Nanobind bindings under py-bindings/, but large parts of the library were still unreachable from Python:

Module Before (approx.) After this PR
base Core only; many samplers/objectives/spaces missing Public bindable API covered
geometric ~15 planners Classic + RRT/PRM variants, informed trees, path utilities, XXL, experience repair helpers
control Core planners ODE solvers, Hy*, full LTL stack
tools Benchmark only SelfConfig, experience DBs, machine specs
util Console/PPM/RNG + ProlateHyperspheroid, Time, Exception, GeometricEquations
multilevel 0 bindings New submodule: projections, bundle-space infra, QRRT/QMP planners

Python users currently cannot run multilevel demos, many optimizing planners, informed-tree planners, or ODE-based control examples without dropping to C++. This PR targets that gap while staying conservative about threading / GIL safety.


What changed (by commit / phase)

Phase 0 — Tooling & policy

  • scripts/list_binding_gaps.py — diffs src/ompl/**/*.h vs py-bindings/**/*.cpp (skips *Impl.h, internal headers, documented exclusions).
  • CI: .github/workflows/build.yml runs python scripts/list_binding_gaps.py --fail-on-gap.
  • py-bindings/templates/PlannerSkeleton.cpp — boilerplate for new planner bindings.
  • doc/markdown/python.md — new "Excluded from Python bindings" section.

Phase 1 — base + util

  • Optimization objectives, termination conditions, informed/deterministic samplers, valid-state samplers.
  • HybridStateSpace, HybridTimeStateSpace, special topologies (sphere, torus, Möbius, Klein bottle).
  • ProlateHyperspheroid, StateStorage, GenericParam/ParamSet, PlannerDataGraph, etc.
  • GoalLazySamples trampoline fixes (nb::gil_scoped_acquire on Python callbacks).

Phase 2 — geometric planners (batch 1)

  • EST/BiEST/ProjEST, SST, SBL, PDST, STRIDE; many RRT variants; LazyPRM/SPARS family (with threading doc warnings).
  • Path utilities: PathHybridization, HillClimbing, GeneticSearch.

Phase 3 — Informed-tree planners

  • ABITstar, AITstar, EITstar, EIRMstar, BLITstar.
  • Registered in inheritance order in python.cpp: BITstar → ABITstar → AITstar → EITstar → EIRMstar → BLITstar.

Phase 4 — control extensions

  • ODESolver hierarchy with GIL-aware ODE callables (ODEBasicSolver, ODEErrorSolver).
  • DiscreteControlSpace, SteeredControlSampler, PlannerDataStorage.
  • HyRRT, HySST, full LTL stack (LTLPlanner, ProductGraph, Automaton, etc.).
  • Active demo: demos/RigidBodyPlanningWithODESolverAndControls.py.

Phase 5 — tools + experience helpers

  • SelfConfig, MagicConstants, LightningDB, ThunderDB, SPARSdb, DynamicTimeWarp.
  • LightningRetrieveRepair, ThunderRetrieveRepair (geometric) — without binding Lightning/Thunder orchestrators (they use ParallelPlan).
  • XXL decomposition types (abstract decompositions are type-only where constructors are unavailable).

Phase 6 — multilevel submodule (new)

  • py-bindings/multilevel/ tree + py-bindings/ompl/multilevel.py.
  • Projection hierarchy, ProjectionFactory, parameters, bundle-space graph infrastructure.
  • Concrete planners: QRRT, QRRTStar, QMP, QMPStar.
  • Demo: demos/multilevel/MultiLevelPlanningRigidBody2D.py.
  • Tests: tests/pytests/test_multilevel_planners.py.

Phase 7 — Tests, docs, registration

  • Expanded pytest smoke coverage; updated doc/markdown/pybindingsPlanner.md (multilevel + ODE examples).
  • All init* wired in py-bindings/python.cpp.

Follow-up fixes (post-phase, same branch)

Commit Why
fix(base): capture this in cost convergence callback CostConvergenceTerminationCondition lambda copied *this; convergence state updated a duplicate object.
fix(pybind): add reference_internal for owned state pointers ProblemDefinition::getStartState, getInputStates, PlannerData, multilevel accessors returned internal pointers without parent lifetime — caused segfaults in Python.
fix(pybind): bind PlannerSpecs Planner::getSpecs() had reference_internal but PlannerSpecs type was not registered.
fix(pybind): convert PathGeometric checkAndRepair pair return Added nanobind/stl/pair.h so std::pair<bool,bool> converts to a Python tuple.

Intentionally excluded (documented, not oversights)

These C++ APIs spawn threads or call Python without GIL management. They are listed in doc/markdown/python.md and skipped by list_binding_gaps.py:

  • ParallelPlan, OptimizePlan, Lightning, Thunder, ExperienceSetup
  • CForest family, pRRT, pSBL, AnytimePathShortening
  • Profiler, PlannerMonitor

Also excluded with rationale:

  • ODEAdaptiveSolver — fails to compile with Boost 1.83 (make_controlled template issue on tested platforms). ODEBasicSolver / ODEErrorSolver are bound instead.
  • Internal planner headers (bitstar/Vertex.h, informed-tree queue/graph internals, etc.) — not public Python API.
  • ompl::vamp, datastructures/* — out of scope unless a public API requires direct access.

Bindable with caution (bound + docstring warnings): LazyPRM, LazyPRMstar, SPARS, SPARStwo may use optional solution-check threads.


Binding conventions (for reviewers)

  • Mirror C++ header paths under py-bindings/<module>/....
  • Declare init* in <module>/init.h; register in python.cpp after dependencies (Nanobind inheritance order matters).
  • Planners: dual solve(PlannerTerminationCondition | double) via lambda (see RRT.cpp / PlannerSkeleton.cpp).
  • Python-overridable virtuals: NB_TRAMPOLINE + nb::gil_scoped_acquire where callbacks cross the C++/Python boundary.
  • Internal pointers/references: nb::rv_policy::reference_internal when storage is owned by self.

Test plan

git submodule update --init --recursive
pip install ./py-bindings
python -c "from ompl import base, geometric, control, util, tools, multilevel; print('ok')"
python scripts/list_binding_gaps.py --fail-on-gap
pytest tests/pytests --ignore=tests/pytests/deprecated -q

Local results (WSL, 2026-06): 93 non-deprecated pytest tests pass; gap audit reports 0 unintentional missing bindings.

tests/pytests/deprecated/ still fails on this branch (missing resource files + legacy API expectations) — pre-existing, not introduced here.


Review suggestions

Because this is large, suggested review order:

  1. Policy / safety: doc/markdown/python.md, scripts/list_binding_gaps.py, .github/workflows/build.yml.
  2. Registration order: py-bindings/python.cpp (informed trees, multilevel init order).
  3. GIL / callbacks: py-bindings/control/ODESolver.cpp, py-bindings/base/goals/GoalLazySamples.cpp.
  4. Lifetime fixes: py-bindings/base/ProblemDefinition.cpp, py-bindings/base/Planner.cpp (PlannerSpecs).
  5. Multilevel: py-bindings/multilevel/ + tests/pytests/test_multilevel_planners.py.
  6. Spot-check one planner binding per family against PlannerSkeleton.cpp pattern.

Detailed inventory and phase checklist: docs/plans/ompl-python-bindings-gap.plan.md (authoring doc on the fork; happy to move/summarize upstream if maintainers prefer).


Commit list (13)

  1. e36481cc feat(pybind): add binding gap tooling and gil exclusion docs
  2. 8116015f feat(pybind): bind base samplers objectives and util helpers
  3. 62fe6226 feat(pybind): bind geometric planners and path utilities
  4. 89ad5293 feat(pybind): bind informed-tree geometric planners
  5. c2f0d09f feat(pybind): bind control ode ltl and hybrid planners
  6. 12f2e9fd feat(pybind): bind tools experience and xxl helpers
  7. 926f8780 feat(pybind): add multilevel submodule bindings
  8. 3997e0d7 feat(pybind): wire registrations tests and binding docs
  9. f17b85ef feat(pybind): close plan audit gaps
  10. 4086ad15 fix(base): capture this in cost convergence callback
  11. 3d3c8de3 fix(pybind): add reference_internal for owned state pointers
  12. 4757467f fix(pybind): bind PlannerSpecs for getSpecs return type
  13. 96e269d1 fix(pybind): convert PathGeometric checkAndRepair pair return

Checklist

  • Builds with pip install ./py-bindings
  • Gap audit passes in CI (--fail-on-gap)
  • Non-deprecated pytest suite passes
  • GIL-unsafe APIs documented and excluded
  • AI assistance disclosed; human review performed
  • Maintainer feedback welcome on commit squashing vs. keeping phase commits

Add list_binding_gaps.py, planner binding skeleton, and document GIL-unsafe APIs excluded from Python.
Expose base objectives, samplers, state spaces, validators, and util helpers needed by planner smoke tests.
Add classic geometric planners, path utilities, and smoke-test coverage for EST, LazyRRT, and related RRT variants.
Register informed-tree planners in BITstar inheritance order for nanobind trampolines.
Bind ODE solvers, hybrid planners, and the LTL stack while skipping ODEAdaptiveSolver on Boost 1.83.
Add tools, experience DB, and XXL decomposition bindings used by planner demos and tests.
Scaffold multilevel projections, parameters, bundle-space infra, and QRRT/QMP planners.
Register all new bindings in python.cpp and add geo/multilevel pytest coverage plus planner docs.
The gap audit still reported false positives and CI did not enforce
coverage after the eight phase commits. Tighten list_binding_gaps.py,
add a CI audit step, GoalLazySamples and ODE smoke tests, active ODE
and multilevel demos, and sync plan docs with the implemented state.
The intermediate-solution lambda previously copied *this, so
processNewSolution updated a duplicate's convergence state while
terminate() and the live termination condition could diverge. Capture
this instead so planner callbacks and direct processNewSolution calls
share one object.
ProblemDefinition start-state accessors returned internal pointers
without a parent lifetime policy, which can segfault in Python.
Apply the same fix to GoalState, StateStorage, PlannerData, and
multilevel vertex/bundle accessors.
Expose PlannerSpecs fields in Python so planner.getSpecs() returns a
usable object. getSpecs already used reference_internal; the struct
binding was missing compared to upstream.
Include nanobind/stl/pair.h so checkAndRepair returns a Python tuple
instead of failing type conversion on std::pair<bool, bool>.
Run clang-format and ruff-format so the Format workflow passes on
upstream PR ompl#1442. No logic changes.

@WeihangGuo WeihangGuo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for creating additional Python bindings. However, this PR includes many files and scripts that were generated by AI during its thinking process. Please remove them.

Comment thread .github/workflows/build.yml Outdated
# POSSIBILITY OF SUCH DAMAGE.
######################################################################

# Author: Mark Moll

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is Mark the author of this script?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was on version 1.7.0. Is it better to modify it, remove it, or leave it as is?

# POSSIBILITY OF SUCH DAMAGE.
######################################################################

# Author: Mark Moll

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

Comment thread docs/plans/ompl-python-bindings-gap.plan.md Outdated
Comment thread docs/plans/python-bindings-execution.plan.md Outdated
Comment thread docs/plans/README.md Outdated
Comment thread scripts/commit_binding_phases.sh Outdated
Comment thread scripts/list_binding_gaps.py Outdated
PR feedback asked to keep planning docs and helper scripts out of
the repository and stop running the binding gap audit in CI.
@joao-pm-santos96

Copy link
Copy Markdown
Contributor Author

Hello @WeihangGuo. Thanks for your feedback. I've resolved most of the comments. Just let me know what is the correct way to fix the demos/ scripts and I'll happily fix it!

OMPL 2.0 bound OutputHandler without a nanobind trampoline, so C++
virtual calls to log() never reached Python overrides and fell through
to OutputHandlerSTD. Add PyOutputHandler and PyOutputHandlerSTD
trampolines plus a regression test that verifies OMPL_INFORM reaches
a Python handler registered via useOutputHandler().
@joao-pm-santos96

Copy link
Copy Markdown
Contributor Author

OutputHandler Python trampoline

What changed

  • Added PyOutputHandler and PyOutputHandlerSTD nanobind trampolines in py-bindings/util/Console.cpp
  • Updated the OutputHandler / OutputHandlerSTD bindings to use those trampolines
  • Added test_python_output_handler_trampoline in tests/pytests/test_console.py

Why it's needed
In OMPL 2.0, OutputHandler was bound without NB_TRAMPOLINE. When C++ code calls output_handler_->log() (e.g. via OMPL_INFORM), virtual dispatch never reached Python subclasses registered with useOutputHandler(). Messages were handled by the default OutputHandlerSTD implementation instead (raw Info: ... on stdout).

This matches the trampoline pattern already used elsewhere in the bindings (e.g. Planner, MotionValidator, StateValidityChecker). Both trampolines are required: a trampoline on the base class alone is not enough when users subclass OutputHandlerSTD and override log().

How to verify

pytest tests/pytests/test_console.py::test_python_output_handler_trampoline -v

@joao-pm-santos96

Copy link
Copy Markdown
Contributor Author

Hello @WeihangGuo.
I've added some more missing bindings/behavior. Would love to have your feedback so that I can fix whats needed.

Thank you!

GoalState and GoalStates failed at runtime when constructed with a
Python SpaceInformation because their translation units lacked
nanobind/stl/shared_ptr.h. The same audit found similar omissions for
vector, pair, function, and string casters in other binding files.

Add the required includes, include GraphSampler.h in BundleSpaceGraph
so shared_ptr casters see a complete BundleSpaceGraphSampler type, and
add pytest coverage for GoalState and GoalStates construction.
@joao-pm-santos96

Copy link
Copy Markdown
Contributor Author

Update: nanobind STL caster includes (ccbf5e61)

Problem

ob.GoalStates(si) and ob.GoalState(si) raised a runtime TypeError even when ob.ProblemDefinition(si) worked with the same SpaceInformation instance:

TypeError: __init__(): incompatible function arguments.
Expected: si: std::shared_ptr<ompl::base::SpaceInformation>
Got:      ompl._ompl.base.SpaceInformation

Root cause: nanobind requires #include <nanobind/stl/...> in each binding translation unit that uses the corresponding C++ type in a Python-facing signature. ProblemDefinition.cpp already included shared_ptr.h; GoalState.cpp / GoalStates.cpp did not, so the constructor compiled but argument conversion failed at runtime.

Fix (ccbf5e61)

Repo-wide audit of py-bindings/**/*.cpp for missing STL caster headers. 12 binding files updated:

Area File Added include(s) Why
P0 — reported bug base/goals/GoalState.cpp shared_ptr.h nb::init<const SpaceInformationPtr &>()
base/goals/GoalStates.cpp shared_ptr.h same
P1 — ctor args base/Constraint.cpp vector.h ConstraintIntersection ctor takes std::vector<ConstraintPtr>
base/spaces/VanaStateSpace.cpp pair.h pitch-range ctor std::pair<double,double>
base/spaces/VanaOwenStateSpace.cpp pair.h same
P2 — lambda returns base/spaces/constraint/AtlasChart.cpp pair.h std::make_pair from bound lambdas
base/spaces/constraint/AtlasStateSpace.cpp pair.h geodesic helper tuple
base/spaces/constraint/ProjectedStateSpace.cpp pair.h same pattern
control/SpaceInformation.cpp pair.h propagateWhileValidWithAlloc(nValid, path)
multilevel/BundleSpaceGraph.cpp pair.h, shared_ptr.h getSolution(bool, PathPtr)
P3 — other control/planners/ltl/ProductGraph.cpp function.h buildGraph(..., initialize) Python callback
base/spaces/WrapperStateSpace.cpp string.h getValueAddressAtName(..., std::string&)

Build fix for BundleSpaceGraph.cpp

Adding shared_ptr.h there caused a compile failure: nanobind tried to instantiate std::shared_ptr<BundleSpaceGraphSampler> for getGraphSampler(), but BundleSpaceGraphSampler was only forward-declared in BundleSpaceGraph.h.

Resolution: also include ompl/multilevel/datastructures/graphsampler/GraphSampler.h so the type is complete in that TU (same header used by py-bindings/multilevel/graphsampler/GraphSampler.cpp).

Tests

New tests/pytests/test_goal_construction.py:

  • test_goal_state_constructionGoalState(si), set state, read back
  • test_goal_states_constructionGoalStates(si), addState, hasStates / getStateCount

Not in this commit

  • No check_nanobind_includes.py audit script (removed per maintainer preference; same approach as prior chore(ci): remove local-only binding docs and audit script on this branch).

Suggested re-test

pip install ./py-bindings
pytest tests/pytests/test_goal_construction.py tests/pytests/test_goal_lazy_samples.py -q
python -c "from ompl import base as ob; s=ob.RealVectorStateSpace(2); si=ob.SpaceInformation(s); si.setStateValidityChecker(lambda x: True); si.setup(); ob.GoalStates(si); print('ok')"

Wheel / cibuildwheel builds should now get past BundleSpaceGraph.cpp.o on this branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants