Phase 1 & 2 - Development Summary
Phase 1: Core Rust Library Design (oxmpl)
The project began with the goal of creating a modern, safe, and extensible motion planning library in Rust. The architecture was built around a set of generic traits to ensure flexibility and composability.
Key Architectural Components
StateTrait: A marker trait representing a single point or configuration in a state space (e.g., a robot's joint angles).StateSpaceTrait: The core trait defining the "universe" for states. It's responsible for the geometry and topology of the space.- Methods:
distance(),interpolate(),enforce_bounds(),satisfies_bounds(), andsample_uniform(). - Properties: It stores fundamental properties of the space, such as its
dimensionandbounds. - Error Handling: The
sample_uniformmethod was designed to return aResultto gracefully handle sampling from unbounded spaces.
- Methods:
StateValidityCheckerTrait: A simple trait with anis_valid(&self, state: &S) -> boolmethod. This allows users to provide problem-specific collision checking logic.GoalTrait Hierarchy: A tiered system of traits was designed to give planners different levels of information about the goal.Goal: The base trait, answers "is the goal satisfied?" withis_satisfied().GoalRegion: A subtrait ofGoal, answers "how far is a state from the goal?" withdistance_goal(). This provides a crucial heuristic for guided planners.GoalSampleableRegion: A subtrait ofGoalRegion, answers "give me a random state in the goal" withsample_goal(). This is essential for goal-biasing and bidirectional planners.
PlannerTrait: A generic trait that defines the contract for all planning algorithms.setup(&mut self, problem_def, validity_checker): Configures the planner for a specific problem.solve(&self, timeout): Runs the planning algorithm.
Concrete Implementations
- State Spaces: Created
RealVectorStateSpace(for N-dimensional Euclidean spaces) andSO2StateSpace(for 2D rotations), with specialized logic for distance (Euclidean vs. shortest angle) and interpolation. - Planner: Implemented a generic
RRT(Rapidly-exploring Random Tree) planner that works with any types satisfying the trait bounds.
Project Organization and Testing
- The project was organized into a clear module structure (
base,geometric,planners). - A testing strategy using both unit tests (for individual components) and integration tests (to validate the public API) was established. Unit tests were organized into separate files for clarity.
Phase 2: Python Bindings (oxmpl-py)
The goal was to provide a high-performance Rust backend with a flexible, Pythonic user interface.
Core Technology Choices
- PyO3: Chosen as the modern, idiomatic framework for creating safe and efficient Rust-to-Python bindings.
- Maturin: Used as the build tool to compile the Rust crate into a Python extension module and manage the development environment.
Binding Architecture and Key Patterns
- Namespacing: To mirror OMPL's clean API, the bindings were organized into Python submodules (
oxmpl_py.base,oxmpl_py.geometric.planners). This was achieved by creating corresponding Rust modules and usingm.add_submodule()in the main#[pymodule]function. - The Callback Bridge (Key Feature): To allow users to define problem logic in Python, a robust callback bridge was created:
- Rust Wrapper Structs: Created
PyStateValidityCheckerandPyGoalstructs in Rust. - Holding Python Objects: These structs hold a
PyObject, which is a safe, reference-counted handle to a user's Python function or class instance. - Implementing Rust Traits: The core Rust traits (
StateValidityChecker,Goal,GoalRegion, etc.) were implemented for these wrapper structs. - Calling Python from Rust: Inside the trait methods, the implementation acquires Python's Global Interpreter Lock (GIL), calls the method on the stored Python object (e.g.,
self.instance.call_method1(...)), and handles the conversion of arguments, return values, and exceptions. This makes the planner completely unaware that it's executing Python code.
- Rust Wrapper Structs: Created
- Code Generation with Macros: To solve the problem of repetitive boilerplate code when wrapping multiple planners for multiple state spaces, a
define_planner_wrapper!macro was created.- This
macro_rules!macro acts as a template that generates the full#[pyclass]and#[pymethods]boilerplate for any given planner. - This makes the binding code highly scalable and maintainable. Adding a new planner binding becomes a single macro invocation.
- This
Final Result
The final product is a two-crate workspace:
oxmpl: A pure Rust library with a generic, powerful, and type-safe API for motion planning.oxmpl-py: A lightweight bindings crate that exposesoxmpl's functionality to Python. It provides a clean, namespaced, and Pythonic API where users can define their own problem logic (like obstacle checkers and goal definitions) in pure Python functions and classes, which are then seamlessly called by the high-performance Rust planner backend.
This architecture successfully combines the performance and safety of Rust with the flexibility and ease of use of Python.