cuRobo V2 Planner#
CuroboPlanner is EmbodiChain’s optional, CUDA-accelerated and collision-aware motion-planning backend. It implements the normal MotionGenerator and atomic-action interfaces while cuRobo performs collision-aware inverse kinematics and trajectory optimization. It supports Cartesian EEF_MOVE and joint-space JOINT_MOVE requests for one configured control part at a time.
planner_type=”curobo” selects this backend. cuRobo V2 is deliberately not an EmbodiChain core dependency: importing EmbodiChain planners does not import cuRobo, and constructing this planner requires a CUDA-capable NVIDIA GPU.
Install cuRobo V2#
cuRobo V2 is installed separately from EmbodiChain because public package indexes do not accept Git dependencies in published package metadata. Select exactly one source requirement that matches the CUDA runtime used by PyTorch:
python -c "import torch; print(torch.version.cuda)"
Use cu12 for a 12.x result and cu13 for a 13.x result. Do not select the
extra from the maximum CUDA version displayed by nvidia-smi.
# Recommended for the normal EmbodiChain environment, where PyTorch is present.
uv pip install "nvidia-curobo[cu12] @ git+https://github.com/NVlabs/curobo.git@v0.8.0"
uv pip install "nvidia-curobo[cu13] @ git+https://github.com/NVlabs/curobo.git@v0.8.0"
# For a fresh environment that also needs PyTorch.
uv pip install "nvidia-curobo[cu12-torch] @ git+https://github.com/NVlabs/curobo.git@v0.8.0"
uv pip install "nvidia-curobo[cu13-torch] @ git+https://github.com/NVlabs/curobo.git@v0.8.0"
python -c "import curobo; print(curobo.__version__)"
pytest --pyargs curobo.tests
These commands follow NVIDIA’s official cuRobo installation
guide
and pin the source dependency to the cuRobo V2 v0.8.0 release. Although
cuRobo supports Python 3.10–3.13, use EmbodiChain’s supported Python 3.10,
3.11, or 3.12 environment on Linux with a supported NVIDIA GPU and driver. The
non-torch variants are preferred for EmbodiChain because the simulation
environment normally already provides PyTorch; the -torch variants delegate
the PyTorch version requirement to cuRobo. Keep cuRobo in the same Python
environment that runs the simulator.
Configure a control part#
The cuRobo robot model and the per-control-part profile are both auto-generated
internally - no external cuRobo robot YAML (e.g. franka.yml) and no
robot_profiles config are needed. On the first plan, the adapter fits collision
spheres to each link of the robot’s URDF and writes a cuRobo V2 robot YAML (see
Auto-generated robot YAML). The tool frame, TCP
offset, and base link are read from the control part’s IK solver, and the
simulator->cuRobo joint mapping is identity (the generated YAML reuses the
URDF’s own joint names). The control part is selected at plan time through
CuroboPlanOptions.control_part and validated against robot.control_parts.
Lock non-controlled joints (for example gripper joints) in the cuRobo robot
profile so they are not exposed as active planner joints. The simulator values of
those joints must remain equal to the V2 profile’s lock_joints values while a
plan is executed; the adapter intentionally preserves non-control simulator
joints in the full-DoF atomic-action output. For example, the Panda V2 profile
locks both fingers at 0.04, so use the same simulated finger state or include
the fingers in the planned control part. A mismatch means cuRobo validates a
different collision geometry from the one replayed in DexSim.
Assuming the scene has been registered as shown in Scene registry, construct the planner world from that catalog:
from embodichain.lab.sim.motion.motion_generator import MotionGenCfg, MotionGenerator
from embodichain.lab.sim.motion.planners import CuroboPlannerCfg, CuroboWorldCfg
from embodichain.lab.task_program.semantics import SceneCollisionWorldMode
collision_mode = registry.resolve_collision_world_mode(
batch_size=robot.num_instances,
)
planner_cfg = CuroboPlannerCfg(
robot_uid="my_franka",
planner_type="curobo",
world=CuroboWorldCfg(
rigid_objects=registry.collision_geometry_by_id(),
obstacle_representation="cuboid",
dynamic_obstacle_names=list(registry.dynamic_collision_entity_ids),
multi_env=collision_mode is SceneCollisionWorldMode.PER_ENV,
),
)
motion_generator = MotionGenerator(MotionGenCfg(planner_cfg=planner_cfg))
scene_provider = registry.make_planning_scene_provider(
motion_generator,
batch_size=robot.num_instances,
)
cuRobo’s Python logger defaults to error-only output. Set
CuroboPlannerCfg.log_level to "debug", "info", "warning", or "error"
to change its verbosity. This setting does not affect EmbodiChain’s own logs.
The physics and planner devices are independent.
SimulationManagerCfg(sim_device="cpu") keeps robot state, targets, and
returned trajectories on CPU,
while cuRobo still performs all model generation and planning on CUDA. By
default a CPU simulation uses PyTorch’s current CUDA device; set
CuroboPlannerCfg.cuda_device="cuda:1" (or an integer GPU index) to select a
different planning GPU. A CPU value is rejected because cuRobo itself has no
CPU backend.
The robot configuration must be a cuRobo V2 robot profile with collision spheres and self-collision data; the adapter generates this from the robot’s URDF automatically. A plain URDF alone is not sufficient for collision planning without that sphere-fitting step.
The adapter automatically rebases simulator-world Cartesian goals and dynamic
obstacle poses through the live simulator control-part base, so parallel arena
offsets and a moved robot base are handled. If the simulator and cuRobo base
frames use different fixed conventions, set
CuroboPlannerCfg.sim_base_to_curobo_base to the transform from the simulator
base to the cuRobo base. Collision-world poses are authored in the cuRobo
base/world frame. tool_frame_to_tcp (read from solver.tcp_xpos) converts an
EmbodiChain TCP goal into the chosen cuRobo tool frame when the solver’s end link
is not itself the TCP. By convention, the adapter uses
T_curobo,X = T_curobo,sim_base @ inv(T_world,sim_base) @ T_world,X. It obtains
the simulator base from the control part’s IK solver root.
CuroboPlannerCfg.use_cuda_graph defaults to True. The planner runs in the
simulator process and reuses its CUDA context; it does not launch a persistent
spawn worker or copy planning tensors through multiprocessing queues. Set
use_cuda_graph=False when lower one-time initialization cost and lower
graph-resident memory are more important than hot planning latency.
In graph mode, planner initialization uses the same per-device
CaptureCoordinator as DexSim’s Newton backend and synchronizes the device
before and after cuRobo warmup. EmbodiChain also forces cuRobo’s PyTorch graphs
to use cuda_graph_capture_error_mode="thread_local" (the default).
Unlike PyTorch’s strict "global" mode, this allows DexSim’s Vulkan render
thread to continue making CUDA calls without invalidating capture on the
planner thread.
If coordinator acquisition times out before recording begins,
cuda_graph_fallback=True waits for the active capture to finish and builds a
non-graph backend. An exception after graph recording starts is deliberately
not downgraded: CUDA may have invalidated the process context, so the planner
raises an error and requires a simulator-process restart. The "global" and
"relaxed" modes remain available for diagnosis, but "thread_local" is the
supported renderer-compatible setting.
cuRobo cannot reset a captured trajectory-optimizer graph when switching between Cartesian pose goals and joint-space goals. EmbodiChain therefore caches those two goal types separately and initializes each lazily. Applications that use only one move type retain one planner backend; using both incurs a second one-time warmup and its graph-resident memory, but still no subprocess or second CUDA context.
The collision world is always auto-generated from live RigidObject meshes via
CuroboWorldCfg.rigid_objects. The canonical, registry-backed form is a mapping
from authoritative registry ID to live object; the adapter reads each object’s
mesh (get_vertices / get_triangles) and world pose (get_local_pose) and
writes a cached cuRobo scene YAML on the first plan, using
CuroboWorldCfg.obstacle_representation ("sphere" by default for fast
collision queries; use "cuboid" for a local-frame AABB placed as an OBB via
the object pose, or "mesh" for the exact triangle mesh).
Generated poses are authored in the cuRobo base/world frame, so this is exact
when the robot base sits at the simulator world origin. The mapping key, rather
than RigidObject.uid, is the canonical logical/source ID used by cache
identity and collision-world validation. For "cuboid" and "mesh", that ID
is also used unchanged as the physical YAML obstacle name and runtime update
key. For obstacles that move or live in an offset base frame, also declare their
canonical IDs in
CuroboWorldCfg.dynamic_obstacle_names and update poses at plan time through
CuroboPlanOptions.dynamic_obstacle_poses (provision
CuroboWorldCfg.collision_cache before planning). Dynamic updates require the
"cuboid" or "mesh" representation because sphere fitting expands one object
into physical YAML obstacles named <canonical_id>_0, <canonical_id>_1, and
so on; dynamic sphere configuration is rejected. These derived names are
backend details. The cache and registry/planner full-world contract continue to
use the unexpanded canonical source ID.
Registry-backed mappings fail fast if a selected source has no mesh geometry required by the chosen representation. This prevents a canonical collision ID from being silently skipped during YAML generation. The advanced sequence form retains its lower-level behavior independently of this registry contract.
CuroboPlanner.collision_world_entity_ids reports every configured logical
source ID: each mapping key on the registry path, or each inferred name on the
advanced sequence path. It deliberately does not expose sphere-expanded
physical YAML names. dynamic_collision_entity_ids reports exactly the
configured dynamic subset. Static entries therefore participate in
construction-time identity validation even though they do not receive per-plan
pose updates.
CuroboWorldCfg validates this planner-local registration at construction:
obstacle IDs must be unique, and every dynamic obstacle ID must match an entry
in rigid_objects. A sequence of objects is retained only as an advanced
direct-core path; it derives names from each uid or an obstacle_<index>
fallback. Do not use that form for a registry-backed world.
The Scene registry integration performs two
higher-level checks before execution. First, all registry STATIC ∪ DYNAMIC
IDs must exactly equal
MotionGenerator.collision_world_entity_ids. Second, registry, derived scene
provider, and planner dynamic-ID subsets must exactly agree. The planner must
also support pose updates and its shared/per-environment batch mode must agree
with the registry. Aliases are normalized at the registry boundary; cuRobo
never translates a canonical ID back to a simulator UID.
Auto-generated robot YAML#
On the first plan, the adapter auto-derives the cuRobo robot profile from the robot’s URDF and solver, so nothing robot-specific needs to be hardcoded:
robot_config_pathis produced bygenerate_curobo_robot_yaml, which fits collision spheres to each link mesh and writes a cuRobo V2 robot YAML.The TCP, tool frame, and base link are read from the robot’s solver (
robot._solvers[control_part]):tool_frame_name<-solver.end_link_name,tool_frame_to_tcp<-solver.tcp_xpos,base_link_name<-solver.root_link_name.sim_to_curobo_joint_namesis the identity mapping, since the generated YAML reuses the simulator’s own URDF joint names.
The generated YAML is cached on disk (default $XDG_CACHE_HOME/embodichain_curobo
or ~/.cache/embodichain_curobo) keyed by the URDF path, URDF content, control
part, tool frame, and fit parameters, so editing the URDF or changing the fit
settings regenerates automatically and subsequent inits reuse the cache. Tune the
fit with CuroboPlannerCfg.auto_gen (fit_type="voxel" by default for fast
first-generation; "morphit" for best quality; force=True to bypass the cache).
The default sphere_density=0.1 keeps the per-link sphere count low (~80 for a
Panda) so planning stays fast; raise it for tighter collision coverage.
Generate a motion#
MotionGenerator passes start_qpos and control_part to the cuRobo backend. For
Cartesian goals, leave EmbodiChain pre-interpolation disabled: cuRobo must
receive the original pose. By default the returned collision-checked samples are
arc-length resampled to the invocation’s MotionPolicy.sample_count waypoint
count (so the same runtime policy controls trajectory length across planners);
set CuroboPlannerCfg.preserve_plan_samples=True to keep
cuRobo’s own samples (whose count is derived from interpolation_dt and the
trajectory duration).
import torch
from embodichain.lab.sim.motion.motion_generator import MotionGenOptions
from embodichain.lab.sim.motion.planners import CuroboPlanOptions, PlanState
goal_pose = torch.eye(4, device=robot.device).unsqueeze(0)
goal_pose[:, :3, 3] = torch.tensor(
[[0.55, 0.30, 0.45]], device=robot.device
)
result = motion_generator.generate(
[PlanState.from_xpos(goal_pose)],
MotionGenOptions(
start_qpos=robot.get_qpos(name="arm"),
control_part="arm",
plan_opts=CuroboPlanOptions(),
),
)
assert result.success.all()
Atomic actions and supported scope#
Single-arm MoveEndEffector is supported through the normal
strategy="motion_gen" route. MoveJoints can opt in to collision-aware
joint-space planning with strategy="motion_gen"; the action uses the planner
already owned by its MotionGenerator. Movement phases of PickUp, Place,
and MoveHeldObject can use the same single-arm static-world route.
This first release intentionally has the following limits:
Only one configured control part is planned per request; coordinated dual-arm planning and CoordinatedPickment are unsupported.
Collision worlds are generated from
RigidObjectmeshes (cuboid/mesh/sphere) plus named dynamic pose updates. Arbitrary geometry insertion and removal at runtime are unsupported.The generated collision world assumes a fixed-base robot at the simulator origin. With a moving base, publish each relevant world obstacle as a named dynamic pose for every plan; automatic reprojection of static obstacles is unsupported.
attached-object collision geometry, automatic attachment/detachment, and collision-aware carrying of a held object are unsupported.
Non-control joints must remain at the matching cuRobo V2
lock_jointsvalues. The adapter does not yet validate cross-model locked-joint name/value equivalence automatically.The legacy Gym ActionBank path is unsupported.
CPU execution of cuRobo itself and cuRobo V1 compatibility are unsupported. CPU physics is supported because tensors are transferred to CUDA only for planning and the resulting trajectory is copied back to the simulation device.
Demo#
After installing cuRobo V2 and configuring a CUDA simulation environment, run the Panda obstacle-avoidance demo from the repository root:
python examples/sim/motion/planners/curobo_planner.py --headless --hold-steps 1 --step-repeat 1
# CPU physics with CUDA planning
python examples/sim/motion/planners/curobo_planner.py --headless --sim-device cpu
The demo exports the DexSim demo_block into the cuRobo collision world via
CuroboWorldCfg.rigid_objects (the robot and world YAMLs are both
auto-generated), prints the result status and trajectory shape, then replays the
returned full-DoF trajectory. CUDA graph capture is enabled by default with the
renderer-compatible "thread_local" mode; pass --no-cuda-graph to disable it.
Headless runs
automatically record this fixed offscreen camera view to an MP4. Set an explicit
destination with --record-save-path outputs/videos/curobo_demo.mp4, adjust
the rate with --record-fps, or pass --disable-record to skip recording. See
MotionGenerator for the common planner interface.