Skip to main content

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 joint suffixes are stripped (panda_joint1..7 becomes one family, wrist_1_joint..wrist_3_joint another), 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. Every joint_names_expr is verified with re.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_pos is the midpoint of the position limits, or 0 when 0 lies within them.
  • Field names are those of Isaac Lab's develop branch as of October 2026 (joint_effort_limit, joint_velocity_limit, PhysxArticulationCfg, fix_base on UrdfFileCfg). Isaac Lab 2.x used effort_limit_sim, velocity_limit_sim and sim_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 scale and 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:maxJointVelocity is read by name and assumed to be in deg/s for revolute joints (PhysX schema documentation); mimic joints are detected from an applied PhysxMimicJointAPI schema. 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)

Source distribution for isaaclab-robot-doctor 0.1.0
File Size Uploaded Provenance
isaaclab_robot_doctor-0.1.0.tar.gz 39.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for isaaclab-robot-doctor 0.1.0
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page