Skip to main content

urdf_validator

A physics-aware URDF validation tool for the ROS 2 community.

The problem

check_urdf, the only official ROS 2 validation tool, checks syntax only. A URDF that passes check_urdf can still silently fail in any physics-based simulator — collapsing robots, undersized motors, unstable configurations. urdf_validator catches this entire class of errors before you ever launch a simulation.

The tool is designed to work on any URDF, not just a curated set of reference robots. When the link-name heuristics cannot reliably classify your robot, you can declare what the tool cannot infer — robot type, ground-contact links, arm chain boundaries — and the heuristics continue running as a cross-check rather than the sole decision-maker.

What it checks

Phase What it analyses
Schema Broken joint references, kinematic loops, duplicate names, zero/missing inertia, non-positive-definite inertia, inverted joint limits, missing effort/velocity limits, missing mesh files
Statics Full-body centre of mass, gravity torque per actuated joint, motor effort margins (PASS / WARN / FAIL), weakest joint identification
Stability Support polygon from wheel/caster contacts, COM-over-polygon containment, signed margin in mm, tip direction, COM height ratio, tipping angle
Workspace Monte Carlo FK reach envelope (max / vertical / horizontal), task-specific reachability, COM stability during reach
Remedies For every FAIL/WARN with a closed-form inverse: the value that would make it pass — minimum effort, maximum payload, link-length change, contact offset, required reach — reported as a target / gap triad, never higher-confidence than the forward computation it derives from, never an invented number
Comparison --compare-to prior.json diffs two runs: status transitions, margin deltas, and how much of the previous gap each change closed
User overrides --robot-type, --contact-links, --arm-root/--arm-tip to declare what heuristics cannot reliably infer; --override / --override-file to patch masses, inertias, and joint limits without editing the URDF. Declared values are labeled exact and heuristics run as a cross-check
Deep (optional) MuJoCo simulation cross-validation of gravity torques and COM; SIMULATED confidence badge

Installation

pip install urdf-validator

Optional extras:

pip install "urdf-validator[xacro]"   # adds xacro preprocessing
pip install "urdf-validator[mujoco]"  # adds MuJoCo deep-validation mode
pip install "urdf-validator[mcp]"     # adds the MCP server for AI agents
pip install "urdf-validator[mcap]"    # adds MCAP reading for the telemetry client (CSV needs no extra)
pip install "urdf-validator[full]"    # adds xacro, mujoco, and mcap (not mcp — install that separately if needed)

No ROS installation required.

Quick start

urdf_validate my_robot.urdf

Writes my_robot_validation.json alongside the URDF. Exits non-zero on any WARN or FAIL finding — directly usable as a CI gate.

CLI reference

urdf_validate <urdf_file> [options]
Flag Values Default Description
--pose zero | limits | custom | home zero Joint configuration for statics/stability/workspace. limits sets each joint to its declared upper limit (worst-case torques). custom requires --joint-angles. home warns and falls back to zero (no URDF standard for home configs).
--joint-angles ANGLES "j1=0.5,j2=1.2" — Joint angles for --pose custom in radians/metres.
--task TASK pick_from_ground | pick_from_table | push_button | custom — Task reachability check. Reports whether the arm can reach the target height and whether the COM remains stable during reach.
--height M float — Target height in metres. Required with --task custom.
--output-dir DIR path alongside input Directory for the JSON validation report.
--deep flag off Run MuJoCo simulation pass to cross-validate gravity torques and COM. Auto-triggers when stability margin is negative. Requires pip install mujoco.
--robot-type TYPE wheeled | ground_vehicle | legged | humanoid | arm_only | aerial | unknown — Declare the robot category explicitly. The link-name heuristic still runs as a cross-check; a mismatch is reported as a warning but does not override this declaration.
--contact-links LINKS "l1,l2,l3" — Comma-separated list of ground-contact link names. Bypasses the geometry heuristic for stability polygon construction; useful for legged robots or non-standard wheel naming.
--arm-root LINK link name — Root link of the arm chain. Bypasses the DOF-heuristic arm detection. Must be used together with --arm-tip.
--arm-tip LINK link name — End-effector link of the arm chain. Bypasses the DOF-heuristic arm detection. Must be used together with --arm-root.
--payload-mass KG float — Payload mass carried at the end-effector (or --payload-link). Gravity torques are recomputed with the load included.
--payload-link LINK link name detected EE Link the payload is attached to. Requires --payload-mass.
--compare-to PRIOR_JSON path — Diff this run against a prior JSON report. The comparison is printed alongside the normal report, not instead of it.
--override SPEC "link3.mass=2.0,joint2.effort=50" — Safe-scalar override(s), target.field=value comma-separated. Bare payload_mass / payload_link are task-level. Geometric/unknown fields are rejected; application is all-or-nothing.
--override-file OVERRIDES_JSON path — JSON file of overrides ({"overrides": [{"target", "field", "value"}]}); the only way to supply a full 6-element inertia tensor. Combinable with --override.

Exit codes

