Subgroup Planning¶
The Fetch robot exposes 11 joints when you load the full URDF — a
3-DOF mobile base (x, y, theta), a 1-DOF torso_lift_joint, and a
7-DOF arm (shoulder_pan → wrist_roll). Planning over all of
them is almost never what you want — for most tasks, only a slice of
the body is under active control while the rest stays put (or
follows its own controller).
Subgroups are user-defined. A subgroup is just an ordered list of indices into the 11-DOF body. The OMPL planner reasons in the reduced joint space; the C++ collision pipeline injects whatever you choose as the frozen stance and still checks against the full body. You can define a subgroup over any subset, in any order — single joint, torso + one wrist, base + arm, anything.
Built-in aliases¶
Convenience aliases ship for the common slicings, discoverable via
available_robots():
| Alias | DOF | What it controls |
|---|---|---|
fetch_base |
3 | mobile base (x, y, yaw) |
fetch_arm |
7 | 7-DOF arm, shoulder_pan → wrist_roll |
fetch_arm_with_torso |
8 | torso_lift + 7-DOF arm |
fetch_base_arm |
10 | mobile base + 7-DOF arm (torso fixed) |
fetch_whole_body |
11 | full body — base + torso + arm |
fetch |
11 | alias for fetch_whole_body |
They exist as starting points. The list is a dict in
fetch_planning.fetch.PLANNING_SUBGROUPS — add your own entry to
extend it, or use the low-level API below for truly one-off
slicings.
Using a built-in alias¶
from fetch_planning.fetch import HOME_JOINTS
from fetch_planning.planning import create_planner
from fetch_planning.types import PlannerConfig
live_config = robot.get_joint_state() # 11-D array from your system
planner = create_planner(
"fetch_arm",
config=PlannerConfig(time_limit=1.0),
base_config=live_config, # frozen joints anchored here
pointcloud=obstacle_cloud,
)
start = planner.extract_config(live_config) # 11-D → 7-D
goal = planner.sample_valid()
result = planner.plan(start, goal)
full_path = planner.embed_path(result.path) # 7-D path → 11-D path
base_config defaults to HOME_JOINTS. Pass the live joint reading
instead to pin every non-active joint exactly where the robot
currently is — collision checks see the real stance, not a synthetic
home pose.
Nonholonomic base¶
Fetch has a differential-drive mobile base, so subgroups that
include any of the base joints (fetch_base, fetch_base_arm,
fetch_whole_body / fetch) cannot be planned as a plain
RealVectorStateSpace — the (x, y, theta) triple has to respect
the non-holonomic constraint. MotionPlanner handles this
automatically:
- The underlying state space becomes a
Compound(SE(2) + R^N)where SE(2) isDubinsStateSpace(forward-only) orReedsSheppStateSpace(reverse allowed), selected byBASE_REVERSE_ENABLEinfetch_planning/fetch.py. - Every geometric planner (
rrtc,rrtstar,prm, …) runs on that space, like on arm-only subgroups. The objective keeps the car-like arc length, so any user cost you add is composed with the non-holonomic shaping rather than replacing it. - Tree extension, rewire, path simplification, and interpolation all stay on the curve, and the motion validator checks each edge along it. The output path is directly executable by a diff-drive controller.
The base's workspace comes from the joint limits, like every other joint's range:
planner.set_joint_limits({
"base_x_joint": (-4.0, 4.0),
"base_y_joint": (-2.0, 4.0),
})
planner.joint_limits["base_x_joint"] # (-4.0, 4.0)
planner.bounds # (lower, upper) of the active DOF
Joints left out keep the robot model's range (the base spans
±10 m), None restores every compiled range, and an unknown joint
name or a range that is not finite with lower < upper raises
ValueError. The ranges cover the full body and survive
set_subgroup; PlannerConfig(joint_limits=...) sets them at
construction. base_theta_joint's range is not used while the base
is active: the Dubins / Reeds-Shepp heading is periodic.
Constrained planning (CasADi manifold constraints — see
Manifold planning) is not supported for
base-including subgroups. Use fetch_arm or fetch_arm_with_torso
when you need the end-effector pinned to a manifold.
Defining a custom subgroup¶
Any ordered list of joint indices is a valid subgroup. Drop straight to the C++ binding when the alias list doesn't match your problem:
from fetch_planning._ompl_vamp import OmplVampPlanner
from fetch_planning.fetch import (
BASE_REVERSE_ENABLE, BASE_TURNING_RADIUS, HOME_JOINTS, fetch_robot_config,
)
# Name the joints you want to plan over; the rest stay frozen.
active_names = [
"torso_lift_joint",
"shoulder_pan_joint",
"shoulder_lift_joint",
"upperarm_roll_joint",
"elbow_flex_joint",
]
full_names = fetch_robot_config.joint_names
active_indices = [full_names.index(j) for j in active_names]
# base_dim = number of base joints (0, 1, 2, or 3) in the subgroup —
# here 0, so a plain RealVectorStateSpace.
planner = OmplVampPlanner(
active_indices, HOME_JOINTS.tolist(), 0,
BASE_TURNING_RADIUS, BASE_REVERSE_ENABLE,
)
planner.add_pointcloud(cloud.tolist(), *planner.min_max_radii(), 0.012)
start = [HOME_JOINTS[i] for i in active_indices]
goal = [...] # your own sampling logic
result = planner.plan(start, goal, planner_name="rrtc", time_limit=1.0)
Same planner, same collision pipeline, same point-cloud broadphase
— just a different slice. You can also switch an existing planner
to a different subgroup on the fly with
set_subgroup(active_indices, frozen_config, base_dim),
which preserves the collision environment.
When it matters¶
Planning in 11 DOF for a task that only needs an arm wastes almost
all of the search time on irrelevant configurations. For a 7-DOF
arm subgroup on the table-obstacle scene, rrtc returns in a
couple of milliseconds. The same plan over the 11-DOF whole body
runs noticeably slower for no practical reason — the base is
already where it should be.
Conversely, whole-body subgroups are the right choice when the task
needs the base to move alongside the arm — e.g. the
examples/planning/nonholonomic.py demo drives the base across the
room while the arm unfolds, carries an apple, and tucks back. Pick
the smallest subgroup that captures the joints your task actually
needs to move.