Metadata-Version: 2.4
Name: simcheck
Version: 0.2.0
Summary: Check that robot and object assets (URDF, MJCF) actually behave in physics engines
Author-email: Jash Shah <jashshah.999@gmail.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/jashshah999/simcheck
Project-URL: Documentation, https://github.com/jashshah999/simcheck/tree/main/docs
Project-URL: Changelog, https://github.com/jashshah999/simcheck/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/jashshah999/simcheck/issues
Keywords: urdf,xacro,mjcf,mujoco,pybullet,ros,simulation,robotics,validation,sim-ready,linter
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Environment :: Console
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Requires-Dist: trimesh>=4.0
Requires-Dist: scipy>=1.10
Requires-Dist: mujoco>=3.2
Requires-Dist: rtree>=1.0
Requires-Dist: pycollada>=0.8
Requires-Dist: pillow>=9
Requires-Dist: tomli>=2.0; python_version < "3.11"
Provides-Extra: pybullet
Requires-Dist: pybullet>=3.2.6; extra == "pybullet"
Provides-Extra: fix
Requires-Dist: coacd>=1.0; extra == "fix"
Provides-Extra: ros
Requires-Dist: xacro>=2.0; extra == "ros"
Provides-Extra: all
Requires-Dist: pybullet>=3.2.6; (platform_system == "Linux" and platform_machine == "x86_64" and python_version < "3.12") and extra == "all"
Requires-Dist: coacd>=1.0; extra == "all"
Requires-Dist: manifold3d>=2.4; extra == "all"
Requires-Dist: xacro>=2.0; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: coacd>=1.0; extra == "dev"
Requires-Dist: manifold3d>=2.4; extra == "dev"
Requires-Dist: xacro>=2.0; extra == "dev"
Requires-Dist: pybullet>=3.2.6; (platform_system == "Linux" and platform_machine == "x86_64" and python_version < "3.12") and extra == "dev"
Dynamic: license-file

# 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, why, and how to fix it.

```console
$ simcheck tests/assets/cabinet_mesh.urdf
tests/assets/cabinet_mesh.urdf
  FAIL  1 error, 4 warnings, 2 notes  (34 of 37 checks pass)   [object, fixed base; mujoco, pybullet; 0.4 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
wrote fixed/cabinet.urdf and meshes in fixed/cabinet_meshes
before: FAIL  1 error, 4 warnings
after:  PASS  0 errors, 0 warnings
```

## Install

```bash
pip install "simcheck[all]"
simcheck doctor          # versions, engines, and a quick self-test
```

- **Requirements:** Python 3.10 or newer, on Linux, macOS or Windows. MuJoCo, trimesh and the other dependencies install automatically.
- **Extras:**
  - `pybullet`: the second engine.
  - `fix`: CoACD, used by `simcheck fix` for convex decomposition.
  - `ros`: xacro, for `.xacro` files.
  - `all`: all of them.