Code Meaning
0 PASS — all checks passed
1 WARN — no failures, at least one warning
2 FAIL or UNKNOWN — at least one check failed or a critical check could not run

Output — reference robot examples

The acceptance standard is correct, non-crashing output on six well-known public robots plus two capability-profile examples added in v0.7. Output below is captured directly from the tool; ANSI colours are stripped.


TurtleBot3 — differential-drive mobile robot

╔════════════════════════════════════════════════════╗
║  urdf_validate — TurtleBot3.urdf                   ║
╚════════════════════════════════════════════════════╝
[SCHEMA]  ✓ PASS (6 infos)
  [INFO]     Joint 'wheel_left_joint' (continuous) has no effort or velocity limit declared
  [INFO]     Joint 'wheel_right_joint' (continuous) has no effort or velocity limit declared
  [INFO]     Link 'base_link': mesh 'package://turtlebot3_description/meshes/bases/burger_base.stl' not found — package:// resolution requires ROS workspace to be sourced
  [INFO]     Link 'wheel_left_link': mesh 'package://turtlebot3_description/meshes/wheels/left_tire.stl' not found — package:// resolution requires ROS workspace to be sourced
  [INFO]     Link 'wheel_right_link': mesh 'package://turtlebot3_description/meshes/wheels/right_tire.stl' not found — package:// resolution requires ROS workspace to be sourced
  [INFO]     Link 'base_scan': mesh 'package://turtlebot3_description/meshes/sensors/lds.stl' not found — package:// resolution requires ROS workspace to be sourced
[PHYSICS]  7 links — mass: 5 exact, 2 missing · inertia: 5 exact, 2 missing
[STATICS]  COM [-0.004, 0.000, 0.031] m  height 0.031 m  total mass 1.002 kg  (estimated)
           Heaviest: base_link (82.4%)
[STATICS]  joints: PASS
[STABILITY]  ✓ STABLE  margin 4.0 mm
             COM height ratio 0.96 — stable  tips at 27.4°
[WORKSPACE]  UNKNOWN — No arm chain detected (robot may be wheeled or legged only)
[OVERALL]  PASS  confidence: MEDIUM
Full report: TurtleBot3_validation.json

Exit 0.


Fetch — wheeled mobile manipulator

