← All work

February 2026 · Engineering

Why your “kinematic body” breaks Isaac Lab articulations

This is the post we wish someone had handed us a quarter ago. Four small USD authoring details, each of which cost a day. None of them are documented in a place a search engine would find. All of them now have a test in the qualification gauntlet (T11–T14).

If you’re bringing assets into Isaac Lab from any pipeline that doesn’t natively author articulations, this post is for you.

Gotcha 1 — UsdPhysics.ArticulationRootAPI must be on the root prim, applied explicitly

The symptom: your asset loads. Your policy trains. The reward curve looks plausible for the first few thousand steps. Nothing visibly moves.

The cause: Isaac Lab silently treats an asset without ArticulationRootAPI as a tree of independent rigid bodies. The joints exist. The simulator doesn’t solve them as articulation. Every joint is unconstrained. The PPO policy converges to a degenerate solution that exploits this.

The fix:

from pxr import UsdPhysics, Usd

stage = Usd.Stage.Open("asset.usd")
root_prim = stage.GetPrimAtPath("/Object")
UsdPhysics.ArticulationRootAPI.Apply(root_prim)
stage.Save()

We hardcoded this into run_repackage_asset.py after the third time. The qualification gauntlet’s T11 catches it now.

The most insidious failures are the ones that look like training problems. This one is a USD problem dressed as a training problem.

Gotcha 2 — kinematicEnabled=True inside an articulation is a crash, not a warning

The symptom: Isaac Lab crashes on environment construction with a message about a “kinematic body inside an articulation.”

The cause: when you author a body that shouldn’t be simulated dynamically (an immovable cabinet body that the drawer attaches to, for example) the natural USD authoring is kinematicEnabled=True on that body. This is correct for standalone rigid-body sims. It is invalid inside an articulation.

The fix: don’t set kinematicEnabled on articulation children. Instead, set fixedBase=True on the articulation root. Same physical effect, different USD authoring.

# Wrong (inside an articulation):
body_prim.GetAttribute("kinematicEnabled").Set(True)

# Right (on the articulation root):
articulation_root.GetAttribute("fixedBase").Set(True)

We caught this twice. Once on the drawer cabinet body. Once on the human-demo replay path, where anchor_to_robot.py was generating a replay config with kinematicEnabled=True and feeding it into a Franka articulation. The script now strips kinematic flags when generating articulated configs.

Gotcha 3 — Cross-articulation PhysicsFixedJoint needs excludeFromArticulation=True

The symptom: your magnetic-grasp implementation works for two seconds and then explodes in numerical instability. The Franka jitters. The gripper detaches. The simulator prints a constraint-violation warning every step.

The cause: a PhysicsFixedJoint that connects two articulations (the gripper articulation and the drawer articulation, in our case) is, by default, treated as part of the larger constraint graph. The two articulations’ solvers fight over it.

The fix: author the fixed joint with excludeFromArticulation=True and enableProjection=True. The joint becomes a stabilized constraint outside the articulation solver. We added this to run_task.py’s magnetic-grasp authoring path.

fixed_joint = UsdPhysics.FixedJoint.Define(stage, "/MagneticGrasp")
fixed_joint.GetExcludeFromArticulationAttr().Set(True)
fixed_joint.GetCreateProjectionAttr().Set(True)

The magnetic grasp is a labelled cheat. We label it as such on the task DNA record. We still have to author it correctly.

The result, when authored correctly, is deterministic: 234.5 mm pull, 3 of 3 runs.

Gotcha 4 — Set joint properties after my_world.reset(), not before

The symptom: you author joint static friction in your USD. You launch Isaac Lab. The drawer free-slides under contact as if the friction value were ignored. You read it back from the simulator. The value is zero.

The cause: my_world.reset() re-initializes physics state from the USD authored values, then the simulator’s runtime overrides several joint properties from defaults. Joint static friction is one of them. Your USD-authored value is overwritten on reset.

The fix: set joint properties after my_world.reset(). Specifically, in our pipeline, we now have a physics_apply.py step that:

  • Calls my_world.reset().
  • Iterates the articulation joints.
  • Re-applies static friction, joint friction, joint limits, and any per-joint damping from physics_calibration.json.

This fix is per-runtime. The USD is correct. The Isaac Lab runtime needs the post-reset re-application. Other simulators may not.

Three smaller gotchas worth knowing

  • PyTorch 2.6+ and torch.load. PyTorch 2.6 changed the default of weights_only to True. Some checkpoint formats, including some skrl checkpoints we use, fail to deserialize under the new default. Set TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1. We learned this the hard way after a clean upgrade broke every saved policy.
  • Gf.Vec3f doesn’t accept numpy.float32. It silently coerces to a zero vector on some platforms. Convert with explicit Python float() calls when authoring USD attributes.
  • pymeshlab needs libopengl0. Without it, the import succeeds but the first mesh operation throws a libOpenGL.so.0 not-found error. Install libopengl0 apt package on Ubuntu 22.04.

Why we’re writing this down

These gotchas are not interesting. They are the kind of small, undocumented friction that slows every team independently. Every robotics team we’ve spoken to has hit at least two of them. Most have hit three. None of us write them up because we’re too busy writing up the policies they enabled.

We added every one of these to the qualification gauntlet so that no future bundle ships in a state that triggers them. The gauntlet is, in part, an institutional memory of every Isaac Lab gotcha we’ve hit.

The qualification gauntlet is the place where lessons learned become tests run.

What we’re still unsure about

  • PhysX vs MuJoCo MJX divergence. The same USD asset behaves slightly differently between Isaac Lab’s PhysX backend and MJX-based runtimes. We haven’t yet quantified this.
  • Fabric sync lag. Isaac Sim 5.1 reads bbox updates one or two physics steps behind PhysX’s actual state. We work around it with slow drives (300+ steps) or by reading from RigidPrimView directly. The qualification suite uses the slow-drive workaround.
  • frictionCombineMode=max interaction with friction-coefficient extremes. We default to max because it stabilizes drawer-handle grip. We haven’t yet stress-tested it on every contact-rich scenario.

The Isaac Lab maintainers are excellent and responsive. None of these are complaints. They are notes from the field.

QUALIFICATION LOG — USD AUTHORING1Body type must be "Articulation", not "Rigid Body"2Joint drives need explicit stiffness + damping3Mass must sit on the child prim, not the joint4Collision mesh must exist on every articulated link

Next: Capturing failure as a first-class artifact: what happens when you treat refinement as a function of structured signals instead of random jitter.