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#
Install cuRobo V2 separately from EmbodiChain. 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#
Select a registered robot with an IK solver for the desired control part.
The adapter generates the robot profile from its URDF, joint names, base link,
and TCP; see robot model generation for caching
and collision-sphere settings. CuroboPlanOptions.control_part selects the
part at plan time.
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(),
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.
Generate a motion#
MotionGenerator forwards start_qpos and control_part to cuRobo. Native
Cartesian goals bypass generic pre-interpolation. If MotionGenOptions.sample_count
requests a different output count, the generator resamples in time while
preserving duration. Set CuroboPlannerCfg.preserve_plan_samples=True to retain
cuRobo’s original collision-checked samples. When resampling changes the grid,
the generator revalidates the final joint samples against the resolved collision
scene and fails rows with invalid samples. This check does not certify the
continuous path between samples.
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).repeat(robot.num_instances, 1, 1)
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()
Devices and CUDA graphs#
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.
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.
Collision world#
Warning
cuRobo self-collision checking is temporarily disabled in this backend. Robot-to-world collision checking remains enabled, but planned trajectories are not currently rejected when two robot links collide with each other.
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.
The collision world is auto-generated from live RigidObject physical
collision shapes via RigidObject.get_collision_shapes(). It does not use
get_vertices() / get_triangles(), which expose combined visual meshes and may
differ from the geometry used by DexSim physics. The canonical, registry-backed
form of CuroboWorldCfg.rigid_objects is a mapping from authoritative registry
ID to live object. The mapping key, rather than RigidObject.uid, becomes the
logical collision ID, cache identity, generated obstacle-name prefix, and
runtime-update key. The sequence form remains available for advanced callers
and derives IDs from each uid or an obstacle_<index> fallback.
CuroboWorldCfg.representation="auto" is the default. The policy preserves
boxes as cuboids, spheres and capsules as analytic primitives, and convex
collision shapes as meshes. Triangle meshes remain meshes up to
mesh_triangle_threshold; above that threshold they become voxel ESDF when the
estimated dense allocation fits max_voxel_count. Pose-dynamic meshes use twice
the threshold before voxelization, while static cached meshes favor ESDF sooner
for repeated collision queries. SDF descriptors fall back to
their canonical collision mesh because the current DexSim Python binding does
not expose reusable SDF grid data. Unsupported descriptors raise an explicit
error instead of silently falling back to visual geometry.
Forced voxel mode and per-object overrides remain available:
world_cfg = CuroboWorldCfg(
rigid_objects={
"room_scan": room_scan,
"precision_fixture": precision_fixture,
},
representation="auto",
overrides={
"room_scan": "voxel",
"precision_fixture": "mesh",
},
)
voxel_size and voxel_padding configure generated ESDF layers. plane_dims
bounds an infinite DexSim plane as a thin cuRobo cuboid. Compound and ACD bodies
produce stable names such as fixture__shape_0; callers still use the owning
canonical ID in dynamic_obstacle_names and
CuroboPlanOptions.dynamic_obstacle_poses, and the adapter fans each update out
through the sub-shapes’ local poses.
DexSim binding requirement: Correct compound/USD offsets require
RigidBody.get_shape_geometry()to copy each physical shape’s local pose intoShapeGeometry.local_pose. Reusing a DexSim SDF as a voxel grid additionally requires grid metadata/data that the current Python API does not expose. Until those upstream bindings are available, verify compound offsets explicitly; SDF descriptors use their canonical collision mesh when one is exposed and otherwise raise an actionable error.
Generated poses are authored in the cuRobo base/world frame, so this is exact
when the robot base sits at the simulator world origin. For obstacles that move
or live in an offset base frame, declare their canonical IDs in
CuroboWorldCfg.dynamic_obstacle_names and update poses at plan time through
CuroboPlanOptions.dynamic_obstacle_poses.
Registry-backed mappings fail fast if a selected source has no physical collision shapes. This prevents a canonical collision ID from being silently omitted from the generated tensor-backed scene. The advanced sequence form may skip such an object with a warning, independently of the registry contract.
CuroboPlanner.collision_world_info.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 compound-expanded
physical shape names. collision_world_info.dynamic_entity_ids reports exactly
the configured dynamic subset. MotionGenerator forwards these through its
collision_world_entity_ids and dynamic_collision_entity_ids properties.
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. Use the mapping form for registry-backed worlds.
For registry-backed integration, derive geometry, dynamic IDs, and the scene provider from the same registry, as in the configuration example. The Scene registry validates exact collision-ID, dynamic-ID, and batch-mode agreement before execution.
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. Sphere
fitting always uses DexSim’s SphereFitType.MORPHIT, with at most 2 convex hulls
per robot link and 16 per voxelized obstacle shape. 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, or set force=True to bypass the cache.
For an Open3D overlay of the robot collision spheres and sampled world collision
representations read back from those caches, call
planner.visualize_robot_collision_models(control_part). Robot sphere centers are
transformed by the simulator’s live link poses. The interactive cuRobo example
calls this once after planner initialization; close the Open3D window to continue.
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.
Supported scope is limited as follows:
Only one configured control part is planned per request; coordinated dual-arm planning and CoordinatedPickment are unsupported.
Collision worlds use the physical shapes described above 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.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 YAML and mixed collision-world cache are
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.