|
| 1 | +# ompl_kit — cheat sheet for a coding agent |
| 2 | + |
| 3 | +You are writing Python that drives the **Open Motion Planning Library (OMPL)** — a |
| 4 | +C++ sampling-based motion planner — through `rclcppyy.kits.ompl_kit`. The kit |
| 5 | +**mirrors OMPL's C++ API**: `bringup_ompl()` returns the real `ompl::base` / |
| 6 | +`ompl::geometric` namespaces (the conventional `ob` / `og`) and you use |
| 7 | +`ob.RealVectorStateSpace`, `og.SimpleSetup`, `og.RRTConnect`, |
| 8 | +`setStateValidityChecker`, `setStartAndGoalStates`, `solve`, `getSolutionPath` |
| 9 | +exactly as in the OMPL C++ tutorials. The kit only removes the cppyy friction |
| 10 | +(bringup, the validity `std::function` signature, the `as` keyword, RNG seeding, |
| 11 | +path extraction). You do **not** need to know cppyy. |
| 12 | + |
| 13 | +(For *why* this exists and the C++-vs-Python comparison, see [WHY.md](WHY.md); for |
| 14 | +the cross-inheritance mechanics and benchmarks, see [REPORT.md](REPORT.md).) |
| 15 | + |
| 16 | +**Requires** the `ompl` pixi env: `pixi run -e ompl python your_script.py`. |
| 17 | + |
| 18 | +**Golden rules** |
| 19 | +- Call `ob, og = ompl_kit.bringup_ompl()` once; it returns the base and geometric |
| 20 | + namespaces. Pass `with_geometric=False` if you only need state spaces (skips the |
| 21 | + ~60 ms planner JIT). Bringup is idempotent (~0.5 s, once). |
| 22 | +- Wrap a raw space/planner/checker in the library `Ptr` to hand it to OMPL: |
| 23 | + `ob.StateSpacePtr(space)`, `ob.PlannerPtr(planner)`, |
| 24 | + `ob.StateValidityCheckerPtr(checker)`. The wrap **transfers ownership** — safe, no |
| 25 | + double-free. |
| 26 | +- The validity checker is the planner's inner-loop callback. Two ways: a Python |
| 27 | + function via `ompl_kit.validity_checker(fn)`, or a Python subclass of |
| 28 | + `ob.StateValidityChecker`. Both are shown below. |
| 29 | +- Inside a validity/cost callback, cppyy **auto-downcasts** the state, so |
| 30 | + `state[0]` / `state[1]` read a `RealVectorStateSpace`'s coordinates directly. |
| 31 | +- **Pin** any Python subclass instance you hand to C++ (`owner=` / |
| 32 | + `cppyy_kit.keep_alive`), or it is collected and the next call raises "callable was |
| 33 | + deleted". |
| 34 | +- Seed with `ompl_kit.set_seed(n)` **before** solving; the global RNG can't be |
| 35 | + re-seeded mid-process (use a fresh process per seed). |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## Pattern 1 — a 2D plan, validity checker as a Python function (the minimal path) |
| 40 | +*Use for:* planning where the validity/obstacle logic is a plain function. |
| 41 | + |
| 42 | +```python |
| 43 | +from rclcppyy.kits import ompl_kit |
| 44 | +ob, og = ompl_kit.bringup_ompl() |
| 45 | + |
| 46 | +def is_state_valid(state): # planner's inner-loop callback |
| 47 | + return (state[0]-0.5)**2 + (state[1]-0.5)**2 > 0.25**2 # outside a circle |
| 48 | + |
| 49 | +space = ob.RealVectorStateSpace(2) # OMPL's own API, verbatim |
| 50 | +bounds = ob.RealVectorBounds(2) |
| 51 | +bounds.setLow(0.0); bounds.setHigh(1.0) |
| 52 | +space.setBounds(bounds) |
| 53 | + |
| 54 | +ss = og.SimpleSetup(ob.StateSpacePtr(space)) |
| 55 | +ss.setStateValidityChecker(ompl_kit.validity_checker(is_state_valid, owner=ss)) |
| 56 | + |
| 57 | +start = ob.ScopedState[ob.RealVectorStateSpace](ss.getStateSpace()) |
| 58 | +start[0], start[1] = 0.1, 0.1 |
| 59 | +goal = ob.ScopedState[ob.RealVectorStateSpace](ss.getStateSpace()) |
| 60 | +goal[0], goal[1] = 0.9, 0.9 |
| 61 | +ss.setStartAndGoalStates(start, goal) |
| 62 | +ss.setPlanner(ob.PlannerPtr(og.RRTConnect(ss.getSpaceInformation()))) |
| 63 | + |
| 64 | +if ss.solve(1.0): |
| 65 | + ss.simplifySolution() |
| 66 | + print(ompl_kit.path_to_list(ss.getSolutionPath(), dim=2)) # [(x,y), ...] |
| 67 | +``` |
| 68 | +`validity_checker(fn, owner=ss)` wraps `fn` as OMPL's |
| 69 | +`std::function<bool(const State*)>` and pins it on `ss`. See |
| 70 | +`scripts/ompl_kit_demos/d01_first_plan.py`. |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +## Pattern 2 — validity checker as a Python subclass (cross-inheritance) |
| 75 | +*Use for:* an OMPL-idiomatic checker, or one that holds state / queries a map. |
| 76 | + |
| 77 | +```python |
| 78 | +from rclcppyy.kits import ompl_kit, cppyy_kit |
| 79 | +ob, og = ompl_kit.bringup_ompl() |
| 80 | + |
| 81 | +class CircleChecker(ob.StateValidityChecker): |
| 82 | + def __init__(self, si): |
| 83 | + super().__init__(si) # REQUIRED: chain the C++ base ctor |
| 84 | + self.calls = 0 |
| 85 | + def isValid(self, state): # override the C++ virtual by name |
| 86 | + self.calls += 1 |
| 87 | + return (state[0]-0.5)**2 + (state[1]-0.5)**2 > 0.25**2 |
| 88 | + |
| 89 | +# ... build space + ss as in Pattern 1 ... |
| 90 | +checker = CircleChecker(ss.getSpaceInformation()) |
| 91 | +cppyy_kit.keep_alive(ss, checker) # PIN it (footgun otherwise) |
| 92 | +ss.setStateValidityChecker(ob.StateValidityCheckerPtr(checker)) |
| 93 | +``` |
| 94 | +The C++ planner calls the Python `isValid` through the vtable. Works because |
| 95 | +`isValid` is a *plain* virtual. Cost: ~345 ns/call (~190x a native checker) — fine |
| 96 | +until validity dominates, then lower it (Pattern 5). |
| 97 | + |
| 98 | +--- |
| 99 | + |
| 100 | +## Pattern 3 — optimal planning with a Python OptimizationObjective (RRT\*) |
| 101 | +*Use for:* minimizing a custom cost (path length, clearance, energy) with RRT\* / |
| 102 | +an optimizing planner. Same cross-inheritance shape, two virtuals. |
| 103 | + |
| 104 | +```python |
| 105 | +class PathLength(ob.OptimizationObjective): |
| 106 | + def __init__(self, si): |
| 107 | + super().__init__(si); self.si = si |
| 108 | + def stateCost(self, s): return ob.Cost(1.0) |
| 109 | + def motionCost(self, s1, s2): return ob.Cost(self.si.distance(s1, s2)) |
| 110 | + |
| 111 | +obj = PathLength(si) |
| 112 | +cppyy_kit.keep_alive(ss, obj) |
| 113 | +ss.setOptimizationObjective(ob.OptimizationObjectivePtr(obj)) |
| 114 | +ss.setPlanner(ob.PlannerPtr(og.RRTstar(si))) |
| 115 | +ss.solve(1.0) # motionCost called ~1M times/s |
| 116 | +``` |
| 117 | + |
| 118 | +--- |
| 119 | + |
| 120 | +## Pattern 4 — a compound state space (SE2/SE3) (explicit downcast) |
| 121 | +*Use for:* poses with orientation. The auto-downcast covers `RealVectorStateSpace`; |
| 122 | +for a compound space, cast explicitly with `as_state` (Python can't write `.as`). |
| 123 | + |
| 124 | +```python |
| 125 | +def is_valid(state): |
| 126 | + se2 = ompl_kit.as_state(state, ob.SE2StateSpace.StateType) |
| 127 | + return in_free_space(se2.getX(), se2.getY(), se2.getYaw()) |
| 128 | +``` |
| 129 | +`bringup_ompl` pre-includes SE2/SE3 headers, so `ob.SE2StateSpace` is ready. |
| 130 | + |
| 131 | +--- |
| 132 | + |
| 133 | +## Pattern 5 — lower the hot checker to native C++ (when validity dominates) |
| 134 | +*Use for:* a planner that calls the checker millions of times (RRT\*, long solves). |
| 135 | +Prototype in Python (Patterns 1–3), then lower that one function to C++ — same OMPL |
| 136 | +calls, ~150x faster inner loop. |
| 137 | + |
| 138 | +```python |
| 139 | +import cppyy |
| 140 | +from rclcppyy.kits import ompl_kit, cppyy_kit |
| 141 | +ob, og = ompl_kit.bringup_ompl() |
| 142 | + |
| 143 | +cppyy.cppdef(r""" |
| 144 | +class CppCircleChecker : public ompl::base::StateValidityChecker { |
| 145 | +public: |
| 146 | + CppCircleChecker(const ompl::base::SpaceInformationPtr& si) |
| 147 | + : ompl::base::StateValidityChecker(si) {} |
| 148 | + bool isValid(const ompl::base::State* s) const override { |
| 149 | + const auto* rv = s->as<ompl::base::RealVectorStateSpace::StateType>(); |
| 150 | + double x = (*rv)[0], y = (*rv)[1]; |
| 151 | + return (x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) > 0.25*0.25; // never leaves C++ |
| 152 | + } |
| 153 | +}; |
| 154 | +""") |
| 155 | +checker = cppyy.gbl.CppCircleChecker(ss.getSpaceInformation()) |
| 156 | +cppyy_kit.keep_alive(ss, checker) |
| 157 | +ss.setStateValidityChecker(ob.StateValidityCheckerPtr(checker)) |
| 158 | +``` |
| 159 | +`bench_ompl_validity.py` (`pixi run -e ompl bench-ompl`) measures Python vs this. |
| 160 | + |
| 161 | +--- |
| 162 | + |
| 163 | +## Pattern 6 — publish a plan as a ROS 2 nav_msgs/Path |
| 164 | +*Use for:* feeding a plan to RViz / a navigation stack. Build the **C++** message. |
| 165 | + |
| 166 | +```python |
| 167 | +import os; os.environ.setdefault("ROS_DOMAIN_ID", "45") |
| 168 | +import cppyy |
| 169 | +from rclcppyy.bringup_rclcpp import bringup_rclcpp, add_ros2_include_paths |
| 170 | +from rclcppyy.kits import ompl_kit |
| 171 | + |
| 172 | +waypoints = [...] # from ompl_kit.path_to_list(...) |
| 173 | +rclcpp = bringup_rclcpp() |
| 174 | +if not rclcpp.ok(): rclcpp.init() |
| 175 | +from nav_msgs.msg import Path # Python type registers the topic |
| 176 | + |
| 177 | +add_ros2_include_paths() |
| 178 | +cppyy.include("nav_msgs/msg/path.hpp"); cppyy.include("geometry_msgs/msg/pose_stamped.hpp") |
| 179 | +msg = cppyy.gbl.nav_msgs.msg.Path() # the C++ message; poses is a vector |
| 180 | +msg.header.frame_id = "map" |
| 181 | +for x, y in waypoints: |
| 182 | + pose = cppyy.gbl.geometry_msgs.msg.PoseStamped() |
| 183 | + pose.pose.position.x, pose.pose.position.y = float(x), float(y) |
| 184 | + pose.pose.orientation.w = 1.0 |
| 185 | + msg.poses.push_back(pose) |
| 186 | + |
| 187 | +node = rclcpp.Node("planner") |
| 188 | +node.create_publisher(Path, "plan", 10).publish(msg) |
| 189 | +``` |
| 190 | +See `scripts/ompl_kit_demos/d02_publish_path.py`. |
| 191 | + |
| 192 | +--- |
| 193 | + |
| 194 | +## Gotchas (short version) |
| 195 | +- **Pin** any Python subclass instance (checker/objective) you hand to C++, or the |
| 196 | + next dispatch raises `TypeError: callable was deleted`. `validity_checker(owner=)` |
| 197 | + does it for you; a raw subclass is yours to `keep_alive`. |
| 198 | +- **`validity_checker` needs no hint** — the kit fixes the `bool(const State*)` |
| 199 | + signature. Don't rely on `cppyy_kit.callback`'s inference here: it would infer a |
| 200 | + `State&` reference, which won't bind to `setStateValidityChecker`. |
| 201 | +- Inside a callback, `state[0]`/`state[1]` work for `RealVectorStateSpace` |
| 202 | + (auto-downcast); a compound space needs `ompl_kit.as_state(state, ...StateType)`. |
| 203 | +- **Seed before solving** with `ompl_kit.set_seed(n)`; a seed set after the first |
| 204 | + sample is ignored (OMPL warns). Re-seeding in one process is unreliable — use a |
| 205 | + fresh process per reproducible run. |
| 206 | +- Wrap raws in the library `Ptr` (`ob.StateSpacePtr`, `ob.PlannerPtr`) to pass them |
| 207 | + to OMPL; the wrap transfers ownership (no double-free). |
| 208 | +- `path_to_list(path, dim)` needs the space dimension (the path doesn't carry it), |
| 209 | + and reads real-vector coordinates; read compound waypoints via `as_state`. |
| 210 | +- Only RRTConnect/RRTstar are pre-included; any other planner is one |
| 211 | + `cppyy.include("ompl/geometric/planners/.../X.h")` away, then `og.X`. |
0 commit comments