simcheck
Does your robot or object asset actually behave in simulation?
A URDF or MJCF file can load without a single warning and still be broken:
- The drawer can be jammed inside a convex hull.
- The mug can be a solid cylinder.
- A door can swing through its own cabinet.
- Joint limits can be silently ignored.
- The inertia tensor can be thrown away by the engine.
simcheck loads the asset in MuJoCo and PyBullet and runs it. It reports what each engine actually does with the file, and why.
$ simcheck tests/assets/cabinet_mesh.urdf
tests/assets/cabinet_mesh.urdf
FAIL score 94.6/100 1 error, 4 warnings, 2 notes [mujoco, pybullet; 0.3 s]
ERRO engine.rest_contacts [mujoco]: 'carcass' and 'drawer' overlap by 45.0 mm at rest in mujoco; the authored geometry doesn't overlap there: it is mujoco's convex hull of 'carcass', so split that mesh into convex parts (CoACD); they are parent and child, but MuJoCo still collides them because the parent is welded to the world, so joint 'drawer_slide' is braked by this contact
WARN engine.collision_shape [mujoco]: mujoco treats 100% of the empty space inside 'carcass' as solid (1.93e+05 cm^3); objects can't go in. Split the mesh into convex parts (CoACD, V-HACD).
...
$ simcheck fix tests/assets/cabinet_mesh.urdf -o fixed/cabinet.urdf
fixed: link 'carcass': concave collision mesh (hull 5.6x its volume) split into 58 convex parts
before: FAIL 94.6 (1 error, 4 warnings)
after: PASS 100 (0 errors, 0 warnings)
Install
pip install "simcheck[all]"
Or from source:
git clone https://github.com/jashshah999/simcheck
pip install -e "simcheck[all]"
- Requirements: Python 3.10+. MuJoCo, trimesh and the other dependencies install automatically.
- Extras:
pybullet: the second engine.fix: CoACD, needed bysimcheck fixfor convex decomposition.all: both of the above.
- A plain install (
pip install simcheck) checks in MuJoCo only.
Use
simcheck path/to/robot.urdf # one file
simcheck assets/ --html report.html # a folder, with an HTML report
simcheck model.xml --engines mujoco # MJCF, one engine
simcheck mug.urdf --free-base --category mug # a loose object: also drop it on a floor
simcheck scan dataset/ -o results.jsonl # thousands of files, parallel, resumable
simcheck fix cabinet.urdf # write cabinet.fixed.urdf and re-check it
simcheck list-checks -v # what every check does
Exit codes:
check:1when any asset has an error.--fail-on warningmakes warnings count too, and--fail-on neveralways exits0.scan: exits0unless you pass--fail-on.fix:1if the repaired file still has an error.- Every command exits
2on a usage mistake, such as a missing file, an unknown engine or a misspelled option.
Output files: --json writes an object for one file and a list for several.
From Python:
import simcheck
report = simcheck.check("cabinet.urdf", engines=("mujoco", "pybullet"))
print(report.verdict, report.score)
for f in report.findings:
print(f.severity, f.check, f.engine, f.message)
report.write_json("report.json")
In CI
- uses: jashshah999/simcheck@v0.1.0
with:
paths: assets/
fail-on: error # or warning
args: --config simcheck.toml
The action does three things:
- Annotates the offending files in the pull request.
- Writes a summary table to the job page.
- Uploads nothing anywhere; reports stay in the workspace (
simcheck-report.json,simcheck-report.html).
What it checks
| Group | Checks |
|---|---|
| File | parses (and flags xacro); kinematic tree; meshes resolve and load; mesh file names that MuJoCo would merge |
| Mass and inertia | every moving part has mass; inertia is positive definite and satisfies the triangle inequality; mass vs size (catches g-vs-kg and mm-vs-m, accepts hollow shells); inertia vs the part's own geometry (placeholders, impossible values, COM off the part) |
| Geometry | plausible overall size (optionally against category priors); moving parts have collision geometry; collision sits where the visual is; mesh quality; concave collision meshes |
| Joints | axis valid and unit length; limits ordered and sane; non-zero effort and velocity limits; mimic joints |
| Kinematics (no engine needed) | traces points on each moving part through its joint range and flags parts that pass through another part (a flipped axis, a hinge in the wrong place, wrong limits); parts that start inside each other; parts floating away from their parent; hinge axes that miss the part they turn |
| In each engine | loads; keeps the joints and limits from the file; keeps the authored mass and inertia; starts without interpenetration; collision shape matches the mesh (measured by probing the engine with small spheres); drops and settles; stable under gravity; conserves energy; robust to the timestep; joint sweeps |
| Across engines | MuJoCo and PyBullet load the same mass and land the drop in the same place |
35 checks in all; the full list with descriptions is in docs/checks.md, and docs/how-it-works.md explains the method.
Robots (files with actuators or transmissions, or robot-like link names) and objects are judged slightly differently: a hollow arm link or an arm that can fold into its own base is normal for a robot but a defect in a cabinet. Override with --robot / --object.
Score:
- The share of applicable checks passed; warnings count half.
- Any error makes the verdict
FAIL. - Any warning makes it
WARN.
What engines do silently
The engine checks exist because each of these behaviours makes a valid-looking file behave differently than written. Each one was reproduced on MuJoCo 3.15 and PyBullet 3.2.7.
MuJoCo:
- Meshes collide as their convex hull, even on static bodies. Cabinets, drawers, mugs and bins become solid blocks.
- Parent-child contacts are skipped, except when the parent is welded to the world. A drawer inside a convexified fixed cabinet is therefore pushed out at the first step, while the same drawer in a free cabinet silently passes through it.
- A URDF joint with
lower > upperorlower == upperbecomes unlimited. PyBullet also leaveslower > upperunlimited but lockslower == upper. - Two mesh files with the same file name in different folders become one mesh.
- A URDF root link without a joint is fused into the world, so a loaded object cannot move.
- It resets the simulation when it diverges. simcheck disables that, so blow-ups are visible.
PyBullet:
- The URDF inertia tensor is ignored unless
URDF_USE_INERTIA_FROM_FILEis set; it is recomputed from the bounding box. - A link without
<inertial>gets 1 kg. - Every joint starts with a velocity motor that holds it in place.
- Self-collision is off by default.
simcheck runs each engine in an "honest" configuration that turns these conveniences off, then reports where defaults would change the result.
Fixing
simcheck fix asset.urdf writes a repaired copy (asset.fixed.urdf, with new meshes in asset.fixed_meshes/) and re-checks it. It works on URDF files. It applies only mechanical fixes:
- decompose: concave collision meshes are split into convex parts with CoACD. It refines automatically until no other part of the asset sits inside the new shapes.
- collision: collision geometry is added from the visual for parts that have none.
- inertia: missing, zero or non-physical inertia is recomputed from geometry. It keeps the mass when it is valid, otherwise uses
--density. - limits: swapped limits are swapped back.
- axes: joint axes are normalized.
Problems that need a human decision are listed under "left", not guessed: a wrong axis direction, a hinge in the wrong place, an implausible mass, a zero effort limit. Convex decomposition of a large mesh can take a minute.
Leaderboard
leaderboard/ scores 1,868 assets from 15 public sources:
| Dataset | Kind | Assets | Pass | Warn | Fail |
|---|---|---|---|---|---|
| MuJoCo Menagerie | human-authored robots (MJCF) | 27 | 70% | 22% | 7% |
| robosuite objects | human-authored objects (MJCF) | 11 | 82% | 18% | 0% |
| RoboCasa kitchen objects | human-authored objects (MJCF) | 100 | 47% | 53% | 0% |
| RoboCasa kitchen fixtures | human-authored articulated (MJCF) | 49 | 0% | 100% | 0% |
| LIBERO articulated objects | human-authored articulated (MJCF) | 10 | 0% | 30% | 70% |
| robot_descriptions URDFs | human-authored robots (URDF) | 34 (+1 timed out) | 3% | 15% | 82% |
| Google Scanned Objects (MJCF) | scanned objects | 80 | 0% | 100% | 0% |
| YCB (URDF) | scanned objects | 78 | 23% | 77% | 0% |
| YCB (MJCF) | scanned objects | 88 | 32% | 68% | 0% |
| EmbodiedGen | generated: image/text-to-3D | 130 | 0% | 99% | 1% |
| projectsim | generated: commercial procedural | 40 | 22% | 32% | 45% |
| Articraft-10K, full | generated: LLM agent | 301 | 1% | 2% | 97% |
| Articraft-10K, visual | generated: LLM agent | 699 | 0% | 0% | 100% |
| URDFormer outputs | generated: image-to-URDF model | 11 | 0% | 0% | 100% |
| LAM: claude-opus-4-6 | generated: LLM (LAM pipeline) | 30 | 0% | 0% | 100% |
| LAM: claude-sonnet-4 | generated: LLM (LAM pipeline) | 30 | 0% | 0% | 100% |
| LAM: gemini-2.5-pro | generated: LLM (LAM pipeline) | 30 | 0% | 0% | 100% |
| LAM: gemini-2.5-flash | generated: LLM (LAM pipeline) | 30 | 0% | 0% | 100% |
| LAM: o3 | generated: LLM (LAM pipeline) | 30 | 0% | 0% | 100% |
| LAM: gpt-5 | generated: LLM (LAM pipeline) | 30 | 0% | 0% | 100% |
| LAM: original pipeline | generated: LLM (LAM pipeline) | 30 | 0% | 0% | 100% |
What it found:
- Generated assets rarely carry real physics. Of 1,000 Articraft-10K assets, 699 have no collision geometry. Of the other 301, 292 have moving parts with no
<inertial>, so MuJoCo invents a mass at the density of water and PyBullet uses 1 kg and 1 kg m^2. In LAM it is 267 of 271, across all seven LLM backbones. - Convex hulls create collisions nobody authored. 127 of 301 Articraft assets start with parts overlapping in MuJoCo. In 50 of them the authored meshes don't overlap; MuJoCo's convex hulls do. The same effect jams the drawers of 10 of the 11 URDFormer cabinets.
- Placeholder inertia is common. 124 of 130 EmbodiedGen objects and 60 of 78 YCB URDFs use the same inertia on every axis whatever their size.
- Visual meshes add mass. All 80 sampled Google Scanned Objects and all 49 RoboCasa fixtures get mass from visual-only geoms (one freezer drawer: 895 of its 956 kg).
- Curated robots have real issues too. Kinova Gen3's base and shoulder overlap by 12 mm, which MuJoCo turns into a brake on joint 1; Talos's default pose puts its grippers inside its thighs.
Every finding category was audited by opening the flagged files; leaderboard/README.md has the method and how to reproduce it, and leaderboard/stats.py recomputes every number above from the results.
Benchmark
simcheck bench assets/ plants 17 kinds of defect into every asset and reports which ones are caught. Examples:
- millimetre units
- reversed axis
- swapped limits
- broken inertia
- grams as kilograms
- missing collision
- offset collision
- centre of mass off the part
- broken mesh path
- joint inside its parent
On 52 real assets (YCB, EmbodiedGen, robot_descriptions and the test fixtures), it caught 631 of 655 planted defects (96%): all 536 on objects and 95 of 119 on robots. Most robot misses are deliberate: missing or offset collision geometry is a note on robots, and a reversed axis on a joint with symmetric limits is invisible geometrically. See leaderboard/BENCH.md.
Configuration
simcheck reads simcheck.toml, or a [tool.simcheck] table in pyproject.toml, from the current directory. You can also pass one with --config.
engines = ["mujoco", "pybullet"]
disable = ["geometry.visual_match"] # check ids from `simcheck list-checks`
kind = "auto" # "robot", "object" or "auto"
fixed_base = true # omit to let simcheck decide
package_paths = { my_robot = "../my_robot_description" } # where package://my_robot lives
seed = 0
[thresholds]
contact_error = 0.01 # m of penetration that counts as an error
convexity_warn = 1.5 # hull volume / mesh volume
density_warn_low = 20.0 # kg/m^3
Thresholds: every threshold is in simcheck/config.py. A misspelled key is reported with the closest valid name.
Size priors (category):
- Set a category per run with
--category, or in the config file. - Otherwise simcheck guesses one from the file and folder names:
Microwave081/model.xmlis a microwave. - A guessed category only adds notes unless the size is off by 10x or more.
- Known categories: airship, blimp, bottle, box, bridge, bucket, cabinet, chair, clock, conveyor, crane, dishwasher, door, drawbridge, drawer, excavator, faucet, ferris, gate, helicopter, kettle, lamp, laptop, lighthouse, microwave, mug, oven, pen, pot, refrigerator, safe, scissors, table, toilet, tower, trashcan, washingmachine, window, windmill.
Limitations
- Formats:
- URDF and MJCF only; USD is not read yet.
- PyBullet reads URDF only, so MJCF files are checked in MuJoCo alone.
- Engines: Newton and Genesis are not backends yet.
- Platforms: tested on Linux with Python 3.10, 3.11 and 3.13. Engines run in forked child processes, so a native crash or hang costs one result. Windows has no fork, so there the engines run in-process and a native crash ends the run.
- Speed: most assets take 1-10 s. Large robots with many detailed meshes can take a few minutes;
scanruns assets in parallel. - What the checks test: the asset against itself and against physics, not against the real object. A cabinet that is internally consistent but twice the real size only shows up through a category size prior.
- Heuristics: some checks are heuristics with thresholds you can change:
- the hollow-shell density rule
- the pass-through share
- the cavity-fill share
- Gaps: soft bodies, cloth, cables, fluids and tendons are not checked.
Related tools
- NVIDIA SimReady: a spec and benchmark for USD assets in Isaac and PhysX.
- simcompat: compares MuJoCo and PyBullet imports of URDF robots against Pinocchio, without contact.
- simready-audit: checks rigid USD/MJCF objects in MuJoCo.
- Static linters:
check_urdf, urdf_parser_py, yourdfpy and others.
simcheck's focus:
- articulated objects and robots in URDF and MJCF;
- contact behaviour across engines;
- measuring the collision shape each engine really uses;
- a public leaderboard.
License
Apache-2.0
Metadata
Release files for simcheck 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| simcheck-0.1.0.tar.gz | 117.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| simcheck-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 224.3 kB
Release files / simcheck-0.1.0.tar.gz
| Download URL | simcheck-0.1.0.tar.gz |
|---|---|
| Size | 117.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6b8fef3c0a516b89682a5bfc5a0d4ac65af7cb4eb6061a4b783cbf78662a443c
|
|
BLAKE2b-256 checksum How to use checksums |
956d33ef594349cf7d6a64a68dd8ac43a0ab91ec87081a166262a9215e47a12f
|
| 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 13, 2026.
Transparency logRelease files / simcheck-0.1.0-py3-none-any.whl
| Download URL | simcheck-0.1.0-py3-none-any.whl |
|---|---|
| Size | 107.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
37d417ecf6f6dcfce9d525c0db8e7a33bcbc9f72aa5d189ed0fbcd1f9ab5a56c
|
|
BLAKE2b-256 checksum How to use checksums |
8217e548848ac3418de6de62c47e8ab375c8f913d694b053eac64c8f20a1f664
|
| 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 13, 2026.
Transparency logSecurity for simcheck 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for simcheck-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 13, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.