Clean, team-facing MuJoCo baseline for the NeoRacer 1/12-scale RC car.
The mesh STLs in assets/meshes/ are stored with Git LFS. Run git lfs install once before cloning, or you'll get 132-byte pointer files instead of the real meshes.
Dependencies are pinned in requirements.txt (mujoco, numpy, pytest). Pick one:
conda env create -f environment.yml # creates the "neoracer-mujoco" env
conda activate neoracer-mujocouv venv --python 3.13
uv pip install -r requirements.txt
source .venv/bin/activateThen install the package (editable), so neoracer_mujoco is importable by the
examples and the validation suite:
pip install -e .Install from a clone, not PyPI: the package is intentionally not published, and
the wheel does not bundle assets/ — the model XML and meshes are the product
and stay at the repo root, so load() resolves them from your checkout.
# Run the model in the viewer (macOS needs mjpython for the passive viewer):
mjpython -m examples.run
# Run the validation suite (any Python with mujoco + pytest installed):
python3 -m pytest validation/ -v.
├── README.md — this file
├── assets/
│ ├── neoracer.xml — MuJoCo MJCF model (the source of truth)
│ └── meshes/ — visual STL meshes (cosmetic; mass="0")
├── src/neoracer_mujoco/ — importable package (the reusable toolbox)
│ ├── contract.py — the car contract (single source of truth)
│ ├── assets.py — cars() / load() model discovery + compile
│ ├── sensors.py — SensorReadings/IMUReading/LidarScan + read()
│ ├── sim.py — compile/settle/run + physics probes
│ └── control/ — classical controllers (track_centering)
├── examples/
│ ├── run.py — viewer launch script (mjpython entry point)
│ ├── manual_drive.py — arrow-key teleop (game-style, python3 entry point)
│ └── track_centering_demo.py — PD centering controller on the corridor track
├── validation/ — pytest physics + logic conformance suite
└── docs/ — (reserved) design notes and parameter log
| Property | Value | Source |
|---|---|---|
| Wheelbase | 288 mm | URDF (joint origins) |
| Track width | 235 mm | URDF (joint origins) |
| Wheel radius | 50 mm | ESTIMATED — STL bbox |
| Chassis mass | 1.62 kg | URDF (base_link) |
| Chassis CoM | ~9 mm ahead of axle center, z≈0.067 m | URDF, re-anchored to axle plane |
| Chassis inertia | diag (0.00238, 0.00584, 0.00701) kg·m² | URDF (base_link) |
| Wheel mass | ~0.058 kg each | URDF — verify vs real wheel+tyre |
| Total mass | 1.86 kg | URDF |
| Steering | Ackermann (4th-order polynomial equality constraints) | |
| Drive | AWD — 4 independent torque motors | |
| Suspension | Coil-over approximation (slide Z joint, k=600 N/m, c=25 Ns/m) | ESTIMATED |
| Sensors | IMU (accel/gyro/quat/vel) · steer · wheel vel · susp pos · 8-beam LiDAR |
| Index | Name | Unit | Direction |
|---|---|---|---|
| ctrl[0] | fl_motor | N·m | + = forward |
| ctrl[1] | fr_motor | N·m | + = forward |
| ctrl[2] | rl_motor | N·m | + = forward |
| ctrl[3] | rr_motor | N·m | + = forward |
| ctrl[4] | steer_servo | rad | + = left turn |
Steering command range is ±0.4 rad; the two Ackermann equality constraints split it into the correct inner/outer front-wheel angles automatically.
examples/ holds runnable, hackable demos — start here to drive the car yourself
or to read sensors. They are meant to be copied and modified, not imported as a
stable API.
run.py— launches the MuJoCo viewer and drives the car with a built-in demo controller (constant throttle + sinusoidal steering) so you can watch it move. FlipSAFE_MODEat the top:Truecaps speed and warns on rollover,Falseuses aggressive inputs. Run withmjpython -m examples.run.manual_drive.py— drive the car yourself with the arrow keys, video-game style: it owns a glfw window and polls held keys each frame, so you hold to build speed, release to coast down, and let the wheel re-center. A back-top chase camera follows the car's heading. Use this to feel the physics — grip, rollover, suspension, wall contacts. Run with plainpython3 -m examples.manual_drive(NOT mjpython — the glfw window must be created on the main thread). Camera and control feel are tunable via the constants at the top of the file.track_centering_demo.py— runs the classical PDTrackCenteringControlleron the straight corridor track, headless by default or with an interactive viewer. Composesneoracer.xmlontoassets/tracks/straight_corridor.xmlat runtime, settles, then drives the controller and reports centering performance.python3 -m examples.track_centering_demo(headless) ormjpython -m examples.track_centering_demo --viewer.
Sensor reading lives in the package, not the examples: import from
neoracer_mujoco.sensors (read, wheel_speed_ms, print_sensors,
lidar_scan) to log IMU / wheel / steer / suspension / LiDAR off a compiled model.
Add your own demos here — a pure-pursuit follower, a data recorder, etc. — using
run.py as the template for the load → step → control loop.
A standardized gate every car model must clear before it's trained on. The suite
globs assets/*.xml, so a new car is tested automatically the moment its XML lands
in assets/ — no edits needed.
python3 -m pytest validation/ -vtest_conformance.py— physics checks, one per failure mode: actuator/sensor contract, won't-explode-at-rest (no NaN, rests on ground, stays upright), correct control response (throttle drives forward, steer yaws the right way, Ackermann inner > outer), and RL-exploit guards (no free energy, bounded top speed, finite sensors under load, determinism).test_logic.py— pure-logic checks: wheel-speed unit conversion, sensor read, and the Ackermann polyfit validated against exact arctan geometry.test_track_centering.py—TrackCenteringControllersign/safety logic plus one physics-integration run on the corridor track.
The car contract (expected actuators, required sensors, mass band, ctrl layout)
lives in src/neoracer_mujoco/contract.py — update it there if the contract changes.