Why nav2_kit — your own Nav stack from Nav2's cores, in Python via cppyy¶
nav2_kit lets you build a navigation stack by driving Nav2's
algorithm cores directly from Python: the real C++ code owns the costmap grid and
runs the planner (NavFn or Smac 2D) and, since the lifecycle unlock, the real
RegulatedPurePursuit controller — while your Python owns the loop and the world. It
does this against the Nav2 that is already installed, with no lifecycle servers and
no pluginlib — and with no code generation and no build step.
That framing is the whole point. Nav2 is a superb, production navigation system — but its Python surface is deliberately client-side: you configure C++ servers with YAML and send them goals. This doc shows what cppyy gives you when you want the opposite: to compose your own miniature stack from Nav2's building blocks. For the API, see SKILL.md; for the feasibility evidence, the honest coupling boundary, and benchmarks, see REPORT.md.
The thing stock Nav2 makes heavy: a custom planning loop¶
Suppose you just want to try your own idea: "take this occupancy grid, plan across it with NavFn, and drive along the result." In stock Nav2, the supported way to run a custom planner or controller is to make it a C++ pluginlib plugin inside a lifecycle server. Concretely, per the Nav2 "writing a new planner plugin" docs:
- Write a C++ class deriving
nav2_core::GlobalPlanner, implementingconfigure() / activate() / deactivate() / cleanup() / createPlan(), taking aLifecycleNode, atf2_ros::Buffer, and aCostmap2DROS. - Export it as a plugin —
PLUGINLIB_EXPORT_CLASS, aplugins.xml,amentregistration, and aCMakeLists.txtthat builds a shared library. - Wire the lifecycle bringup — a
planner_serverwith a params YAML naming your plugin, then launch the lifecycle manager toconfigure→activateit. - Provide tf + a costmap — the
Costmap2DROSneeds a transform tree (map→odom→base_link) and sensor/static layers to populate the grid.
That is: colcon build, a plugin XML, a YAML config, a launch file, a lifecycle
manager, and a tf tree — before you can call your planner once. It is the right
architecture for a fleet in production; it is a lot of ceremony for "try my idea on a
grid."
Contrast the cppyy "after": pixi install -e nav2, then python your_plan.py,
JIT-including the installed Nav2 headers in ~70 ms at startup.
Side by side: a custom planning loop, stock Nav2 vs nav2_kit¶
Stock Nav2 — the shape of a custom global planner¶
// my_planner.hpp / .cpp — a nav2_core::GlobalPlanner plugin
class MyPlanner : public nav2_core::GlobalPlanner {
void configure(const rclcpp_lifecycle::LifecycleNode::WeakPtr & parent,
std::string name, std::shared_ptr<tf2_ros::Buffer> tf,
std::shared_ptr<nav2_costmap_2d::Costmap2DROS> costmap_ros) override;
void activate() override; void deactivate() override; void cleanup() override;
nav_msgs::msg::Path createPlan(const geometry_msgs::msg::PoseStamped & start,
const geometry_msgs::msg::PoseStamped & goal, ...) override;
};
PLUGINLIB_EXPORT_CLASS(MyPlanner, nav2_core::GlobalPlanner)
<!-- my_planner_plugin.xml -->
<library path="my_planner"><class type="MyPlanner" base_class_type="nav2_core::GlobalPlanner"/></library>
# nav2_params.yaml
planner_server:
ros__parameters:
planner_plugins: ["GridBased"]
GridBased: {plugin: "MyPlanner"}
CMakeLists.txt building the plugin, a launch file bringing up the
planner_server + lifecycle manager, and a tf tree feeding a Costmap2DROS. Then a
colcon build and a lifecycle bringup — before the planner runs once.
nav2_kit — the complete runnable file this repo ships¶
#!/usr/bin/env python
import numpy as np
import nav2_kit
nav2_kit.bringup_nav2()
grid = np.zeros((100, 100), dtype=np.uint8) # your world
grid[:, 50] = nav2_kit.LETHAL_OBSTACLE # a wall
grid[44:56, 50] = nav2_kit.FREE_SPACE # ... with a doorway
costmap = nav2_kit.costmap_from_numpy(grid, resolution=0.05)
path = nav2_kit.plan_navfn(costmap, start=(20, 50), goal=(80, 50)) # NavFn (C++)
print(f"Planned {len(path)} waypoints from {tuple(path[0])} to {tuple(path[-1])}")
Run it: pixi run -e nav2 demo-nav2-plan. It plans across the grid with Nav2's
real NavFn algorithm — the same C++ nav2_navfn_planner::NavFn the
planner_server runs — and prints the path, with no server, no plugin XML, no YAML,
no tf, no build.
What we gain (from the comparison above)¶
- No plugin/lifecycle/YAML/tf ceremony, no build. The stock path needs a C++
plugin,
plugins.xml, params YAML, a launch file + lifecycle manager, and a tf tree; nav2_kit runs the moment you invoke it (~70 ms one-time cppyy bringup). - The world and the loop are just Python. The occupancy grid is a NumPy array;
the follow controller is a Python function you can breakpoint and edit. You iterate
in seconds, not
colcon buildcycles. - It is the same
libnav2_*.so.Costmap2DandNavFnare Nav2's own classes, header-following, so nav2_kit tracks whatever Nav2 is installed — no binding to fall behind. - A prototype-to-native path. As with the other kits, this is the L0 rung: prototype the stack with cppyy JIT today; the same calls lower to a compiled Nav2 plugin when you want to deploy inside the real servers.
What stock Nav2 buys that this does not. A production stack: lifecycle management, dynamic costmap layers from live sensors, tf/localization, recovery behaviors, the full planner/controller/behavior-tree ecosystem, and the operational maturity of the servers. nav2_kit is for composing and prototyping from the cores, not for running a robot in production.
The honest part: what is a clean core, and what is not¶
nav2_kit draws the line where the evidence does, and the lifecycle unlock moved it (full detail in REPORT.md):
- Pure cores (surfaced, no rclcpp at all):
Costmap2D,NavFn. Plain classes —Costmap2D(w, h, res, ox, oy),NavFn(nx, ny)on a rawunsigned char*cost array. No node, no tf, no pluginlib. Directly drivable. - Lifecycle-coupled cores (NOW surfaced): Smac 2D + the real RegulatedPurePursuit
controller. These take a
LifecycleNode(and RPP aCostmap2DROS+tf2_ros::Buffer) — and the key insight is that aLifecycleNodeis a plain class you construct in-process from Python, exactly like therclcpp::Nodewe already build. So nav2_kit builds the node object (and a plugin-freeCostmap2DROS) the ctors ask for — no lifecycle server, no pluginlib, no YAML. The showcase's follow controller can now be Nav2's actual RPP (--controller rpp); the ~30-line Python pure-pursuit is a lightweight choice, no longer a forced limitation. - Still walled: Smac Hybrid-A* (SE(2)). Not a coupling problem — it constructs fine — but its OMPL-backed distance heuristic segfaults non-deterministically under Cling. A documented flaky partial, not shipped.
This honesty is the point: a real, working core road — now including the lifecycle-coupled planners/controllers — with the one remaining wall (a runtime OMPL instability, not "it needs a node") clearly marked.
Two ways to use it¶
Mode A — plan from Python on your own grid¶
Synthesize or load an occupancy grid, build a Costmap2D, plan with NavFn, and use
the path however you like (d01_plan_grid.py). Good for planner experiments,
map-based reasoning, and dataset generation where edit-run speed matters.
Mode B — a whole miniature nav stack, live to rviz2¶
nav2_kit/demos/d02_own_nav_stack.py (the showcase) plans, follows over simulated
diff-drive kinematics, and publishes a live nav_msgs/OccupancyGrid +
nav_msgs/Path + geometry_msgs/TwistStamped via rclcppyy — so an rviz2 (Fixed Frame
map) shows the map, plan, and commanded velocity as the robot drives to the goal.
Pick the pieces: --planner navfn|smac and --controller pursuit|rpp. All four
combinations reach the goal; --planner smac --controller rpp runs Nav2's real Smac 2D
planner and its real RegulatedPurePursuit controller — both C++, driven from one
self-contained Python file.
Advantages of the cppyy approach¶
Grounded in the spike's measured numbers (see REPORT.md):
- No plugin/YAML/lifecycle/build ceremony.
python x.pyis the workflow; bringup is a one-time ~70 ms JIT. - Header-following, tracks the installed Nav2. No hand-maintained binding.
- Bulk data stays fast. A NumPy grid →
Costmap2Dis a singlememcpy(~600–3600× a per-cell Python loop); the plan never leaves C++ (NavFn on 1024² in tens of ms vs ~2 s for a pure-Python A* — the orchestration story). - A prototype-to-native lowering path, as with bt_kit / pcl_kit / ompl_kit: the same calls become a compiled Nav2 plugin when you deploy.
Limits¶
nav2_kit is deliberately not a Nav2 stack: no lifecycle servers/manager, no
pluginlib-by-name loading, no tf tree/localization, no dynamic obstacle/inflation
layers, no recovery behaviors. Surfaced: Costmap2D + NavFn (pure cores) and — since
the lifecycle unlock — Smac 2D + the real RPP controller (via an in-process LifecycleNode +
plugin-free Costmap2DROS, still no servers). Smac Hybrid-A* remains out (a flaky
OMPL-under-Cling crash). The complementary direction — loading a Python
planner/controller plugin inside a real Nav2 server — is a separate planned spike.
The full, honest list is in REPORT.md §6.