feat(pybind): complete missing Python bindings (base, geometric, control, tools, multilevel)#1442
feat(pybind): complete missing Python bindings (base, geometric, control, tools, multilevel)#1442joao-pm-santos96 wants to merge 17 commits into
Conversation
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
left a comment
There was a problem hiding this comment.
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.
| # POSSIBILITY OF SUCH DAMAGE. | ||
| ###################################################################### | ||
|
|
||
| # Author: Mark Moll |
There was a problem hiding this comment.
Is Mark the author of this script?
There was a problem hiding this comment.
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 |
PR feedback asked to keep planning docs and helper scripts out of the repository and stop running the binding gap audit in CI.
|
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().
OutputHandler Python trampolineWhat changed
Why it's needed This matches the trampoline pattern already used elsewhere in the bindings (e.g. How to verify pytest tests/pytests/test_console.py::test_python_output_handler_trampoline -v |
|
Hello @WeihangGuo. 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.
Update: nanobind STL caster includes (
|
| 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_construction—GoalState(si), set state, read backtest_goal_states_construction—GoalStates(si),addState,hasStates/getStateCount
Not in this commit
- No
check_nanobind_includes.pyaudit script (removed per maintainer preference; same approach as priorchore(ci): remove local-only binding docs and audit scripton 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.
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.multilevelsubmodule, 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/**/*.cppfiles auto-discovered by CMake).Branch is rebased on current
ompl/main(merge-basecf7095db, 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:
pip install ./py-bindings, import smoke tests, andpytest tests/pytests(excluding deprecated) locally on WSL.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:basegeometriccontroltoolsutilmultilevelPython 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— diffssrc/ompl/**/*.hvspy-bindings/**/*.cpp(skips*Impl.h, internal headers, documented exclusions)..github/workflows/build.ymlrunspython 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+utilHybridStateSpace,HybridTimeStateSpace, special topologies (sphere, torus, Möbius, Klein bottle).ProlateHyperspheroid,StateStorage,GenericParam/ParamSet,PlannerDataGraph, etc.GoalLazySamplestrampoline fixes (nb::gil_scoped_acquireon Python callbacks).Phase 2 —
geometricplanners (batch 1)PathHybridization,HillClimbing,GeneticSearch.Phase 3 — Informed-tree planners
ABITstar,AITstar,EITstar,EIRMstar,BLITstar.python.cpp:BITstar → ABITstar → AITstar → EITstar → EIRMstar → BLITstar.Phase 4 —
controlextensionsODESolverhierarchy with GIL-aware ODE callables (ODEBasicSolver,ODEErrorSolver).DiscreteControlSpace,SteeredControlSampler,PlannerDataStorage.HyRRT,HySST, full LTL stack (LTLPlanner,ProductGraph,Automaton, etc.).demos/RigidBodyPlanningWithODESolverAndControls.py.Phase 5 —
tools+ experience helpersSelfConfig,MagicConstants,LightningDB,ThunderDB,SPARSdb,DynamicTimeWarp.LightningRetrieveRepair,ThunderRetrieveRepair(geometric) — without bindingLightning/Thunderorchestrators (they useParallelPlan).Phase 6 —
multilevelsubmodule (new)py-bindings/multilevel/tree +py-bindings/ompl/multilevel.py.Projectionhierarchy,ProjectionFactory, parameters, bundle-space graph infrastructure.QRRT,QRRTStar,QMP,QMPStar.demos/multilevel/MultiLevelPlanningRigidBody2D.py.tests/pytests/test_multilevel_planners.py.Phase 7 — Tests, docs, registration
doc/markdown/pybindingsPlanner.md(multilevel + ODE examples).init*wired inpy-bindings/python.cpp.Follow-up fixes (post-phase, same branch)
fix(base): capture this in cost convergence callbackCostConvergenceTerminationConditionlambda copied*this; convergence state updated a duplicate object.fix(pybind): add reference_internal for owned state pointersProblemDefinition::getStartState,getInputStates,PlannerData, multilevel accessors returned internal pointers without parent lifetime — caused segfaults in Python.fix(pybind): bind PlannerSpecsPlanner::getSpecs()hadreference_internalbutPlannerSpecstype was not registered.fix(pybind): convert PathGeometric checkAndRepair pair returnnanobind/stl/pair.hsostd::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.mdand skipped bylist_binding_gaps.py:ParallelPlan,OptimizePlan,Lightning,Thunder,ExperienceSetupCForestfamily,pRRT,pSBL,AnytimePathShorteningProfiler,PlannerMonitorAlso excluded with rationale:
ODEAdaptiveSolver— fails to compile with Boost 1.83 (make_controlledtemplate issue on tested platforms).ODEBasicSolver/ODEErrorSolverare bound instead.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,SPARStwomay use optional solution-check threads.Binding conventions (for reviewers)
py-bindings/<module>/....init*in<module>/init.h; register inpython.cppafter dependencies (Nanobind inheritance order matters).solve(PlannerTerminationCondition | double)via lambda (seeRRT.cpp/PlannerSkeleton.cpp).NB_TRAMPOLINE+nb::gil_scoped_acquirewhere callbacks cross the C++/Python boundary.nb::rv_policy::reference_internalwhen storage is owned byself.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 -qLocal 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:
doc/markdown/python.md,scripts/list_binding_gaps.py,.github/workflows/build.yml.py-bindings/python.cpp(informed trees, multilevel init order).py-bindings/control/ODESolver.cpp,py-bindings/base/goals/GoalLazySamples.cpp.py-bindings/base/ProblemDefinition.cpp,py-bindings/base/Planner.cpp(PlannerSpecs).py-bindings/multilevel/+tests/pytests/test_multilevel_planners.py.PlannerSkeleton.cpppattern.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)
e36481ccfeat(pybind): add binding gap tooling and gil exclusion docs8116015ffeat(pybind): bind base samplers objectives and util helpers62fe6226feat(pybind): bind geometric planners and path utilities89ad5293feat(pybind): bind informed-tree geometric plannersc2f0d09ffeat(pybind): bind control ode ltl and hybrid planners12f2e9fdfeat(pybind): bind tools experience and xxl helpers926f8780feat(pybind): add multilevel submodule bindings3997e0d7feat(pybind): wire registrations tests and binding docsf17b85effeat(pybind): close plan audit gaps4086ad15fix(base): capture this in cost convergence callback3d3c8de3fix(pybind): add reference_internal for owned state pointers4757467ffix(pybind): bind PlannerSpecs for getSpecs return type96e269d1fix(pybind): convert PathGeometric checkAndRepair pair returnChecklist
pip install ./py-bindings--fail-on-gap)