╔════════════════════════════════════════════════════╗
║  urdf_validate — fetch.urdf                        ║
╚════════════════════════════════════════════════════╝
[SCHEMA]  ⚠ WARN — 42 issues
  [WARN]     Link 'r_gripper_finger_link' has non-positive-definite inertia tensor (min eigenvalue: 0.000000) — physically impossible
  [WARN]     Link 'l_gripper_finger_link' has non-positive-definite inertia tensor (min eigenvalue: 0.000000) — physically impossible
  [INFO]     (40 mesh resolution notices — package:// resolution requires ROS workspace)
[PHYSICS]  28 links — mass: 22 exact, 6 missing · inertia: 22 exact, 6 missing
[STATICS]  COM [0.045, 0.001, 0.260] m  height 0.260 m  total mass 121.538 kg  (estimated)
           Heaviest: base_link (57.7%)
[STATICS]  joints: PASS  weakest: torso_lift_joint
[STABILITY]  ✓ STABLE  margin 43.7 mm
             COM height ratio 0.69 — stable  tips at 35.8°
[WORKSPACE]  max reach 2.182 m  vertical 2.158 m  horizontal 1.165 m  (estimated)
             from base 2.182 m
[OVERALL]  WARN  confidence: MEDIUM
Full report: fetch_validation.json

Exit 1. The two gripper finger links have singular inertia tensors in the upstream URDF — a known issue with this file. All statics and stability checks proceed and pass.


╔════════════════════════════════════════════════════╗
║  urdf_validate — PR2.urdf                          ║
╚════════════════════════════════════════════════════╝
[SCHEMA]  ⚠ WARN — 88 issues
  [WARN]     Link 'r_gripper_l_finger_tip_frame' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'l_gripper_l_finger_tip_frame' has no inertial block (mass unknown) — will cause physics engine instability
  [INFO]     Joint 'torso_lift_motor_screw_joint' (continuous) has no effort or velocity limit declared
  [INFO]     Joint 'r_gripper_motor_screw_joint' (continuous) has no effort or velocity limit declared
  [INFO]     Joint 'l_gripper_motor_screw_joint' (continuous) has no effort or velocity limit declared
  [INFO]     Robot has 88 links (>50) — may be slow to simulate or validate
  [INFO]     (82 mesh resolution notices — package:// resolution requires ROS workspace)
[PHYSICS]  88 links — mass: 73 exact, 15 missing · inertia: 73 exact, 15 missing
[STATICS]  COM [-0.016, 0.005, 0.477] m  height 0.477 m  total mass 265.732 kg  (estimated)
           Heaviest: base_link (43.7%)
[STATICS]  joints: FAIL  weakest: l_shoulder_lift_joint
[STABILITY]  ✓ STABLE  margin 208.5 mm
             COM height ratio 1.06 — manageable  tips at 25.2°
[WORKSPACE]  max reach 1.887 m  vertical 1.777 m  horizontal 1.177 m  (estimated)
             from base 1.931 m
[OVERALL]  FAIL  confidence: MEDIUM
Full report: PR2_validation.json

Exit 2. The shoulder lift joints on both arms are undersized: the URDF declares 30 Nm but the gravity torque at zero pose requires ~49 Nm. The real PR2 compensates with passive counterbalance springs not modelled in the URDF.


ANYmal — legged quadruped

╔════════════════════════════════════════════════════╗
║  urdf_validate — ANYmal.urdf                       ║
╚════════════════════════════════════════════════════╝
[SCHEMA]  ✓ PASS (17 infos)
  [INFO]     (17 mesh resolution notices — package:// resolution requires ROS workspace)
[PHYSICS]  22 links — mass: 17 exact, 5 missing · inertia: 17 exact, 5 missing
[STATICS]  COM [-0.001, -0.001, -0.034] m  height -0.034 m  total mass 30.421 kg  (estimated)
           Heaviest: base_inertia (55.2%)
[STATICS]  joints: PASS  weakest: RF_HAA
[STABILITY]  UNKNOWN — robot type 'quadruped' — stability only computed for wheeled robots
[WORKSPACE]  N/A — not applicable — robot type 'quadruped' has no manipulator
[OVERALL]  PASS  confidence: MEDIUM
Full report: ANYmal_validation.json

Exit 0. Stability is UNKNOWN for legged robots — support polygon computation requires declared foot contacts, which are not a standard URDF field. Use --contact-links to supply foot contact link names explicitly and get a real stability margin. Workspace is N/A — the capability profile for quadruped classifies this as a locomotion platform with no manipulator; v0.6 incorrectly reported leg reach as workspace reach.


Spot — legged quadruped (unofficial URDF, no masses)

╔════════════════════════════════════════════════════╗
║  urdf_validate — Spot.urdf                         ║
╚════════════════════════════════════════════════════╝
[SCHEMA]  ⚠ WARN — 37 issues
  [WARN]     Link 'fl.hip' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'fl.uleg' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'fl.lleg' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'fr.hip' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'fr.uleg' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'fr.lleg' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'hl.hip' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'hl.uleg' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'hl.lleg' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'hr.hip' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'hr.uleg' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'hr.lleg' has no inertial block (mass unknown) — will cause physics engine instability
  [INFO]     Link 'fl.hip' has visual geometry (mesh) but no collision geometry — will be invisible to physics engines
  [INFO]     (11 more visual-without-collision notices + 13 mesh resolution notices)
[PHYSICS]  13 links — mass: 0 exact, 13 missing · inertia: 0 exact, 13 missing
[STATICS]  COM unknown (missing)
[STATICS]  joints: PASS
[STABILITY]  UNKNOWN — robot type 'quadruped' — stability only computed for wheeled robots
[WORKSPACE]  N/A — not applicable — robot type 'quadruped' has no manipulator
[OVERALL]  WARN  confidence: LOW
Full report: Spot_validation.json

Exit 1. This unofficial URDF omits all link masses. The tool degrades gracefully: schema warns on every link, statics and stability correctly report missing data. Workspace is N/A — the capability profile for quadruped classifies this as a locomotion platform with no manipulator.


Franka Panda — fixed-base arm (no masses declared)

╔════════════════════════════════════════════════════╗
║  urdf_validate — Franka_Panda.urdf                 ║
╚════════════════════════════════════════════════════╝
[SCHEMA]  ⚠ WARN — 25 issues
  [WARN]     Link 'panda_base1' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'panda_base_arm' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'panda_link1' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'panda_link2' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'panda_link3' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'panda_link4' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'panda_link5' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'panda_link6' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'panda_link7' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'frankie_leftfinger' has no inertial block (mass unknown) — will cause physics engine instability
  [WARN]     Link 'frankie_rightfinger' has no inertial block (mass unknown) — will cause physics engine instability
  [INFO]     (2 visual-without-collision notices + 12 mesh resolution notices)
[PHYSICS]  15 links — mass: 0 exact, 15 missing · inertia: 0 exact, 15 missing
[STATICS]  COM unknown (missing)
[STATICS]  joints: PASS
[STABILITY]  UNKNOWN — robot type 'unknown' — stability only computed for wheeled robots
[WORKSPACE]  max reach 1.255 m  vertical 1.626 m  horizontal 1.062 m  (estimated)
             from base 1.643 m
[OVERALL]  WARN  confidence: LOW
Full report: Franka_Panda_validation.json

Exit 1. No masses declared in this public URDF variant — a common issue with arm-only files intended for kinematic use only. Workspace is still computed from joint limits alone.


ground_vehicle — wheeled vehicle without manipulator

╔════════════════════════════════════════════════════╗
║  urdf_validate — ground_vehicle.urdf               ║
╚════════════════════════════════════════════════════╝
[SCHEMA]  ✓ PASS
[PHYSICS]  ✓ 5 links — all mass & inertia declared
[STATICS]  COM [0.000, 0.000, -0.025] m  height -0.025 m  total mass 120.000 kg  (estimated)
           Heaviest: chassis (83.3%)
[STATICS]  joints: PASS
  j_wheel_fl                    req 0.0 Nm      declared 50.0 Nm         no margin  PASS  — OK — torque negligible
  j_wheel_fr                    req 0.0 Nm      declared 50.0 Nm         no margin  PASS  — OK — torque negligible
  j_wheel_rl                    req 0.0 Nm      declared 50.0 Nm         no margin  PASS  — OK — torque negligible
  j_wheel_rr                    req 0.0 Nm      declared 50.0 Nm         no margin  PASS  — OK — torque negligible
[STABILITY]  ✓ STABLE  margin 300.0 mm
[WORKSPACE]  N/A — not applicable — robot type 'ground_vehicle' has no manipulator
[WARN]  User declared --robot-type=ground_vehicle, but link-name heuristic suggests wheeled
[OVERALL]  PASS  confidence: HIGH
Full report: ground_vehicle_validation.json

Exit 0. Stability runs the wheel-contact heuristic (ground_vehicle profile: locomotion_model="wheeled"). Workspace is N/A — this robot category has no manipulator. --robot-type ground_vehicle was supplied explicitly. The heuristic mismatch warning (ground_vehicle vs wheeled) is expected: both share the same wheel-contact algorithm but ground_vehicle disables workspace checks.


aerial_drone — airborne robot (4 fixed rotors)

╔════════════════════════════════════════════════════╗
║  urdf_validate — aerial_drone.urdf                 ║
╚════════════════════════════════════════════════════╝
[SCHEMA]  ✓ PASS
[PHYSICS]  ✓ 5 links — all mass & inertia declared
[STATICS]  COM [0.000, 0.000, 0.000] m  height 0.000 m  total mass 1.900 kg  (estimated)
           Heaviest: body (78.9%)
[STABILITY]  N/A — not applicable — robot type 'aerial' has no ground contact
[WORKSPACE]  N/A — not applicable — robot type 'aerial' has no manipulator
[WARN]  User declared --robot-type=aerial, but link-name heuristic suggests unknown
[OVERALL]  PASS  confidence: HIGH
Full report: aerial_drone_validation.json

Exit 0. Both stability (no ground contact) and workspace (no manipulator) are N/A. --robot-type aerial was supplied explicitly. Schema and statics still run.


User-declared robot info

When heuristics cannot reliably classify your robot — non-English link names, non-standard wheel geometry, hexapod naming conventions — you can declare the information directly:

# Legged robot with known foot contact links
urdf_validate my_hexapod.urdf \
  --robot-type legged \
  --contact-links foot_fl,foot_fr,foot_ml,foot_mr,foot_rl,foot_rr

# Robot with a non-obvious arm chain
urdf_validate my_robot.urdf \
  --arm-root arm_base_link \
  --arm-tip tool_flange

Declared values are used directly and labeled exact in the JSON output. The corresponding heuristic still runs in the background: if it disagrees, a warning is added to the report naming the mismatch. The declared value always wins — the disagreement is surfaced, never silently resolved.

If you omit these flags, heuristic output is used and labeled estimated to make clear it is an unverified inference, not a verified value.

Capability profiles

The tool uses a built-in profile table to decide which checks apply to each robot category. Consult this table when the default output shows N/A or UNKNOWN and you want to understand why:

Robot type Stability check Workspace check
wheeled Runs (wheel-contact heuristic) Runs if arm chain detected
ground_vehicle Runs (wheel-contact heuristic) N/A — ground vehicles have no manipulator
arm_only N/A — fixed-base arm has no ground contact Runs
legged / quadruped UNKNOWN — foot contacts must be declared via --contact-links N/A — legged robots have no manipulator
humanoid UNKNOWN — foot-contact algorithm not yet implemented Runs if arm chain detected
aerial N/A — airborne robot has no ground contact N/A — aerial vehicles have no manipulator
unknown UNKNOWN — cannot determine contact geometry Runs

N/A means the check does not apply to this robot category and is excluded from the overall status derivation. UNKNOWN means the check was attempted but could not produce a result — either the required data is unavailable or the algorithm does not yet cover this case.

Use --robot-type to force the category. Without it, the link-name heuristic runs and the result is labeled estimated.

Payload-augmented statics

Pass --payload-mass <kg> to recompute gravity torques with a carried load included. The [STATICS] section shows the payload contribution and re-evaluates PASS/WARN/FAIL per joint:

urdf_validate fetch.urdf --payload-mass 5.0
urdf_validate fetch.urdf --payload-mass 5.0 --payload-link gripper_link

--payload-link sets which link the load is attached to. It defaults to the automatically-detected end-effector. payload_mass and payload_link are included in the JSON output.

Payload torque is computed as the cross-product of the moment arm (payload link world position relative to each joint) × gravity force. The same PASS/WARN/FAIL thresholds apply: PASS ≥ 1.5×, WARN 1.0–1.5×, FAIL < 1.0×.

Iterating on a design

Every FAIL/WARN that has a closed-form inverse already tells you the value that would make it pass — the -> target: line under the finding, e.g.:

[FAIL] shoulder_lift_joint — req 49.0 Nm, declared 30.0 Nm, margin 0.61
       -> target: >= 58.2 Nm effort, OR <= 1.4 kg payload, OR -20% link3 length

Multiple remedies are shown side by side, unranked — the tool computes the options, you choose.

Patch values without editing the URDF. Try a change directly from the command line:

urdf_validate my_arm.urdf --override "link3.mass=2.0,shoulder_lift_joint.effort=50"

# a full inertia tensor needs the file form (combinable with --override)
urdf_validate my_arm.urdf --override-file overrides.json
{
  "overrides": [
    {"target": "link3", "field": "mass", "value": 2.0},
    {"target": "link3", "field": "inertia", "value": [0.01, 0.0, 0.0, 0.01, 0.0, 0.02]},
    {"target": null, "field": "payload_mass", "value": 1.5}
  ]
}

An override-supplied value surfaces with exact confidence; it never upgrades the confidence of anything it did not set, and the report schema is unchanged. Geometric or unknown fields are rejected with one [ERROR] override rejected: ... line each, and application is all-or-nothing.

Diff two runs. Point a run at an earlier run's JSON to see what moved:

urdf_validate my_arm.urdf                     # writes my_arm_validation.json
cp my_arm_validation.json baseline.json
urdf_validate my_arm.urdf --override "link3.mass=2.0" --compare-to baseline.json

The comparison prints alongside the normal report: status transitions per check, the delta on the scalar each status derives from, and pct_of_gap_closed — how much of the previous run's gap the change actually closed.

CI integration

GitHub Actions

name: URDF validation

on:
  push:
    paths: ['**.urdf', '**.xacro']
  pull_request:
    paths: ['**.urdf', '**.xacro']

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install urdf-validator
        run: pip install "urdf-validator[full]"

      - name: Validate URDF
        run: urdf_validate robot/my_robot.urdf --output-dir /tmp/reports

      - name: Upload validation report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: urdf-validation-report
          path: /tmp/reports/*.json

The urdf_validate step exits non-zero on any WARN or FAIL finding, failing the CI job. Remove the paths: filter if you want to run on every push regardless of which files changed.

Validating multiple URDFs

      - name: Validate all URDFs
        run: |
          find . -name '*.urdf' | while read f; do
            echo "=== $f ==="
            urdf_validate "$f" || exit 1
          done

Allowing warnings but blocking failures

      - name: Validate URDF (block on FAIL only)
        run: |
          urdf_validate robot/my_robot.urdf
          code=$?
          if [ $code -eq 2 ]; then exit 1; fi

Exit code 1 (WARN) passes; only exit code 2 (FAIL / UNKNOWN) fails the job.


Output sections

[SCHEMA]

Structural checks. Severities:

Severity Example
CRITICAL Joint references a link that does not exist
CRITICAL Kinematic loop detected
WARN Non-positive-definite inertia tensor
WARN Missing <inertial> block on a non-fixed link
WARN Inverted joint limits (lower > upper)
INFO Missing effort or velocity limit
INFO Mesh file not found (package:// paths require a sourced ROS workspace)
INFO Robot has more than 50 links

[STATICS]

Computed at the declared --pose (default: zero pose).

  • COM — full-body centre of mass [x, y, z] in metres, total mass in kg.
  • Per joint — gravity torque required (req), declared effort limit (declared), margin = declared / req. PASS ≥ 1.5×, WARN 1.0–1.5×, FAIL < 1.0×.

[STABILITY]

Available for wheeled robots by default. Contact point detection runs three passes in priority order:

  1. Links named *wheel*
  2. Cylindrical links with a wheel-like radius-to-length ratio (r/L > 0.3)
  3. Links named *caster* with cylinder or sphere geometry

Use --contact-links to supply contact link names directly and bypass this heuristic entirely — required for legged robots and any configuration where link naming does not follow these conventions.

Reports signed margin in mm (positive = stable, negative = tipping), tip direction, COM height ratio, and tipping angle.

com_height_ratio_class Ratio Meaning
very_stable < 0.5 Passive tip resistance
stable 0.5 – 1.0 Normal for wheeled mobile robots
manageable 1.0 – 2.0 Typical humanoid standing
requires_active_balancing 2.0 – 3.0 Needs active balance control
will_fall > 3.0 Will fall without fast active control

[WORKSPACE]

Monte Carlo FK sampling over joint limits. The arm chain is detected automatically via a BFS + DOF heuristic. Use --arm-root / --arm-tip to specify the chain boundary explicitly when auto-detection picks the wrong chain (e.g. a gripper chain instead of the full arm, or an arm on a non-standard robot).

Computes max_reach, vertical_reach, horizontal_reach, and reach_from_base. With --task, also reports whether the arm can reach the target height and whether the COM stays over the support polygon during full extension.

[OVERALL]

Worst status across all sections. Confidence level:

Level Condition
HIGH All link masses and inertia tensors declared
MEDIUM ≥ 50% of link masses declared
LOW Sparse physics data

Confidence labels

Every physics estimate carries an explicit label:

Label Meaning
exact Value read directly from a declared URDF field, or supplied via a user override flag
estimated Derived from declared masses and geometry via analytical formula, or from a heuristic that ran without a user declaration to cross-check against
guessed Heuristic estimate (e.g. mesh geometry — no explicit dims available)
missing No data available
simulated Cross-validated against MuJoCo simulation (--deep mode)

The tool never presents an estimated value as ground truth.


JSON output

Every run writes <robot>_validation.json containing the full ValidationReport. The schema is stable across minor versions and documented in json_schema.md.

{
  "overall_status": "WARN",
  "confidence_level": "MEDIUM",
  "robot_type": "wheeled",
  "robot_type_confidence": "exact",
  "statics": {
    "full_body_com": [0.045, 0.001, 0.260],
    "total_mass": 121.538,
    "com_height_above_ground": 0.260,
    "weakest_joint_name": "torso_lift_joint",
    "status": "PASS"
  },
  "stability": {
    "stable": true,
    "margin_mm": 43.7,
    "com_height_ratio": 0.69,
    "com_height_ratio_class": "stable",
    "tipping_angle_deg": 35.8,
    "contact_confidence": "estimated",
    "status": "PASS"
  }
}

robot_type_confidence and contact_confidence are "exact" when the value was supplied via --robot-type or --contact-links, and "estimated" when derived from the heuristic alone.


Task-query API

For AI agents and programmatic callers, urdf_validator exposes a structured Python interface. It runs the full physics pipeline and returns per-sub-check results without spawning a subprocess:

from urdf_validator_main.api.task_runner import run_pick_task, run_pick_sweep
from urdf_validator_main.api.task_schema import TaskQueryRequest

req = TaskQueryRequest(
    urdf_path="fetch.urdf",
    task_type="pick",
    target_position=[0.5, 0.0, 0.8],    # [x, y, z] metres, robot frame
    target_orientation="top_down",        # or "side", [r,p,y], [qw,qx,qy,qz]
    object_mass_kg=0.5,
    terrain_angle_deg=0.0,
)
resp = run_pick_task(req)
print(resp.overall_status)               # "PASS" | "FAIL" | "UNKNOWN"
for check in resp.sub_checks:
    print(check.name, check.status, check.reason)

Sub-checks returned per request:

Sub-check Passes when
reach reach_from_base >= dist(target_position)
reach_orientation ≥ 5% of workspace samples satisfy target_orientation within tolerance
payload_strength all joint effort margins ≥ 1.0× with object_mass_kg included
stability_during_reach COM XY shift at maximum reach stays within the stability polygon margin
self_collision ≥ 95% of sampled arm poses are collision-free

To sweep over a list of parameter variations (different heights, payloads, target positions):

responses = run_pick_sweep([req1, req2, req3])
# Returns List[TaskQueryResponse], order preserved, failures isolated

Full schema: urdf_validator_main/api/task_schema.py and urdf_validator_main/api/task_runner.py. Documented in json_schema.md.


MCP server (for AI agents)

The same capability surface is available over the Model Context Protocol as a local, stdio-based server. It is a thin adapter: it adds no validation logic, and every number it returns is produced by the pipeline documented above.

pip install "urdf-validator[mcp]"
urdf_validate_mcp            # serves MCP over stdio; not meant to be run by hand

Without the extra installed, the command prints [ERROR] MCP support requires the 'mcp' extra: pip install "urdf-validator[mcp]" and exits 2. Everything else in the tool works exactly as before — the mcp dependency is imported lazily and is never needed by the CLI or the Python API.

Client configuration (the shape most MCP clients use):

{
  "mcpServers": {
    "urdf_validator": {
      "command": "urdf_validate_mcp",
      "args": []
    }
  }
}

Six tools are exposed:

Tool Arguments Returns
validate_urdf urdf_path, optional payload_mass, payload_link, robot_type The full validation report as JSON — the same object the CLI writes to <stem>_validation.json
run_pick_task TaskQueryRequest fields (urdf_path, target_position, target_orientation, object_mass_kg, terrain_angle_deg, overrides) One TaskQueryResponse
run_pick_sweep requests: array of the above A list of TaskQueryResponse, request order preserved
solve_target urdf_path, optional payload_mass, payload_link Only the reverse-solved targets per check, plus each check's status — a "what would it take?" query with no prior report needed
compare_reports report_a, report_b (report objects the caller supplies) A ComparisonResult: status transitions, deltas, per-lever percentage of the gap closed
apply_overrides urdf_path, overrides The full report for the patched robot — or a structured rejection listing every invalid override

Properties worth relying on:

  • Stateless. The server keeps no history, caches nothing, and writes no files — validate_urdf returns the report rather than exporting it. Pass every input on every call (that is why compare_reports takes both reports).
  • Never crashes. A bad path, a malformed URDF, or garbage arguments come back as a tool error result carrying a structured {"error": {"tool", "type", "message", "details"}} payload. The session stays usable.
  • Honest values. Confidence labels, null-with-reason targets, and the unranked list of levers reach the agent unchanged — the adapter never upgrades a confidence or drops a field.

Tool argument and result shapes are documented in json_schema.md.


Telemetry extraction client (urdf_telemetry)

A separate, local-only tool for teams recording real robot telemetry (joint position/velocity/effort) for their own use — for example, as input to a calibration workflow. It reads a rosbag2/MCAP or CSV joint-state recording and turns it into two compact, human-inspectable record types (StaticHoldRecord, TrajectoryWindowRecord) that summarize static holds and smooth dynamic-motion windows. It does not predict, compare, calibrate, or correct anything — it only extracts and packages already- recorded data, and it transmits nothing anywhere. There is no account, no network call, and no server this tool talks to; everything happens on your machine, the same trust model as the CLI itself.

urdf_telemetry extract --input recording.mcap --urdf my_robot.urdf \
    --out records.json --robot-id my_robot_001 --mode both

urdf_telemetry preview --records records.json --allow-upload
Subcommand Purpose
extract Read an MCAP or CSV joint-state recording, optionally convert motor-current effort to torque/force via an actuator sidecar, run automatic static-hold / dynamic-window segmentation, and write the resulting records to a JSON file.
preview Show exactly what a records file (and, only if explicitly opted in, named raw files) would contain if ever transmitted — the two-level, opt-in-by-default consent UX. Currently always a local no-op: nothing is transmitted under any flag combination, because no upload endpoint exists yet.

extract flags (see urdf_telemetry extract --help for the authoritative list):

Flag Default Description
--input required Path to a .mcap or .csv joint-state recording.
--urdf required The URDF this recording was taken against — binds every record to its exact file hash (urdf_hash); a calibration against a stale URDF is meaningless, so mismatched hashes are never silently mixed downstream.
--out required Path to write the extracted records JSON array to.
--robot-id "" Free-text robot identifier stamped on every record.
--mode both static-hold, trajectory, or both.
--topic — Restrict MCAP reading to one topic (ignored for CSV).
--velocity-threshold 0.01 rad/s (or m/s) below which a joint counts as "held".
--min-duration 0.5 Minimum seconds for a hold or window to be recorded.
--effort-source unknown torque_sensor | motor_current | unknown. motor_current requires --sidecar and degrades the record's effort_confidence to estimated — a datasheet torque constant is never exact.
--sidecar — Actuator sidecar file (JSON, or YAML if PyYAML is installed) describing torque_constant_nm_per_a, gear_ratio, and optionally gearbox_efficiency/rated_effort. Two illustrative example sidecars ship under urdf_validator_main/telemetry_client/sidecars/.

Full record-schema field tables (both record types, plus the sidecar format and segmentation method) live in docs/recording_protocol.md — explicitly marked DRAFT / v0: field names and shapes may still change before a stable v1 is published, unlike the core validator's json_schema.md contract.

Properties worth relying on, same as the rest of the tool:

  • Stateless and local-only. No network dependency, no account, no persistence beyond the files you tell it to write.
  • Never crashes. Malformed/truncated recordings, a missing sidecar field, or an unreadable URDF come back as a structured error on stderr and a non-zero exit code — never an unhandled traceback.
  • Honest confidence. Every record's effort_confidence uses the same fixed vocabulary as the rest of the tool (exact / estimated / guessed / missing / simulated); a field that cannot be computed ships as null alongside a *_reason string, never silently dropped.

Status

Version Month Status Delivered
v0.1 1 Complete Parser, schema checks, physics confidence labels, no-crash on all 6 reference URDFs
v0.2 2 Complete Chain walker, full-body COM, gravity torques, MuJoCo ground-truth validation (0% error)
v0.3 3 Complete Robot type detection, support polygon, COM projection stability check
v0.4 4 Complete Workspace FK, task reachability, full report pipeline, JSON export
v0.5 5 Complete Pose flags, geometry contact detection, COM height ratio, --deep MuJoCo wiring, JSON schema docs, performance (PR2: 12.5s → 4.1s)
v0.6 6 Complete --robot-type, --contact-links, --arm-root/--arm-tip user override flags; heuristic cross-check and mismatch warnings; exact vs estimated confidence labeling
v0.7 7 Complete Capability profiles (N/A vs UNKNOWN); --payload-mass payload-augmented statics
v0.8 8 Complete Orientation-aware reachability (position + orientation, not position alone)
v0.9 9 Complete Real-pose COM stability during reach; self-collision/clearance checks
v0.10 10 Complete Structured task-query API for AI agents and programmatic callers
v0.11 11 Complete Hardening — full regression across all 6 reference robots + capability-profile URDFs
v1.0 12 Complete Public release — ROS Discourse announcement, pip package
v1.1 13 Complete Reverse-solve & target-value layer — every invertible check reports the value that would make it pass (target / gap triad); declared-vs-derived inertia divergence
v1.2 14 Complete Delta & comparison layer — --compare-to diffs two runs (status transitions, margin deltas, pct_of_gap_closed)
v1.3 15 Complete Fast-iteration input layer — --override / --override-file patch scalars, inertia tensors, and joint limits without editing the URDF
v1.4 16 Complete Consolidated actionable report — every FAIL/WARN terminal line carries its remedy triad; inertia divergence surfaced in terminal output (presentation only, JSON unchanged)
v1.5 17 Complete Agent protocol exposure — urdf_validate_mcp MCP server, six tools, [mcp] extra
v1.5.1 — Complete Bugfix — humanoid/legged declared-type false-positive warning (F3)
v1.6.0 18 Complete Telemetry extraction client (telemetry_client, urdf_telemetry CLI) — MCAP/CSV joint-state extraction, automatic static-hold/dynamic-window segmentation, actuator sidecar motor-current-to-effort conversion, and the point-of-extraction consent preview UX. Local-only, stateless, transmits nothing — submit() is not yet wired to any transport. See docs/recording_protocol.md.

Dependencies

Core (pip install urdf-validator): urdf_parser_py, numpy, shapely, ikpy

Xacro (pip install "urdf-validator[xacro]"): adds xacro (.xacro / .xacro preprocessing)

MuJoCo (pip install "urdf-validator[mujoco]"): adds mujoco (--deep simulation mode)

MCP (pip install "urdf-validator[mcp]"): adds the mcp SDK (the urdf_validate_mcp stdio server)

MCAP (pip install "urdf-validator[mcap]"): adds mcap + mcap-ros2-support (MCAP joint-state reading for urdf_telemetry extract; CSV input needs no extra)

Full (pip install "urdf-validator[full]"): xacro, mujoco, and mcap (not mcp — install that separately if needed)

No ROS installation required.

Known limitations

Limitation Status
Humanoid foot-contact stability PENDING — stability is always UNKNOWN for humanoid robots. Supply foot positions with --contact-links to get a real margin.
Unknown-type contact fallback PENDING — for --robot-type unknown, stability is UNKNOWN. Lowest-link fallback is not yet implemented.
Mimic joints OPEN — mimic followers (common in parallel grippers) are treated as fixed. Joint angles and torques for mimic joints are not computed.
SDF format DEFERRED — only URDF (.urdf, .xacro) is supported.
--pose home Falls back silently to zero pose. URDF has no standard home-configuration field (that lives in SRDF, outside this tool's scope).
Per-arm workspace breakdown Aggregated — max_reach is the maximum across all detected arm chains, not per-arm. A --detailed flag for per-arm envelopes is a planned future feature.
--deep drop test PENDING — the 2-second MuJoCo drop test is not implemented. Only the static cross-validation pass runs under --deep.

Project direction

The urdf_validator CLI — its deterministic physics, confidence labels, never-crash and exit-code contracts, and reference-robot verification — is MIT and stays MIT.

The local telemetry extraction client (urdf_telemetry, above) is part of that same free core and stays MIT — it runs entirely on your machine and uploads nothing. Work aimed at teams rather than single URDFs is coming separately, as a commercial offering that funds ongoing maintenance of the free core: a hosted service to store and calibrate against your telemetry, a calibrated confidence tier, and a sim-to-real workflow mode. That hosted side does not exist yet; today, your URDFs and telemetry stay with you no matter what.

Teams wanting early access can reach out via email (m06robotic@gmail.com).

License

MIT

Metadata

Release files for urdf-validator 1.6.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 urdf-validator 1.6.0
File Size Uploaded
urdf_validator-1.6.0.tar.gz 256.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for urdf-validator 1.6.0
File Interpreter ABI Platform
urdf_validator-1.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 374.2 kB

Release files / urdf_validator-1.6.0.tar.gz

Download URL urdf_validator-1.6.0.tar.gz
Size 256.6 kB
Tags Source
SHA-256 checksum
How to use checksums
885ccb73ef90ad8f8014b87ffccb2a48842d7b04054330862640011b32230b44
BLAKE2b-256 checksum
How to use checksums
e087be1e308e8b77b52a75d99fb89b2efe8c0b3829793d885ca3ba17809ed5f0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.12

Release files / urdf_validator-1.6.0-py3-none-any.whl

Download URL urdf_validator-1.6.0-py3-none-any.whl
Size 117.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e73d37f689b23c7fe1f16108644dfb86324b7b92853cbfe015679f04bf83a02c
BLAKE2b-256 checksum
How to use checksums
3ca3858e72548d16825dfbaa5a62ec90e9f74655405cb3f98487d958b271c61b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.12

Release history Release notifications | RSS feed

This release

1.6.0 This release

2 release files

1.5.1

2 release files

1.0.0

2 release files

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