USD Articulation — Multi-Arm Robots
Two layers stack on a robot USD:
- Physics:
UsdPhysics.ArticulationRootAPI, UsdPhysics.Joint, UsdPhysics.RigidBodyAPI, PhysxSchema.PhysxArticulationAPI.
- Robot Schema (
usd.schema.isaac.robot_schema): semantic overlay used by isaacsim.robot.poser, the importers, manipulators examples, and any tool that walks "this robot's links / joints / named poses".
Rule: visual plausibility is not mechanical correctness. If it doesn't articulate, it doesn't exist.
Multi-arm assembly (correct pattern)
from pxr import Usd, UsdGeom, UsdPhysics, Sdf, Gf
# Spawn chassis
chassis = stage.DefinePrim("/World/Robot", "Xform")
chassis.GetReferences().AddReference(CHASSIS_USD)
# Spawn arms as children
left = stage.DefinePrim("/World/Robot/LeftArm", "Xform")
left.GetReferences().AddReference(ARM_USD)
UsdGeom.Xformable(left).AddTranslateOp().Set(Gf.Vec3d(0.0, 0.6, 0.5))
# REQUIRED: FixedJoint connecting arm to chassis
joint = UsdPhysics.FixedJoint.Define(stage, "/World/Robot/LeftArmAttachment")
joint.CreateBody0Rel().SetTargets([Sdf.Path("/World/Robot/.../Chassis")])
joint.CreateBody1Rel().SetTargets([Sdf.Path("/World/Robot/LeftArm/.../BaseMount")])
# REQUIRED: Remove ArticulationRootAPI from arm (only chassis keeps it)
arm_prim = stage.GetPrimAtPath("/World/Robot/LeftArm/...")
arm_prim.RemoveAPI(UsdPhysics.ArticulationRootAPI)
What does not work
- Referencing the same arm USD twice via sublayer composition.
- Using
over "Geometry" to add arms in a parts layer.
- Assuming visual presence equals physical attachment.
Robot Schema overlay (Kit 110)
Apply the modern Isaac Robot Schema on top of the physics layer. The URDF/MJCF importers do this automatically; for hand-authored or retrofitted USDs apply it manually.
from pxr import Usd
from usd.schema.isaac.robot_schema import (
Classes, Attributes,
ApplyRobotAPI, ApplyLinkAPI, ApplyJointAPI, ApplySiteAPI,
CreateNamedPose, CreateSurfaceGripper,
PopulateRobotSchemaFromArticulation, GenerateRobotLinkTree, GetAllNamedPoses,
)
stage = Usd.Stage.Open("/path/robot.usd")
robot_prim = stage.GetPrimAtPath("/World/Robot")
# IsaacRobotAPI on the root: stores robot_type, ordered link/joint relations.
ApplyRobotAPI(robot_prim)
robot_prim.GetAttribute(Attributes.ROBOT_TYPE).Set("Mobile Manipulators")
# Or pick from get_allowed_tokens(Attributes.ROBOT_TYPE) — tokens include:
# Default, End Effector, Manipulator, Humanoid,
# Wheeled, Holonomic, Quadruped, Mobile Manipulators, Aerial.
# IsaacLinkAPI on each rigid link; IsaacJointAPI on each joint.
for link in (link_prims): # walk articulated links
ApplyLinkAPI(link)
for joint in (joint_prims):
ApplyJointAPI(joint)
# IsaacSiteAPI on grasp / mount / reference frames (replaces deprecated
# IsaacReferencePointAPI).
ApplySiteAPI(grasp_frame_prim)
# One-shot retrofit: populate ordered link/joint graph from an existing
# Articulation (uses physics traversal to fill ROBOT_LINKS / ROBOT_JOINTS).
PopulateRobotSchemaFromArticulation(stage, robot_prim)
| Schema |
Applied to |
Role |
IsaacRobotAPI (Classes.ROBOT_API) |
robot root |
robot_type, ordered link/joint relations, named-pose container |
IsaacLinkAPI (Classes.LINK_API) |
each rigid link |
mass/visual aux for tools that walk the chain |
IsaacJointAPI (Classes.JOINT_API) |
each joint |
semantic joint metadata |
IsaacSiteAPI (Classes.SITE_API) |
grasp / mount / reference frames |
named frames for IK targets, mounts, sensors |
IsaacNamedPose (Classes.NAMED_POSE) |
pose container prims |
stored joint configurations (see manipulation-ik) |
Classes.SURFACE_GRIPPER |
end-effector site |
author surface gripper via CreateSurfaceGripper |
Classes.ATTACHMENT_POINT_API |
site or link |
attachment points used by accessory tooling |
Validation checklist
Before trusting any multi-arm robot:
- Exactly 1
UsdPhysics.ArticulationRootAPI on the chassis, nowhere else.
FixedJoints connect each arm BaseMount to the chassis.
- Joint count matches:
N arm joints per arm + chassis joints.
IsaacRobotAPI present on the root; robot_type set to a valid token.
ROBOT_LINKS / ROBOT_JOINTS relations populated (call PopulateRobotSchemaFromArticulation if not).
- Each grasp frame carries
IsaacSiteAPI (not deprecated IsaacReferencePointAPI).
- Render from four angles (front, side, 3/4, top).
- Send to a vision LLM with a binary question ("Arms attached? PASS/FAIL").
Validation snippet (self-contained)
Run via $ISAAC_SIM_DIR/python.sh. Enforces exactly one ArticulationRootAPI, prints joints and connected bodies, flags arms whose BaseMount is not bound to the chassis via a FixedJoint, and reports the Robot Schema state.
import sys
from pxr import Usd, UsdPhysics, Sdf
from usd.schema.isaac.robot_schema import Classes, Attributes, GetAllNamedPoses
stage = Usd.Stage.Open(sys.argv[1])
art_roots = [p.GetPath() for p in stage.Traverse() if p.HasAPI(UsdPhysics.ArticulationRootAPI)]
print(f"ArticulationRootAPI count: {len(art_roots)} -- {art_roots}")
assert len(art_roots) == 1, "must be exactly 1 articulation root"
joints = [p for p in stage.Traverse() if p.IsA(UsdPhysics.Joint)]
chassis_paths = {str(art_roots[0])}
attached_arms = set()
for j in joints:
j_api = UsdPhysics.Joint(j)
b0 = j_api.GetBody0Rel().GetTargets()
b1 = j_api.GetBody1Rel().GetTargets()
if j.IsA(UsdPhysics.FixedJoint) and any(str(t).startswith(p) for t in b0 for p in chassis_paths):
attached_arms.update(str(t) for t in b1)
print(f"Joints: {len(joints)} | FixedJoint-attached children: {sorted(attached_arms)}")
# Robot Schema overlay
robots = [p for p in stage.Traverse() if p.HasAPI(Classes.ROBOT_API)]
print(f"IsaacRobotAPI count: {len(robots)}")
for r in robots:
rt = r.GetAttribute(Attributes.ROBOT_TYPE).Get()
poses = GetAllNamedPoses(stage, r)
print(f" {r.GetPath()}: robot_type={rt!r} named_poses={list(poses)}")
Extend with whatever arm/leg path patterns your asset uses. Binary goal: exactly one root, every limb connected to the chassis via a FixedJoint or articulated joint chain, and the Robot Schema overlay present.
Common failures
| Symptom |
Cause |
Fix |
| Arms render but float |
No FixedJoint to chassis |
add FixedJoint |
| Multiple articulation roots |
Arm USD has its own root |
RemoveAPI(UsdPhysics.ArticulationRootAPI) from arms |
| Training works but arms independent |
Separate articulation trees |
single root + FixedJoints |
RobotPoser.solve_ik() errors out |
missing IsaacRobotAPI / link relations |
ApplyRobotAPI + PopulateRobotSchemaFromArticulation |
Importer applied IsaacReferencePointAPI |
older asset |
re-import with current Isaac Sim, or migrate to IsaacSiteAPI (deprecation warning) |
robot_type attribute value rejected |
typo or stale token |
pick from get_allowed_tokens(Attributes.ROBOT_TYPE) |
1---2name: usd-articulation3description: Validate and build multi-arm robot articulations in USD for Isaac Sim 6 / Lab. Covers programmatic assembly (chassis + arms + `FixedJoint`s), `ArticulationRootAPI` placement, the modern Isaac Robot Schema overlay (`IsaacRobotAPI`, `IsaacLinkAPI`, `IsaacJointAPI`, `IsaacSiteAPI`, `IsaacNamedPose`, `KinematicChain`), `PopulateRobotSchemaFromArticulation` for retrofitting existing USDs, flatten-before-deploy, and the validation checklist (articulation roots, joint connections, robot-schema integrity, floating arms). Use when creating multi-limb robots in USD, retrofitting an existing articulation with the Robot Schema, debugging "floating arms" or disconnected geometry, or flattening articulations for deployment to Isaac Lab.4---56# USD Articulation — Multi-Arm Robots78Two layers stack on a robot USD:9101. **Physics**: `UsdPhysics.ArticulationRootAPI`, `UsdPhysics.Joint`, `UsdPhysics.RigidBodyAPI`, `PhysxSchema.PhysxArticulationAPI`.112. **Robot Schema** (`usd.schema.isaac.robot_schema`): semantic overlay used by `isaacsim.robot.poser`, the importers, manipulators examples, and any tool that walks "this robot's links / joints / named poses".1213Rule: visual plausibility is not mechanical correctness. If it doesn't articulate, it doesn't exist.1415## Multi-arm assembly (correct pattern)1617```python18from pxr import Usd, UsdGeom, UsdPhysics, Sdf, Gf1920# Spawn chassis21chassis = stage.DefinePrim("/World/Robot", "Xform")22chassis.GetReferences().AddReference(CHASSIS_USD)2324# Spawn arms as children25left = stage.DefinePrim("/World/Robot/LeftArm", "Xform")26left.GetReferences().AddReference(ARM_USD)27UsdGeom.Xformable(left).AddTranslateOp().Set(Gf.Vec3d(0.0, 0.6, 0.5))2829# REQUIRED: FixedJoint connecting arm to chassis30joint = UsdPhysics.FixedJoint.Define(stage, "/World/Robot/LeftArmAttachment")31joint.CreateBody0Rel().SetTargets([Sdf.Path("/World/Robot/.../Chassis")])32joint.CreateBody1Rel().SetTargets([Sdf.Path("/World/Robot/LeftArm/.../BaseMount")])3334# REQUIRED: Remove ArticulationRootAPI from arm (only chassis keeps it)35arm_prim = stage.GetPrimAtPath("/World/Robot/LeftArm/...")36arm_prim.RemoveAPI(UsdPhysics.ArticulationRootAPI)3738```3940### What does not work4142- Referencing the same arm USD twice via sublayer composition.43- Using `over "Geometry"` to add arms in a parts layer.44- Assuming visual presence equals physical attachment.4546## Robot Schema overlay (Kit 110)4748Apply the modern Isaac Robot Schema on top of the physics layer. The URDF/MJCF importers do this automatically; for hand-authored or retrofitted USDs apply it manually.4950```python51from pxr import Usd52from usd.schema.isaac.robot_schema import (53 Classes, Attributes,54 ApplyRobotAPI, ApplyLinkAPI, ApplyJointAPI, ApplySiteAPI,55 CreateNamedPose, CreateSurfaceGripper,56 PopulateRobotSchemaFromArticulation, GenerateRobotLinkTree, GetAllNamedPoses,57)5859stage = Usd.Stage.Open("/path/robot.usd")60robot_prim = stage.GetPrimAtPath("/World/Robot")6162# IsaacRobotAPI on the root: stores robot_type, ordered link/joint relations.63ApplyRobotAPI(robot_prim)64robot_prim.GetAttribute(Attributes.ROBOT_TYPE).Set("Mobile Manipulators")65# Or pick from get_allowed_tokens(Attributes.ROBOT_TYPE) — tokens include:66# Default, End Effector, Manipulator, Humanoid,67# Wheeled, Holonomic, Quadruped, Mobile Manipulators, Aerial.6869# IsaacLinkAPI on each rigid link; IsaacJointAPI on each joint.70for link in (link_prims): # walk articulated links71 ApplyLinkAPI(link)72for joint in (joint_prims):73 ApplyJointAPI(joint)7475# IsaacSiteAPI on grasp / mount / reference frames (replaces deprecated76# IsaacReferencePointAPI).77ApplySiteAPI(grasp_frame_prim)7879# One-shot retrofit: populate ordered link/joint graph from an existing80# Articulation (uses physics traversal to fill ROBOT_LINKS / ROBOT_JOINTS).81PopulateRobotSchemaFromArticulation(stage, robot_prim)82```8384| Schema | Applied to | Role |85|---|---|---|86| `IsaacRobotAPI` (`Classes.ROBOT_API`) | robot root | `robot_type`, ordered link/joint relations, named-pose container |87| `IsaacLinkAPI` (`Classes.LINK_API`) | each rigid link | mass/visual aux for tools that walk the chain |88| `IsaacJointAPI` (`Classes.JOINT_API`) | each joint | semantic joint metadata |89| `IsaacSiteAPI` (`Classes.SITE_API`) | grasp / mount / reference frames | named frames for IK targets, mounts, sensors |90| `IsaacNamedPose` (`Classes.NAMED_POSE`) | pose container prims | stored joint configurations (see `manipulation-ik`) |91| `Classes.SURFACE_GRIPPER` | end-effector site | author surface gripper via `CreateSurfaceGripper` |92| `Classes.ATTACHMENT_POINT_API` | site or link | attachment points used by accessory tooling |9394## Validation checklist9596Before trusting any multi-arm robot:97981. Exactly 1 `UsdPhysics.ArticulationRootAPI` on the chassis, nowhere else.992. `FixedJoint`s connect each arm `BaseMount` to the chassis.1003. Joint count matches: `N` arm joints per arm + chassis joints.1014. `IsaacRobotAPI` present on the root; `robot_type` set to a valid token.1025. `ROBOT_LINKS` / `ROBOT_JOINTS` relations populated (call `PopulateRobotSchemaFromArticulation` if not).1036. Each grasp frame carries `IsaacSiteAPI` (not deprecated `IsaacReferencePointAPI`).1047. Render from four angles (front, side, 3/4, top).1058. Send to a vision LLM with a binary question ("Arms attached? PASS/FAIL").106107## Validation snippet (self-contained)108109Run via `$ISAAC_SIM_DIR/python.sh`. Enforces exactly one `ArticulationRootAPI`, prints joints and connected bodies, flags arms whose `BaseMount` is not bound to the chassis via a `FixedJoint`, and reports the Robot Schema state.110111```python112import sys113from pxr import Usd, UsdPhysics, Sdf114from usd.schema.isaac.robot_schema import Classes, Attributes, GetAllNamedPoses115116stage = Usd.Stage.Open(sys.argv[1])117118art_roots = [p.GetPath() for p in stage.Traverse() if p.HasAPI(UsdPhysics.ArticulationRootAPI)]119print(f"ArticulationRootAPI count: {len(art_roots)} -- {art_roots}")120assert len(art_roots) == 1, "must be exactly 1 articulation root"121122joints = [p for p in stage.Traverse() if p.IsA(UsdPhysics.Joint)]123chassis_paths = {str(art_roots[0])}124attached_arms = set()125for j in joints:126 j_api = UsdPhysics.Joint(j)127 b0 = j_api.GetBody0Rel().GetTargets()128 b1 = j_api.GetBody1Rel().GetTargets()129 if j.IsA(UsdPhysics.FixedJoint) and any(str(t).startswith(p) for t in b0 for p in chassis_paths):130 attached_arms.update(str(t) for t in b1)131print(f"Joints: {len(joints)} | FixedJoint-attached children: {sorted(attached_arms)}")132133# Robot Schema overlay134robots = [p for p in stage.Traverse() if p.HasAPI(Classes.ROBOT_API)]135print(f"IsaacRobotAPI count: {len(robots)}")136for r in robots:137 rt = r.GetAttribute(Attributes.ROBOT_TYPE).Get()138 poses = GetAllNamedPoses(stage, r)139 print(f" {r.GetPath()}: robot_type={rt!r} named_poses={list(poses)}")140```141142Extend with whatever arm/leg path patterns your asset uses. Binary goal: exactly one root, every limb connected to the chassis via a `FixedJoint` or articulated joint chain, and the Robot Schema overlay present.143144145## Common failures146147| Symptom | Cause | Fix |148|---|---|---|149| Arms render but float | No `FixedJoint` to chassis | add `FixedJoint` |150| Multiple articulation roots | Arm USD has its own root | `RemoveAPI(UsdPhysics.ArticulationRootAPI)` from arms |151| Training works but arms independent | Separate articulation trees | single root + `FixedJoint`s |152| `RobotPoser.solve_ik()` errors out | missing `IsaacRobotAPI` / link relations | `ApplyRobotAPI` + `PopulateRobotSchemaFromArticulation` |153| Importer applied `IsaacReferencePointAPI` | older asset | re-import with current Isaac Sim, or migrate to `IsaacSiteAPI` (deprecation warning) |154| `robot_type` attribute value rejected | typo or stale token | pick from `get_allowed_tokens(Attributes.ROBOT_TYPE)` |