- **PyBullet:** its last release only has prebuilt wheels for Linux x86_64 with Python 3.10 and 3.11. Elsewhere, `[all]` leaves PyBullet out and simcheck checks in MuJoCo only. To get both engines quickly, use Python 3.11 on Linux. See [troubleshooting](docs/troubleshooting.md#installing).
- **From source:**

  ```bash
  git clone https://github.com/jashshah999/simcheck && pip install -e "simcheck[all]"
  ```

## Use

```bash
simcheck robot.urdf                          # one file (URDF, xacro or MJCF)
simcheck assets/ --html report.html          # a folder, with an HTML report
simcheck model.xml --engines mujoco          # MJCF in one engine
simcheck mug.urdf --free-base --category mug # a loose object: also drop it on a floor
simcheck scanned_part.obj                    # a bare mesh, checked as a solid object (density of water)
simcheck explain engine.rest_contacts        # what a check means and how to fix it
simcheck fix cabinet.urdf                    # write cabinet.fixed.urdf and re-check it
```

**ROS packages.** simcheck expands `.xacro` files itself. Pass arguments with `--xacro-arg name:=value`. It finds `package://` and `$(find ...)` packages in this order:
- next to the file;
- through `ROS_PACKAGE_PATH`;
- under a ROS 2 install;
- in folders you name with `--package-path src/`.

An SRDF next to the URDF (or `--srdf`) marks the link pairs MoveIt already ignores.

```bash
simcheck src/my_robot_description/urdf/robot.urdf.xacro --package-path src/
```

**Datasets.** `scan` checks thousands of files in parallel and can resume. `select` prints the ones worth keeping, and `report` turns results into HTML, Markdown, SARIF or JUnit:

```bash
simcheck scan generated/ -o results.jsonl                  # resumable; --retry-failed re-runs timeouts
simcheck select results.jsonl --ignore inertia.geometry > keep.txt
simcheck report results.jsonl --html results.html
```

**CI.** The GitHub Action checks only the assets a pull request changes, including through a mesh. A baseline means existing problems don't block anyone:

```yaml
- uses: actions/checkout@v4
  with: { fetch-depth: 0 }
- uses: jashshah999/simcheck@v0.2.0
  with:
    paths: assets
    changed-since: ${{ github.event_name == 'pull_request' && format('origin/{0}', github.base_ref) || '' }}
    baseline: simcheck-baseline.json   # from: simcheck check assets --write-baseline simcheck-baseline.json
```

It annotates the offending lines in the pull request and writes a summary to the job page. It can also write SARIF for code scanning. GitLab (JUnit), pre-commit hooks and caching are in [docs/ci.md](docs/ci.md).

**Python.**

```python
import simcheck

report = simcheck.check("cabinet.urdf", engines="mujoco")
print(report.verdict, [f.message for f in report.errors])
reports = simcheck.check_many(paths, jobs=8)        # under `if __name__ == "__main__":` on macOS/Windows
```

The full API is in [docs/python-api.md](docs/python-api.md).

**Exit codes:**

| Code | Meaning |
|---|---|
| 0 | Nothing at or above `--fail-on`. |
| 1 | An asset has an error (or a warning, with `--fail-on warning`). |
| 2 | A usage mistake. |
| 3 | The run is incomplete: a timeout, or a bug in simcheck. |
| 130 | Interrupted. Results checked so far are still written. |

## What it checks

| Group | Checks |
|---|---|
| **File** | parses (expanding 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) | moves each joint through its 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 |

There are 35 checks in all. [docs/checks.md](docs/checks.md) explains each one and how to fix what it reports, and [docs/how-it-works.md](docs/how-it-works.md) explains the method.

**Robots 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. Each report says which simcheck decided (`[robot, fixed base; ...]`); `--robot` and `--object` override it.

## 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–3.16 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. So a drawer inside a convexified fixed cabinet is pushed out at the first step, while the same drawer in a free cabinet silently passes through it.
- **A URDF joint with `lower > upper` or `lower == upper` becomes unlimited.** PyBullet also leaves `lower > upper` unlimited, but locks `lower == 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.
- **COLLADA and glTF meshes can't be read.** Visual ones are dropped; collision ones make loading fail.

**PyBullet:**
- **The URDF inertia tensor is ignored** unless `URDF_USE_INERTIA_FROM_FILE` is set. It is recomputed from the bounding box instead.
- **A link without `<inertial>` gets 1 kg.**
- **A non-physical inertia tensor is replaced with zero**, with only a console message.
- **Every joint starts with a velocity motor that holds it in place.**
- **Self-collision is off by default.**
- **A file with very long lines doesn't parse.** This happens with generated one-line URDFs. glTF meshes don't parse either, even when only visual.

simcheck runs each engine in an "honest" configuration that turns these conveniences off. It then reports where the 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 and xacro files. It changes only what it repairs: comments, formatting and `package://` paths are kept, so the diff shows just the fix. It applies only mechanical fixes:
- **decompose:** concave collision meshes are split into convex parts with CoACD. It refines until no other part of the asset sits inside the new shapes, within `--max-time` seconds per mesh (120 by default).
- **collision:** collision geometry is added from the visual for parts that have none. Robots are skipped unless you ask with `--only collision`, since they often leave parts without collision on purpose.
- **inertia:** missing, zero, non-physical or placeholder inertia is recomputed from geometry. It keeps the mass when it is valid, and 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. Examples are a wrong axis direction, a hinge in the wrong place, an implausible mass and a zero effort limit. `fix` exits 1 when the repaired file still has an error, or has a problem the original didn't (it says which).

On a robot, `fix` decomposes only the links whose shape causes a warning or an error; a concave link alone is just a note there. A xacro file is repaired into plain URDF.

## Configuration

simcheck reads the nearest `simcheck.toml`, or a `[tool.simcheck]` table in `pyproject.toml`; `simcheck init` writes a commented one. You can set the engines, which checks run, robot or object, package paths, xacro arguments, thresholds, a baseline and a cache. You can also choose which findings don't count:
- by path, check, part or pair of parts;
- with a `<!-- simcheck-disable: CHECK -->` comment in the asset itself.

Everything is in [docs/configuration.md](docs/configuration.md).

## Leaderboard

[`leaderboard/`](leaderboard/) scores 1,868 assets from 15 public sources (simcheck 0.1.0):

| 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](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 only a note on robots.
- A reversed axis on a joint with symmetric limits is invisible geometrically.

See [leaderboard/BENCH.md](leaderboard/BENCH.md).

## Limitations

- **Formats:**
  - URDF, xacro and MJCF only; SDF and USD are not read yet.
  - PyBullet reads URDF only, so MJCF files are checked in MuJoCo alone.
- **Engines:** Newton, Isaac and Genesis are not backends yet.
- **Platforms:** CI runs the test suite on Linux (Python 3.10-3.13), macOS and Windows. On macOS and Windows, engines run in a fresh child process per asset (about a second more per engine), and PyBullet has no prebuilt wheels, so checks run in MuJoCo.
- **Speed:** most assets take 1-10 s. Large robots with many detailed meshes can take a minute or more. `check -j` and `scan` run assets in parallel, and `--cache` skips unchanged ones.
- **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 (`pass_through_share`, `cavity_fill_share`, `shell_wall_warn`; see [configuration](docs/configuration.md#thresholds)).
- **Gaps:** soft bodies, cloth, cables, fluids, tendons and actuator tuning are not checked.

## Documentation

- [Checks and how to fix them](docs/checks.md)
- [Configuration](docs/configuration.md)
- [CI: GitHub, GitLab, pre-commit, baselines](docs/ci.md)
- [Python API](docs/python-api.md)
- [Output formats](docs/output-format.md)
- [How it works](docs/how-it-works.md)
- [Troubleshooting](docs/troubleshooting.md)
- [Changelog](CHANGELOG.md)

## 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
