Motion Generator#
The MotionGenerator class in EmbodiChain provides a unified and extensible interface for robot trajectory planning. It supports time-optimal trajectory generation (currently via TOPPRA), joint/Cartesian interpolation, and is designed for easy integration with RL, imitation learning, and classical control scenarios.
Key Features#
Unified API: One interface for multiple planning strategies (time-optimal, interpolation, etc.)
Constraint Support: Velocity/acceleration constraints configurable per joint
Flexible Input: Supports both joint space and Cartesian space waypoints
Extensible: Easy to add new planners (RRT, PRM, etc.)
Integration Ready: Can be used in RL, imitation learning, or classical pipelines
The Code#
The tutorial corresponds to the motion_generator.py script in the scripts/tutorials/sim directory.
Code for motion_generator.py
1# ----------------------------------------------------------------------------
2# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15# ----------------------------------------------------------------------------
16
17from __future__ import annotations
18
19import argparse
20import time
21from collections.abc import Sequence
22
23import numpy as np
24import torch
25
26from embodichain.lab.gym.utils.gym_utils import add_env_launcher_args_to_parser
27from embodichain.lab.sim import SimulationManager, SimulationManagerCfg
28from embodichain.lab.visualization import visualization_cfg_from_args
29from embodichain.lab.sim.cfg import RenderCfg
30from embodichain.lab.sim.objects import Robot
31from embodichain.lab.sim.motion.motion_generator import (
32 MotionGenCfg,
33 MotionGenOptions,
34 MotionGenerator,
35)
36from embodichain.lab.sim.motion.planners import (
37 PlanState,
38 ToppraPlanOptions,
39 ToppraPlannerCfg,
40)
41from embodichain.lab.sim.motion.planners.utils import TrajectorySampleMethod
42from embodichain.lab.sim.robots import CobotMagicCfg
43
44RECORD_WIDTH = 1920
45RECORD_HEIGHT = 1080
46DEFAULT_ARENA_SPACE = 3.0
47DEFAULT_RECORD_TARGET_Z = 0.95
48DEFAULT_RECORD_MAX_MEMORY = 2048
49
50
51def parse_args() -> argparse.Namespace:
52 """Parse command line arguments for the motion-generator tutorial."""
53 parser = argparse.ArgumentParser(
54 description="Generate and replay MotionGenerator trajectories for one or more environments."
55 )
56 add_env_launcher_args_to_parser(parser)
57 parser.add_argument(
58 "--arena-space",
59 type=float,
60 default=DEFAULT_ARENA_SPACE,
61 help="Spacing between replicated tutorial environments.",
62 )
63 parser.add_argument(
64 "--step-delay",
65 type=float,
66 default=0.1,
67 help="Seconds to wait between trajectory waypoints during playback.",
68 )
69 parser.add_argument(
70 "--record-fps",
71 type=int,
72 default=20,
73 help="Output video FPS for headless recording.",
74 )
75 parser.add_argument(
76 "--record-save-path",
77 type=str,
78 default=None,
79 help="Optional mp4 output path for headless recording.",
80 )
81 parser.add_argument(
82 "--disable-record",
83 action="store_true",
84 help="Disable automatic whole-scene recording in headless mode.",
85 )
86 return parser.parse_args()
87
88
89def compute_record_look_at(
90 num_envs: int,
91 arena_space: float,
92) -> tuple[
93 tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]
94]:
95 """Return a fixed camera pose that frames the full replicated arena grid."""
96 if num_envs <= 0:
97 raise ValueError(f"num_envs must be positive, got {num_envs}.")
98
99 scene_grid_length = int(np.ceil(np.sqrt(num_envs)))
100 scene_grid_rows = int(np.ceil(num_envs / scene_grid_length))
101 span_x = float(max(scene_grid_length - 1, 0) * arena_space)
102 span_y = float(max(scene_grid_rows - 1, 0) * arena_space)
103 scene_extent = 0.5 * max(span_x, span_y)
104
105 target = (
106 0.5 * span_x,
107 0.5 * span_y,
108 DEFAULT_RECORD_TARGET_Z,
109 )
110 eye = (
111 2.6 + target[0] + scene_extent,
112 -2.2 - scene_extent,
113 1.6 + 0.4 * scene_extent,
114 )
115 return eye, target, (0.0, 0.0, 1.0)
116
117
118def move_robot_along_trajectory(
119 sim: SimulationManager,
120 robot: Robot,
121 arm_name: str,
122 qpos_trajectory: torch.Tensor | Sequence[torch.Tensor],
123) -> None:
124 """Play back a planned joint trajectory for one or more environments.
125
126 This function assumes the simulation is in manual-update mode and calls
127 :meth:`SimulationManager.update` after each waypoint so physics advances.
128
129 Args:
130 sim: Simulation manager instance.
131 robot: Robot instance.
132 arm_name: Name of the robot arm to control.
133 qpos_trajectory: Joint positions shaped ``(B, N, DOF)``, ``(N, DOF)``,
134 or a sequence of waypoint tensors.
135 delay: Time delay between each step in seconds.
136 """
137 if isinstance(qpos_trajectory, Sequence):
138 qpos_steps = list(qpos_trajectory)
139 if not qpos_steps:
140 return
141 if qpos_steps[0].dim() == 1:
142 qpos_trajectory = torch.stack(qpos_steps, dim=0).unsqueeze(0)
143 else:
144 qpos_trajectory = torch.stack(qpos_steps, dim=1)
145 if qpos_trajectory.dim() == 2:
146 qpos_trajectory = qpos_trajectory.unsqueeze(0)
147 if qpos_trajectory.dim() != 3:
148 raise ValueError(
149 "qpos_trajectory must have shape (B, N, DOF) or (N, DOF), "
150 f"got {tuple(qpos_trajectory.shape)}."
151 )
152
153 joint_ids = robot.get_joint_ids(arm_name)
154 for qpos_step in qpos_trajectory.transpose(0, 1):
155 robot.set_qpos(qpos=qpos_step, joint_ids=joint_ids)
156 sim.update(step=4)
157
158
159def create_demo_trajectory(
160 robot: Robot,
161 arm_name: str,
162 num_envs: int = 1,
163) -> tuple[list[torch.Tensor], list[torch.Tensor]]:
164 """Generate a three-point batched demo trajectory for the requested env count."""
165 if num_envs <= 0:
166 raise ValueError(f"num_envs must be positive, got {num_envs}.")
167
168 tensor_kwargs = {"dtype": torch.float32}
169 robot_device = getattr(robot, "device", None)
170 if isinstance(robot_device, (str, torch.device)):
171 tensor_kwargs["device"] = robot_device
172
173 qpos_fk = torch.tensor(
174 [[0.0, np.pi / 4, -np.pi / 4, 0.0, np.pi / 4, 0.0]],
175 **tensor_kwargs,
176 ).repeat(num_envs, 1)
177 xpos_begin = robot.compute_fk(name=arm_name, qpos=qpos_fk, to_matrix=True)
178 xpos_mid = xpos_begin.clone()
179 xpos_mid[:, 2, 3] -= 0.1
180 xpos_final = xpos_mid.clone()
181 xpos_final[:, 0, 3] += 0.2
182
183 qpos_begin = robot.compute_ik(pose=xpos_begin, name=arm_name)[1]
184 qpos_mid = robot.compute_ik(pose=xpos_mid, name=arm_name)[1]
185 qpos_final = robot.compute_ik(pose=xpos_final, name=arm_name)[1]
186 return [qpos_begin, qpos_mid, qpos_final], [xpos_begin, xpos_mid, xpos_final]
187
188
189def start_headless_recording(
190 sim: SimulationManager,
191 args: argparse.Namespace,
192) -> bool:
193 """Start headless viewer recording with a whole-scene camera."""
194 if not args.headless or args.disable_record:
195 return False
196
197 look_at = compute_record_look_at(
198 num_envs=sim.num_envs,
199 arena_space=sim.sim_config.arena_space,
200 )
201 if not sim.start_window_record(
202 save_path=args.record_save_path,
203 fps=args.record_fps,
204 max_memory=DEFAULT_RECORD_MAX_MEMORY,
205 video_prefix="motion_generator_headless",
206 look_at=look_at,
207 use_sim_time=False,
208 ):
209 raise RuntimeError("Failed to start headless recording")
210
211 print("[INFO]: Headless recording enabled.")
212 print(
213 "[INFO]: The output path is reported by `SimulationManager.start_window_record()`."
214 )
215 return True
216
217
218def main() -> None:
219 """Run the motion-generator tutorial."""
220 args = parse_args()
221
222 np.set_printoptions(precision=5, suppress=True)
223 torch.set_printoptions(precision=5, sci_mode=False)
224
225 sim = SimulationManager(
226 SimulationManagerCfg(
227 width=RECORD_WIDTH,
228 height=RECORD_HEIGHT,
229 headless=True,
230 physics_dt=1.0 / 100.0,
231 sim_device=args.device,
232 render_cfg=RenderCfg(renderer=args.renderer),
233 num_envs=args.num_envs,
234 arena_space=args.arena_space,
235 visualization=visualization_cfg_from_args(args),
236 )
237 )
238
239 robot: Robot = sim.add_robot(cfg=CobotMagicCfg.from_dict({"uid": "CobotMagic"}))
240 arm_name = "left_arm"
241
242 if sim.is_use_gpu_physics:
243 sim.init_gpu_physics()
244
245 if not args.headless:
246 sim.open_window()
247
248 print(
249 f"[INFO]: Running motion generator tutorial with {sim.num_envs} environment(s)"
250 )
251
252 recording_started = start_headless_recording(sim, args)
253 try:
254 qpos_list, xpos_list = create_demo_trajectory(
255 robot=robot,
256 arm_name=arm_name,
257 num_envs=sim.num_envs,
258 )
259
260 motion_generator = MotionGenerator(
261 cfg=MotionGenCfg(
262 planner_cfg=ToppraPlannerCfg(
263 robot_uid=robot.uid,
264 )
265 )
266 )
267
268 options = MotionGenOptions(
269 strategy="motion_gen",
270 control_part=arm_name,
271 start_qpos=qpos_list[0],
272 is_interpolate=True,
273 is_linear=False,
274 plan_opts=ToppraPlanOptions(
275 constraints={
276 "velocity": 0.2,
277 "acceleration": 0.5,
278 },
279 sample_method=TrajectorySampleMethod.QUANTITY,
280 sample_interval=20,
281 ),
282 )
283
284 joint_plan = motion_generator.generate(
285 target_states=[PlanState.from_qpos(qpos) for qpos in qpos_list],
286 options=options,
287 )
288 if joint_plan.positions is None:
289 raise RuntimeError("Joint-space planning did not produce any positions.")
290 move_robot_along_trajectory(
291 sim=sim,
292 robot=robot,
293 arm_name=arm_name,
294 qpos_trajectory=joint_plan.positions,
295 )
296
297 options.is_linear = True
298 cartesian_plan = motion_generator.generate(
299 target_states=[PlanState.from_xpos(xpos) for xpos in xpos_list],
300 options=options,
301 )
302 if cartesian_plan.positions is None:
303 raise RuntimeError(
304 "Cartesian-space planning did not produce any positions."
305 )
306 sim.reset()
307 move_robot_along_trajectory(
308 sim=sim,
309 robot=robot,
310 arm_name=arm_name,
311 qpos_trajectory=cartesian_plan.positions,
312 )
313 finally:
314 if sim.is_window_recording():
315 sim.stop_window_record()
316 sim.wait_window_record_saves()
317 sim.destroy()
318
319
320if __name__ == "__main__":
321 main()
Typical Usage#
from embodichain.lab.sim.motion.motion_generator import MotionGenerator, MotionGenCfg
from embodichain.lab.sim.motion.planners import ToppraPlannerCfg
from embodichain.lab.sim.motion.planners.toppra_planner import ToppraPlanOptions
from embodichain.lab.sim.motion.planners.utils import PlanState, TrajectorySampleMethod, MoveType
# Assume you have a robot instance and arm_name
# Constraints are now specified in ToppraPlanOptions, not in ToppraPlannerCfg
motion_cfg = MotionGenCfg(
planner_cfg=ToppraPlannerCfg(
robot_uid=robot.uid,
)
)
motion_gen = MotionGenerator(cfg=motion_cfg)
# Create options with constraints and planning parameters
plan_opts = ToppraPlanOptions(
constraints={
"velocity": 0.2,
"acceleration": 0.5,
},
sample_method=TrajectorySampleMethod.TIME,
sample_interval=0.01
)
# Create motion generation options
motion_opts = MotionGenOptions(
strategy="motion_gen",
plan_opts=plan_opts,
control_part=arm_name,
is_interpolate=True,
interpolate_nums=10,
)
# Plan a joint-space trajectory (use generate() method instead of plan())
target_states = [
PlanState(move_type=MoveType.JOINT_MOVE, qpos=torch.tensor([0.5, 0.2, 0., 0., 0., 0.]))
]
plan_result = motion_gen.generate(
target_states=target_states,
options=motion_opts
)
success = plan_result.success
positions = plan_result.positions
velocities = plan_result.velocities
accelerations = plan_result.accelerations
duration = plan_result.duration
API Reference#
Initialization
from embodichain.lab.sim.motion.planners.toppra_planner import ToppraPlanOptions
motion_cfg = MotionGenCfg(
planner_cfg=ToppraPlannerCfg(
robot_uid=robot.uid,
)
)
MotionGenerator(cfg=motion_cfg)
cfg: MotionGenCfg instance, containing the specific planner’s configuration (likeToppraPlannerCfg)robot_uid: Robot unique identifierconstraints: Now specified inToppraPlanOptions(passed viaMotionGenOptions.plan_opts)
MotionGenOptions
motion_opts = MotionGenOptions(
strategy="motion_gen", # "motion_gen" or "ik_interp"
sample_count=None, # Optional normalized output length
interpolation_dt=None, # Required for deterministic interpolation
plan_opts=ToppraPlanOptions(...), # Options for the underlying planner
control_part=arm_name, # Robot part to control (e.g., 'left_arm')
is_interpolate=False, # Whether to pre-interpolate trajectory
interpolate_nums=10, # Number of interpolation points between waypoints
is_linear=False, # Use Cartesian linear interpolation if True, else joint space
interpolate_position_step=0.002, # Step size for Cartesian interpolation (meters)
interpolate_angle_step=np.pi/90, # Step size for joint interpolation (radians)
start_qpos=torch.tensor([...]), # Optional starting joint configuration
)
generate (formerly plan)
generate(
target_states: list[PlanState],
options: MotionGenOptions | None = None,
) -> PlanResult
strategy="motion_gen"delegates to the configured backend;strategy="ik_interp"performs deterministic waypoint IK and joint interpolation and requiresinterpolation_dt.Returns a normalized, environment-batched
PlanResultwith explicitdtand deriveddurationwhenever positions are present. Missing timing raises immediately.Uses
target_states(list of PlanState) andoptions(MotionGenOptions) instead of individual parameters.
interpolate_trajectory
interpolate_trajectory(
control_part: str | None = None,
xpos_list: torch.Tensor | None = None,
qpos_list: torch.Tensor | None = None,
options: MotionGenOptions | None = None,
) -> Tuple[torch.Tensor, torch.Tensor | None]
Interpolates trajectory between waypoints (joint or Cartesian), auto-handles FK/IK.
estimate_trajectory_sample_count
estimate_trajectory_sample_count(
xpos_list=None,
qpos_list=None,
step_size=0.01,
angle_step=np.pi/90,
control_part=None,
) -> torch.Tensor
Estimates the number of samples needed for a trajectory.
plan_with_collision
plan_with_collision(...)
(Reserved) Plan trajectory with collision checking (not yet implemented).
Notes & Best Practices#
Only collision-free planning is currently supported; collision checking is a placeholder.
Input/outputs are numpy arrays or torch tensors; ensure type consistency.
Robot instance must implement get_joint_ids, compute_fk, compute_ik, get_proprioception, etc.
For custom planners, extend the PlannerType Enum and _create_planner methods.
Constraints (velocity, acceleration) are now specified in
ToppraPlanOptions, not inToppraPlannerCfg.Use
PlanState.qpos(notposition) for joint positions.