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.
PR2 — dual-arm wheeled robot (88 links)
╔════════════════════════════════════════════════════╗
║ 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×,WARN1.0–1.5×,FAIL< 1.0×.
[STABILITY]
Available for wheeled robots by default. Contact point detection runs three passes in priority order:
- Links named
*wheel* - Cylindrical links with a wheel-like radius-to-length ratio (r/L > 0.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_urdfreturns the report rather than exporting it. Pass every input on every call (that is whycompare_reportstakes 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
stderrand a non-zero exit code — never an unhandled traceback. - Honest confidence. Every record's
effort_confidenceuses the same fixed vocabulary as the rest of the tool (exact/estimated/guessed/missing/simulated); a field that cannot be computed ships asnullalongside a*_reasonstring, 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)
| File | Size | Uploaded | |
|---|---|---|---|
| urdf_validator-1.6.0.tar.gz | 256.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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
|