Subgroup Planning¶
Each robot exposes 5 joints — a 3-DOF mobile base (x, y, theta) and
the camera pan and tilt. A subgroup is an ordered list of indices into
that body: the planner reasons in the reduced space, and the collision
pipeline injects the frozen joints and checks the whole robot.
Built-in aliases¶
| Alias | DOF | What it controls |
|---|---|---|
locobot_base |
3 | LoCoBot base (x, y, yaw), camera held |
locobot_whole_body |
5 | LoCoBot base + pan + tilt |
locobot |
5 | alias for locobot_whole_body |
ridgeback_base |
3 | Ridgeback base (x, y, yaw), PTU held |
ridgeback_whole_body |
5 | Ridgeback base + PTU pan + tilt |
ridgeback |
5 | alias for ridgeback_whole_body |
They are dicts in nav_planning.locobot.PLANNING_SUBGROUPS and
nav_planning.ridgeback.PLANNING_SUBGROUPS.
Using a built-in alias¶
from nav_planning.planning import create_planner
from nav_planning.types import PlannerConfig
live_config = robot.get_joint_state() # 5-D array from your system
planner = create_planner(
"locobot_base",
config=PlannerConfig(time_limit=1.0),
base_config=live_config, # pan / tilt held here
pointcloud=obstacle_cloud,
)
start = planner.extract_config(live_config) # 5-D → 3-D
result = planner.plan(start, goal)
full_path = planner.embed_path(result.path) # 3-D path → 5-D path
base_config defaults to HOME_JOINTS (all zero). Pass the live joint
reading to pin the camera exactly where it is.
set_joint_limits({joint name: (lower, upper)}) sets the ranges the
planner samples in, for any joint of the full body: for example
{"base_x_joint": (-1.0, 7.0), "base_y_joint": (-3.0, 3.0)} for the
map's extent (the model spans ±10 m). Joints left out keep their
compiled range, None restores every compiled range, and an unknown
name or a bad range raises. With the base active the heading is
periodic, so the base_theta_joint range is not used. bounds gives
the active joints' ranges, joint_limits the full body's.
set_subgroup(name) switches between a robot's subgroups and keeps the
point cloud and the joint ranges.
The low-level API¶
The C++ planner takes the robot at construction and is otherwise the
OmplVampPlanner of Autolife-Planning:
import numpy as np
from nav_planning._ompl_vamp import OmplVampPlanner
planner = OmplVampPlanner("ridgeback") # full body, 5 DOF
lower, upper = planner.default_joint_limits() # full-body order
lower[:2], upper[:2] = (-1.0, -3.0), (7.0, 3.0) # base x / y box
planner.set_joint_limits(lower, upper)
planner.set_subgroup([0, 1, 2], [0.0, 0.0, 0.0, 0.3, -0.2]) # base; PTU held
planner.add_pointcloud(cloud.astype(np.float32), 0.0) # replaces the cloud
result = planner.plan(start, goal, planner_name="rrtc", time_limit=1.0,
simplify=True, interpolate=False)
OmplVampPlanner(robot, active_indices, frozen_config) builds a
subgroup planner directly. The base joints (0, 1, 2) are planned
together, leading the subgroup — any other combination with base
joints raises, as do indices outside 0–4, repeated indices and a frozen
configuration without five entries. Joint limits are kept for the full body, so they survive
set_subgroup; base_theta_joint is periodic and its limits are not
used.
What a camera-only subgroup's edges guarantee¶
Without the base (for example OmplVampPlanner(robot, [3, 4], frozen)),
an edge is checked by VAMP on the straight line between its ends, at
8 n evenly spaced samples, n = ceil(L / 8 * 32) for an edge of length
L (32 samples per unit). The path is collision-free at those samples
only. Planners that keep the valid start of a blocked motion (the
KPIECE family, PDST, STRIDE) add the last valid sample as a waypoint;
that edge was checked at the samples of the longer motion that lie on
it, which a fresh check of the edge does not reproduce. Every waypoint
is a checked state.
On the camera-subgroup test scene (one obstacle point the camera hits at
pan 0, tilt 0; pan from −1.5 to 1.5 rad; simplify=False), the paths
that touch the point between those samples, densified 64 per unit:
| Planner | LoCoBot | Ridgeback |
|---|---|---|
kpiece |
6 / 2000 | 0 / 2000 |
bkpiece |
2 / 2000 | 0 / 2000 |
lbkpiece |
7 / 2000 | 0 / 2000 |
pdst |
9 / 2000 | 0 / 2000 |
stride |
11 / 2000 | 0 / 2000 |
Of the 35 touching edges, 28 are valid at their own samples; the other
7 fail those but are valid at every point of an even spacing L / m
with 32 L <= m <= 64 L, the samples a longer motion they were cut from
can have had. A fresh
check of every edge rejects 1 of 1000 LoCoBot paths for pdst and for
stride (an edge cut from a longer motion). Measured at commit 5fb2f7b
on 2026-10-19, single-threaded on an AMD EPYC 7543, with
pixi run python /nas/pollux/tianrun/nav_geo/camera_subgroup_rate.py locobot stride 2000
pixi run python /nas/pollux/tianrun/nav_geo/camera_subgroup_fresh.py locobot stride 1000
for each robot and planner; raw output in
/nas/pollux/tianrun/nav_geo/camera_subgroup_rate.txt and
camera_subgroup_fresh.txt.
When it matters¶
The base subgroup is the right choice for navigation with the camera fixed; the whole body when the camera has to end up looking somewhere, or when its pose matters for clearance — the LoCoBot's camera on its tower, the Ridgeback's PTU on its mast.