TrapezoidalPlanner#
TrapezoidalPlanner is a lightweight, natively batched joint-trajectory
planner. It time-parameterizes piecewise-linear joint paths with either a
trapezoidal velocity profile or a jerk-limited seven-phase Double-S profile.
The Torch implementation is the reference and CPU fallback; the NVIDIA Warp
backend parallelizes profile construction, phase evaluation, and batched joint
trajectory composition for CUDA workloads.
Use this planner when you need deterministic joint-space timing, explicit velocity and acceleration outputs, and no collision-world planning dependency. For collision-aware Cartesian planning, use cuRobo instead.
Capabilities#
Batched input and output with a leading environment dimension.
Scalar or per-joint velocity, acceleration, and jerk limits.
Synchronized multi-joint motion for every path segment.
Triangular fallback when a short move cannot reach the requested trapezoidal cruise velocity.
Jerk-limited seven-phase Double-S trajectories without display filtering.
Fixed-count and approximately fixed-time sampling.
Optional minimum-duration scaling without changing the path.
Redundant collinear-waypoint compression and optional quintic corner blends.
Explicit
torch,warp, andautobackend selection.Native
positions,velocities,accelerations,dt, and constraint diagnostics inPlanResult.
The planner accepts JOINT_MOVE waypoints. Cartesian straight-line planning in
the accompanying tutorial is a separate pipeline: it applies a scalar time law
in Cartesian space first, solves the complete pose path through continuous
batched IK, and derives joint velocity and acceleration using differential
kinematics. This ordering avoids distorting the requested end-effector path by
applying joint-space timing after independently sampled IK.
MotionGenerator integration#
Configure the planner through the common MotionGenerator facade:
import torch
from embodichain.lab.sim.motion.motion_generator import (
MotionGenCfg,
MotionGenOptions,
MotionGenerator,
)
from embodichain.lab.sim.motion.planners import (
PlanState,
TrapezoidalPlannerCfg,
TrapezoidalPlanOptions,
)
generator = MotionGenerator(
MotionGenCfg(
planner_cfg=TrapezoidalPlannerCfg(robot_uid=robot.uid),
)
)
# PlanState tensors are batched: (B, DOF).
start_qpos = robot.get_qpos(name="arm")
goal_qpos = torch.zeros_like(start_qpos)
result = generator.generate(
[PlanState.from_qpos(goal_qpos)],
MotionGenOptions(
start_qpos=start_qpos,
control_part="arm",
plan_opts=TrapezoidalPlanOptions(
profile="double_s",
constraints={
"velocity": 0.5,
"acceleration": 1.0,
"jerk": 3.0,
},
sample_interval=200,
stop_at_waypoints=False,
backend="auto",
),
),
)
TrapezoidalPlanner owns sparse joint-waypoint timing. MotionGenerator
prepends start_qpos when the first requested waypoint is not already the
start, and does not run generic joint pre-interpolation. It also preserves the
planner’s native sample grid and its analytical velocity and acceleration
outputs. MotionGenOptions.sample_count therefore does not replace an explicit
TrapezoidalPlanOptions.sample_interval.
When options are automatically resolved from a backend-neutral request, the
requested quantity is treated as a lower bound if Cartesian-to-joint conversion
produces more required waypoints; an explicit
TrapezoidalPlanOptions.sample_interval remains authoritative.
Results follow the shared
PlanResult contract.
constraint_report adds derivative peaks, per-joint utilization, limits, and
status. Native trajectories start with a zero arrival interval.
Profiles and constraints#
Set profile="trapezoidal" for acceleration-limited trapezoidal timing. The
planner automatically uses a triangular profile for moves too short to reach a
constant-velocity phase. Acceleration changes discontinuously at phase
boundaries, so this profile does not enforce the configured jerk limit.
Set profile="double_s" for a seven-phase profile constrained by velocity,
acceleration, and jerk. The constraints mapping accepts a positive scalar or
one value per joint for each derivative. All joints within a segment are
synchronized to the same segment duration.
minimum_duration stretches trajectories that would otherwise finish sooner.
It does not shorten slower trajectories or relax derivative limits.
Waypoint behavior#
By default, stop_at_waypoints=True, so every supplied waypoint is a rest
point. Set it to False when intermediate samples describe a continuous path:
duplicate and same-direction collinear points are compressed;
real corners remain in the path;
collinearity_tolerancecontrols the angular comparison;positive
blend_toleranceinserts quintic corner blends and requiresstop_at_waypoints=False.
If the caller passes start_qpos and also includes exactly the same first
waypoint, MotionGenerator avoids inserting a duplicate zero-length segment.
Sampling and backends#
TrajectorySampleMethod.QUANTITY interprets sample_interval as the requested
integer output count. It must provide at least one sample for every retained
waypoint. TrajectorySampleMethod.TIME interprets it as a positive time step in
seconds. In a batch with different durations, shorter rows hold their exact
final position with zero velocity, zero acceleration, and zero arrival
intervals after completion.
Backend selection follows these rules:
backend="torch"uses the reference implementation.backend="warp"requests the Warp implementation and requires float32 tensors plus an available Warp runtime; it can be exercised on Warp CPU or CUDA devices.backend="auto"selects Warp for CUDA float32 inputs and Torch for CPU or float64 inputs.
No new runtime dependency is needed: Warp is already part of EmbodiChain and is only required when its backend is selected.
Atomic Skill integration#
MoveJoints reaches the same planner through AtomicActionEngine:
from embodichain.lab.sim.atomic_actions import (
ActionInvocation,
AtomicActionEngine,
JointPositionGoal,
MotionPolicy,
)
engine = AtomicActionEngine(generator)
binding = engine.bind_control_parts(
"move_joints",
{"primary": {"motion": "arm"}},
)
invocation = ActionInvocation(
skill_id="move_joints",
goal=JointPositionGoal(target=goal_qpos),
binding=binding,
motion_policy=MotionPolicy(
strategy="motion_gen",
plan_opts=TrapezoidalPlanOptions(
profile="double_s",
constraints={
"velocity": 0.5,
"acceleration": 1.0,
"jerk": 3.0,
},
sample_interval=100,
backend="auto",
),
),
)
plan = engine.plan(invocation, latest_context)
The planner’s timed trajectory is retained at the MotionGenerator boundary.
Atomic execution may retime it to the environment control grid and derives the
velocity targets for that executed grid; terminal and stationary holds command
zero velocity.
Tutorials and benchmark#
Run the full batched simulation tutorial for joint planning, Cartesian path IK, timed replay, and position/velocity/acceleration diagnostics:
python scripts/tutorials/sim/planner/trapezoidal_planner.py \
--path joint --profile both --backend auto
The minimal scalar-profile example is
scripts/tutorials/sim/planner/trapezoidal_profile.py. Reproducible Torch/Warp
measurements are provided by
scripts/benchmark/motion_generation/trapezoidal_planner.py.
See the MotionGenerator guide for the shared planning contract and the API reference for complete class and option documentation.