isaaclab-robot-doctor
One command that looks at your robot description (URDF or USD) and tells you what will go wrong
when you import it into NVIDIA Isaac Lab, then writes a
starting ArticulationCfg for it. It runs on a laptop: no GPU, no Isaac Sim, no mesh files opened.
isaaclab-robot-doctor check robot.urdf # findings grouped ERROR / WARN / INFO, exit 1 on any ERROR
isaaclab-robot-doctor gen-cfg robot.urdf --name robot # Python source for an ArticulationCfg
It exists because the same questions keep coming up when people bring their own robot to Isaac Lab: masses and inertias that PhysX rejects, descriptions in millimetres, joints with no effort limit, mimic joints, kinematic loops, a base that should have been fixed, joint indices that do not match the file order, and "what PD gains do I start with?" (Isaac Lab issues #2783, #4918, #4129).
Install
pip install 'isaaclab-robot-doctor[usd]'
Until the first PyPI release lands, install from GitHub instead:
pip install 'isaaclab-robot-doctor[usd] @ git+https://github.com/Lynn-hh/isaaclab-robot-doctor'
The [usd] extra pulls in usd-core (Pixar's USD wheel) for .usd / .usda / .usdc input.
Without it the tool still reads URDF. Python 3.10 or newer; the only hard dependency is numpy.
check
$ isaaclab-robot-doctor check fr3_franka_hand.urdf
fr3_franka_hand.urdf: robot 'fr3' (URDF): 14 links, 13 joints (9 movable; 7 revolute, 4 fixed, 2 prismatic)
ERROR (0)
WARN (1)
joint.mimic [fr3_finger_joint2]: joint 'fr3_finger_joint2' mimics 'fr3_finger_joint1' (multiplier 1, offset 0)
fix: the follower stays a joint of its own in Isaac Lab; the shipped Franka/Robotiq cfgs give it a zero-gain ImplicitActuatorCfg group and rely on a mimic constraint authored in the USD - check your converted asset for that constraint (URDF importer 3.0 dropped the convert_mimic_joints_to_normal_joints option)
INFO (6)
mass.massless_fixed_link [base]: link 'base' has no mass and is a root whose joints are all fixed
fix: fine with merge_fixed_joints=True (the UrdfConverterCfg default merges fixed-joint children into their parent); with merge_fixed_joints=False it would become a massless rigid body - check
mass.massless_fixed_link [fr3_link8]: link 'fr3_link8' has no mass and is attached to its parent by a fixed joint
fix: fine with merge_fixed_joints=True (the UrdfConverterCfg default merges fixed-joint children into their parent); with merge_fixed_joints=False it would become a massless rigid body - check
mass.massless_fixed_link [fr3_hand_tcp]: link 'fr3_hand_tcp' has no mass and is attached to its parent by a fixed joint
fix: fine with merge_fixed_joints=True (the UrdfConverterCfg default merges fixed-joint children into their parent); with merge_fixed_joints=False it would become a massless rigid body - check
tree.no_collision: 3 link(s) have no collision geometry: base, fr3_link8, fr3_hand_tcp
fix: they cannot make contact; fine for internal frames, otherwise add <collision> (or set UrdfConverterCfg.collision_from_visuals=True to reuse the visual meshes)
base.floating [base]: root link 'base' is not attached to a world link (floating base)
fix: manipulator: pass fix_base=True (UrdfFileCfg) or fix_root_link=True (UsdFileCfg); legged/mobile robot: leave the base free and spawn it above the ground with init_state.pos
order.joints: breadth-first joint order from 'base': fr3_joint1, fr3_joint2, fr3_joint3, fr3_joint4, fr3_joint5, fr3_joint6, fr3_joint7, fr3_finger_joint1, fr3_finger_joint2 - same as the file order
fix: Isaac Lab docs: the physics joint order is breadth-first over the kinematic tree (siblings in authored joint order), not the file order; read Articulation.joint_names at runtime and address joints with joint_names_expr regexes / find_joints, never by index
0 errors, 1 warnings, 6 infos -> OK
(fr3_franka_hand.urdf is the Franka FR3 description from
franka_description, as redistributed in
newton's franka_emika_panda asset.)
--json prints the same findings as JSON (id, severity, message, hint, subject) plus a
summary; --no-info hides the INFO group. Exit codes: 0 clean, 1 at least one ERROR, 2 the file
could not be read.
gen-cfg
$ isaaclab-robot-doctor gen-cfg fr3_franka_hand.urdf --name fr3 --fixed-base
"""ArticulationCfg for fr3, generated by isaaclab-robot-doctor 0.1.0 from fr3_franka_hand.urdf.
Field names follow Isaac Lab's develop branch (October 2026). Starting PD gains use
stiffness = 1.1 x joint_effort_limit and damping = 0.04 x stiffness, the medians
over the ImplicitActuatorCfg entries shipped in isaaclab_assets. They are starting points:
tune them with a step-response test on your robot before training anything.
"""
from isaaclab_newton.sim.schemas import NewtonArticulationCfg
from isaaclab_physx.sim.schemas import PhysxArticulationCfg, PhysxRigidBodyCfg
import isaaclab.sim as sim_utils
from isaaclab.actuators import ImplicitActuatorCfg
from isaaclab.assets.articulation import ArticulationCfg
FR3_CFG = ArticulationCfg(
# set prim_path where the cfg is used, e.g. FR3_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")
spawn=sim_utils.UrdfFileCfg(
asset_path="/path/to/fr3_franka_hand.urdf",
fix_base=True,
merge_fixed_joints=True,
rigid_props=PhysxRigidBodyCfg(disable_gravity=False, max_depenetration_velocity=5.0),
articulation_props=[
PhysxArticulationCfg(
enabled_self_collisions=False, solver_position_iteration_count=8, solver_velocity_iteration_count=0
),
NewtonArticulationCfg(self_collision_enabled=False),
],
),
init_state=ArticulationCfg.InitialStateCfg(
joint_pos={
"fr3_joint1": 0.0,
"fr3_joint2": 0.0,
"fr3_joint3": 0.0,
"fr3_joint4": -1.597,
"fr3_joint5": 0.0,
"fr3_joint6": 2.531,
"fr3_joint7": 0.0,
"fr3_finger_joint1": 0.0,
"fr3_finger_joint2": 0.0,
},
),
actuators={
"arm": ImplicitActuatorCfg(
joint_names_expr=["fr3_joint[1-7]"],
joint_effort_limit={"fr3_joint[1-4]": 87.0, "fr3_joint[5-7]": 12.0},
joint_velocity_limit={"fr3_joint[1-4]": 2.62, "fr3_joint(5|7)": 5.26, "fr3_joint6": 4.18},
stiffness={"fr3_joint[1-4]": 95.7, "fr3_joint[5-7]": 13.2},
damping={"fr3_joint[1-4]": 3.83, "fr3_joint[5-7]": 0.528},
),
"gripper": ImplicitActuatorCfg(
joint_names_expr=["fr3_finger_joint1"],
joint_effort_limit=100.0,
joint_velocity_limit=0.2,
stiffness=110.0,
damping=4.4,
),
"fr3_finger_joint2_passive": ImplicitActuatorCfg(
joint_names_expr=["fr3_finger_joint2"],
joint_effort_limit=100.0,
joint_velocity_limit=0.2,
# mimic follower of 'fr3_finger_joint1': zero gains, as in Isaac Lab's shipped Franka cfg;
# if the converted USD has no mimic constraint, give it the leader's gains instead
stiffness=0.0,
damping=0.0,
),
},
soft_joint_pos_limit_factor=1.0,
)
"""Configuration of fr3 (9 actuated joints)."""
The output above is verbatim except for asset_path, which is the absolute path of the input file.
Options: --usd-path PATH spawns from an already converted USD (UsdFileCfg) instead of the input
file; --fixed-base / --floating-base override what the file says (a URDF without a world
link is assumed floating, and the generated code says so); -o FILE writes to a file.
What the generator does:
- Actuator groups come from joint-name families: trailing indices and
jointsuffixes are stripped (panda_joint1..7becomes one family,wrist_1_joint..wrist_3_jointanother), singletons that share a last or first token are merged (FL_hip_joint+FR_hip_joint->.*_hip_joint;shoulder_pan_joint+shoulder_lift_joint->shoulder_.*), revolute and prismatic joints never share a group, and mimic followers get their own zero-gain group. Everyjoint_names_expris verified withre.fullmatch(what Isaac Lab uses) to match each joint exactly once; when no clean pattern exists the joint names are listed explicitly. - Limits are copied from the description. When they differ inside a group the field becomes a
dict keyed by sub-patterns, the style used in Isaac Lab's own Franka config. Joints without a
limit are left out of the dict and marked with a
TODO. joint_posis the midpoint of the position limits, or 0 when 0 lies within them.- Field names are those of Isaac Lab's
developbranch as of October 2026 (joint_effort_limit,joint_velocity_limit,PhysxArticulationCfg,fix_baseonUrdfFileCfg). Isaac Lab 2.x usedeffort_limit_sim,velocity_limit_simandsim_utils.ArticulationRootPropertiesCfg(fix_root_link=...); rename by hand if you are on 2.x.
The gain rule, and where it comes from
stiffness = 1.1 x joint_effort_limit
damping = 0.04 x stiffness
These are not derived from physics. They are the medians over every ImplicitActuatorCfg in
Isaac Lab's shipped robot configs (source/isaaclab_assets/isaaclab_assets/robots/*.py, develop,
October 2026), read with a small AST script:
| ratio | n | median | 1st-3rd quartile |
|---|---|---|---|
| stiffness / joint_effort_limit | 106 | 1.125 | 0.5 - 10 |
| damping / stiffness | 104 | 0.038 | 0.022 - 0.057 |
The quartiles tell the real story: the shipped configs span two orders of magnitude for the same ratio (a UR10 arm uses 9.2, Unitree legs use 0.5-0.7, a gripper may use 0.01). The medians give a PD controller that is neither floppy nor explosive on a typical arm, which is all a starting point can promise. Run a step-response test and tune from there; the generated file says so in its docstring.
What check looks at
| id | severity | what it catches |
|---|---|---|
urdf.xacro |
ERROR | unexpanded xacro (${...} or <xacro:...> outside comments) |
names.duplicate, names.empty |
ERROR | duplicate or empty link/joint names |
names.invalid_prim_name |
WARN | names that are not valid USD prim names (spaces, dots, dashes, leading digit); the converted USD will rename them |
tree.unknown_link |
ERROR | joint references a link that does not exist |
tree.kinematic_loop, tree.multiple_parents, tree.no_root |
ERROR | the joint graph is not a tree (PhysX articulations must be trees) |
tree.multiple_roots |
ERROR | more than one root link |
mass.missing |
ERROR | a link that moves on its own has no mass or mass <= 0 |
mass.tiny |
WARN | mass below 1 g (large mass ratios destabilise PhysX articulations) |
mass.massless_fixed_link |
INFO | massless link attached by a fixed joint; merged away by merge_fixed_joints=True |
mass.from_density |
WARN / INFO | USD body without explicit mass; PhysX computes it from collider volume x density |
inertia.missing |
WARN | mass given but no inertia tensor |
inertia.not_symmetric, inertia.not_positive_definite |
ERROR | invalid inertia tensor |
inertia.triangle_inequality |
WARN | principal moments violate Ia + Ib >= Ic (not physically realisable) |
joint.limit_missing |
ERROR | revolute/prismatic joint without <limit> |
joint.limits_inverted |
ERROR | lower > upper |
joint.zero_range |
WARN | lower == upper (including URDF's default of 0 when both are omitted) |
joint.effort_missing, joint.velocity_missing |
WARN | no effort / velocity limit to feed the actuator cfg |
joint.prismatic_range |
WARN | prismatic travel above 5 m (usually millimetres) |
joint.continuous |
INFO | continuous joint: unlimited in UsdPhysics, so wrap angles in observations |
joint.mimic, joint.mimic_unknown |
WARN / ERROR | mimic joints (see the hint above) |
joint.unsupported_type |
WARN / INFO | floating, planar, spherical, distance or D6 joints, which this tool does not model |
tree.no_collision |
INFO | links without collision geometry |
units.primitive_size, units.joint_origin |
WARN | primitive shape over 5 m or under 1 mm, or a joint origin over 5 m from its parent: looks like millimetres |
units.mesh_scale_mm, units.mesh_scale |
INFO / WARN | mesh scaled by 0.001 (fine) or by an unusual factor |
units.stage_meters_per_unit |
WARN | USD stage not in metres |
base.fixed, base.floating |
INFO | whether the root is attached to the world, with the matching fix_base / fix_root_link hint |
order.joints |
INFO | the breadth-first joint order Isaac Lab will expose, and whether it differs from the file order |
usd.no_articulation_root, usd.multiple_articulation_roots, usd.no_rigid_bodies |
ERROR / WARN | USD files missing the schemas ArticulationCfg needs |
usd.joint_no_drive |
INFO | USD joints without UsdPhysics.DriveAPI |
Every hint that describes Isaac Lab behaviour was checked against the Isaac Lab source or docs
(develop, October 2026). Where the behaviour of the Isaac Sim URDF importer could not be verified
from the Isaac Lab code, the hint says "check" rather than asserting.
Limitations
- Mesh files are never opened, so unit checks rely on primitive shapes, mesh
scaleand joint origins. A robot described purely with unscaled meshes in millimetres is only caught through its joint origins. - The USD reader reads UsdPhysics schemas only.
physxJoint:maxJointVelocityis read by name and assumed to be in deg/s for revolute joints (PhysX schema documentation); mimic joints are detected from an appliedPhysxMimicJointAPIschema. The inertia check uses only the principal moments, not the principal axes. - Nothing is simulated. The gains are a table lookup, the joint order is the breadth-first order the Isaac Lab docs describe (OVPhysX orders siblings by joint type then path, which can differ).
- MJCF input is not supported yet.
Roadmap
- GPU step-response PD tuning: spawn the generated config in Isaac Sim, apply a position step to each actuator group and fit stiffness/damping to a target rise time and overshoot.
- MJCF input.
- A flag to emit Isaac Lab 2.x field names.
Development
git clone https://github.com/Lynn-hh/isaaclab-robot-doctor
cd isaaclab-robot-doctor
pip install -e '.[usd,dev]'
ruff check . && ruff format --check . && pytest
The FR3 smoke test is skipped unless FR3_URDF points at fr3_franka_hand.urdf (or newton's
asset cache is present). The hand-written URDF/USDA fixtures live in tests/assets/.
Not affiliated with NVIDIA
This is an independent tool. Isaac Lab, Isaac Sim and PhysX are NVIDIA products; this project is not endorsed by or associated with NVIDIA. Field names and behaviour were checked against the public Isaac Lab repository and may lag behind it.
License
Apache-2.0. Copyright 2026 Lin He.
Metadata
Release files for isaaclab-robot-doctor 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| isaaclab_robot_doctor-0.1.0.tar.gz | 39.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| isaaclab_robot_doctor-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 74.0 kB
Release files / isaaclab_robot_doctor-0.1.0.tar.gz
| Download URL | isaaclab_robot_doctor-0.1.0.tar.gz |
|---|---|
| Size | 39.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bf4771e1bc2c0f12b00ee39f9370a4706d99fc8b61b3c797d4654e92aa9612fd
|
|
BLAKE2b-256 checksum How to use checksums |
884205b75f239c7bcb03602b63d7eba8e928693aaecf5c74f00ad3d5b8a3cc99
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / isaaclab_robot_doctor-0.1.0-py3-none-any.whl
| Download URL | isaaclab_robot_doctor-0.1.0-py3-none-any.whl |
|---|---|
| Size | 35.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
45facff9901a0a179fbeafd27c36bc0d9ff89fca70644c27978f70af64d4055b
|
|
BLAKE2b-256 checksum How to use checksums |
dbe676489656bede8518babfb84a8f1eb936d0a5e0ba389bd2458bc8358a4bfb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logSecurity for isaaclab-robot-doctor 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for isaaclab_robot_doctor-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.