A ray-cast wheel + Pacejka-tire vehicle physics SDK on top of the Genesis physics engine.
genesis_vehicle is a Python package that wraps Genesis with a batched
(n_envs ≥ 1), composable vehicle physics layer. The wheel-count of your
URDF and the choice of steering / drivetrain / coupling / tire model are
independent — wire any combination through VehicleConfig and let the SDK
run the same 5-step pipeline (raycast → suspension → slip → tire → ω) for
every wheel, every step, every env. Designed for RL / MPPI control loops
and Real2Sim parameter fitting.
At a glance, the SDK provides:
- A single
VehiclePhysicsdriver that orchestrates the 5-step ray-wheel + Pacejka pipeline batched overn_envs. Onestep()call advances every env, every wheel, in lock-step. - Composable building blocks for steering, drivetrain, wheel coupling, tire model, and stability hooks (see the table below). Each block is a small class you can subclass.
- URDF-as-default, API-as-truth merge. Wheel positions, radii, masses,
inertia, and joint names come from your URDF; anything you set explicitly
on
WheelConfigwins. Afterresolve(), the resultingResolvedConfigis the only source of truth at runtime. - ISO 8855 sign convention end-to-end:
+Xforward,+Yleft,+Zup,+steer= right turn,+throttle= forward. Internal sign flips for Genesis RHS / URDF axis quirks are absorbed inside the strategies. - Stability profiles (
"control"/"raw"/"research") that bundle the right numerical stabilizers for your use case so MPPI / RL / Real2Sim users don't have to assemble hook lists by hand. - Two wheel-contact models.
wheel_contact="point"(the default) is one downward ray per wheel — light and stable, but a zero-radius probe that reads ground height as a step function.wheel_contact="swept_envelope"(v1.6.0, opt-in) casts M rays per wheel and reduces them with the swept-circle lower envelope, so a wheel starts climbing a kerb aboutsqrt(2*r*h)before it instead of taking the whole obstacle in onedt. Seedocs/tire-and-contact.md. - Pure-Python tests (no Genesis runtime needed) that you can run in any CI.
The full mental model is laid out in
docs/concepts.md; the API surface lives in
docs/api-reference.md.
Vehicle simulation has three orthogonal axes you can batch over. The SDK hits all three with the same lightweight Python surface:
| Axis | Meaning | API | Batched by | Status |
|---|---|---|---|---|
| L1 | Wheels of ONE vehicle (4-10) | VehiclePhysics (built-in) |
Compute pipeline runs on (*, n_wheels) tensors — no per-wheel Python loop |
✓ since v0.5.0 (always on) |
| L2 | K vehicles of the same kind sharing ONE Genesis env | MultiVehiclePhysics(scene, vehicles) |
One batched compute per kind; per-vehicle I/O via batched solver calls | ✓ since v0.5.11 |
| L3 | N parallel Genesis envs (parallel-universe rollouts) | scene.build(n_envs=N) + VehiclePhysics(n_envs=N) |
All env work collapses into one CUDA dispatch chain | ✓ since v0.1 |
| L2 × L3 | K vehicles × N parallel envs (N·K total) |
MultiVehiclePhysics(scene, vehicles, n_envs=N) |
Combined: compute on (N·K, n_wheels) tensors, one batched call per kind |
✓ since v0.5.14 |
Rule of thumb:
- 1 vehicle, many parallel rollouts → L3 alone (
n_envs > 1). 44× at N=64. - K vehicles visible in one scene (traffic, multi-agent demo) → L2 alone.
- K vehicles per scenario × N parallel scenarios (ego + traffic for MPPI) → L2 + L3 combined, ~multiplicative speedup.
Measured (RTX 5070 Laptop, see docs/batching.md):
| K | N | total | gain vs (K=1, N=1) |
|---|---|---|---|
| 1 | 4 | 4 | 2.8× (L3 only) |
| 2 | 1 | 2 | 1.5× (L2 only) |
| 2 | 4 | 8 | 4.6× (combined ≈ 1.5 × 2.8) |
| 1 | 64 | 64 | 44× (L3 push) |
Full design + decision matrix in docs/batching.md.
Runnable benchmarks (headless):
samples/perf_vectorization.py (L3),
samples/perf_multi_vehicle.py (L2),
samples/perf_l2_l3_combined.py (combined).
Visual demos (with --viewer):
samples/multi_env_render.py (L3 grid),
samples/road_loop.py (L2 multi-kind track),
samples/city_traffic_ego.py (L2 × L3 ego + traffic highway).
Requires Python 3.12+ and
Genesis — genesis-world 1.4.0 is
the supported floor (v1.6.6). It is the only version the test suite and every
published measurement run on, and the only one the project supports. Earlier
releases are NOT supported; the ≥ 1.0.0 claim printed here through v1.6.4 was
never enforced by any branch or test, and the "1.3.3 or 1.4.0" wording that
replaced it in v1.6.5 is now narrowed to 1.4.0.
What the code actually does, since nothing enforces the floor: the
<= 1.3.3 branches in _gs_compat.py (the force API, the link inertial reads,
the Scene.sim_options name) and in vehicle_scene.py are still there and will
still be taken if you install 1.3.3 — the SDK does not refuse it and there is no
version check. Those branches are dead weight to be removed in a later
release, not a compatibility promise: they are untested, unmeasured, and no
new ones will be added. See the 1.5.0 entry (and 1.2.7 for the renderer API
split) for what moved at the 1.3.3 → 1.4.0 bump; its cross-version parity table
is a record of a measurement taken once at that bump, not an ongoing guarantee.
On
genesis-world >= 1.4.0, ask for the solver rate with
VehicleScene(substeps=...) and do not pass dt= to RigidOptions —
that field now means the substep interval. See
CHANGELOG.md for the per-release backend-compatibility
notes.
The SDK is a single Python package directory; there is no pip install
yet. Clone and add the parent directory to PYTHONPATH:
git clone https://github.com/korfriend/GenesisVehicle.git
# either:
export PYTHONPATH="$(pwd):$PYTHONPATH"
# or inside your entry script:
# import sys; sys.path.insert(0, "<parent of cloned dir>")Then import genesis_vehicle from anywhere.
Dev dependency for running the test suite: pip install pytest.
from genesis_vehicle import VehicleScene, car_4w_rwd_ackermann
URDF = "<path to your 4-wheel-car URDF>"
# Physics backend — process-global, set ONCE, before any scene. Default is
# "cpu" (faster below ~100 envs; GPU is kernel-launch bound at small batch).
# The renderer is separate (always GPU). Use "gpu" only for large-n_envs L3.
VehicleScene.init_backend("cpu")
# VehicleScene is the single entry point: it owns gs.init / the scene(s) / build / step.
# raycast_mode defaults to "dual_scene" — keep it. "single_scene" works on a
# plane but cannot host a detailed road mesh at all (docs/dual-scene-raycast.md).
# substeps refines the CHASSIS contact integration, NOT the ray-wheel
# suspension, and changing it changes the trajectory (docs/server.md §2.3).
vs = VehicleScene(dt=0.025, substeps=4) # dt: the preset's recommended_dt
# (VehicleScene's own default is
# 1/200 s); substeps=4 IS the default
vs.add_ground_plane(friction=1.0)
veh = vs.add_vehicle(URDF, preset=car_4w_rwd_ackermann, pos=(0, 0, 1.0))
vs.build()
for step in range(480): # 12 s @ 40 Hz
veh.set_inputs(throttle=0.5, brake=0.0, steer=0.0) # scalars or (n_envs,) tensors
vs.step()
print(veh.get_pos()[0].cpu().numpy())
print(veh.all_wheels_grounded) # (n_envs,) bool — is it actually on the terrain?veh.wheels_grounded / veh.all_wheels_grounded (v1.5.2) answer "did each
wheel's ray find ground on the last step?" against the raycaster's own
miss sentinel. Use them rather than thresholding veh.distances yourself — a
miss reads back as the sensor's max_range, which is whatever you configured.
VehicleScene is the recommended entry point — it registers vehicles / static /
dynamic bodies (add_vehicle / add_static / add_dynamic), cameras
(add_camera), L2/L3 batching (solver="batched" default, n_envs=), and the
per-step loop, and owns the physics backend (VehicleScene.init_backend). To drop
below it to the raw gs.Scene + VehiclePhysics / MultiVehiclePhysics layer
(for control it doesn't expose), see docs/api-reference.md
§1 and the two-API-layers note in docs/concepts.md.
On first VehiclePhysics construction, the SDK prints a one-line banner:
[genesis_vehicle v0.6.0] Initialized: 4 wheels, Ackermann, RWD, Independent, n_envs=1, hooks=[RollingResistance, LowSpeedRegularizer]
Telling you, at a glance: version, topology, which strategies are wired up, batch size, and which stability hooks are active.
More examples and the worked-out 10 s SETTLE / ACCEL / TURN / BRAKE scenario
walkthrough are in docs/quickstart.md.
The five composition slots a VehicleConfig exposes. All are independent
of wheel count — pick any combination as long as the soft per-strategy
constraints hold (Ackermann needs an L + R wheel pair on each steered
axle; SkidSteer and SameSideBelt need every wheel to declare side='L'
or side='R').
| Slot | Concrete options shipped |
|---|---|
| Steering | Ackermann, PartialAckermann, SkidSteer, NoSteer |
| Drivetrain | FWD, RWD, AWD, PerSide |
| Coupling | Independent, SameSideBelt |
| Tire model | PacejkaAnisotropic, CoulombIsotropic |
Wheel contact (via add_vehicle(wheel_contact=), v1.6.0) |
"point" (default, one ray per wheel), "swept_envelope" (M rays reduced by the swept-circle lower envelope) |
Stability hooks (via stability= profile) |
RollingResistance, LowSpeedRegularizer, StaticFrictionLock |
Subclass any of the ABCs (SteeringStrategy, DrivetrainStrategy,
CouplingStrategy, TireModel, StabilityHook) to add new behaviors.
Genesis only gives each link's world pose. For telemetry → animation
retargeting, attaching external sensors/effects, or placing ghost copies you
usually want each component relative to its URDF parent. get_link_transforms
(or physics.link_transforms(...)) composes that for you:
from genesis_vehicle import get_link_transforms
lt = get_link_transforms(car, frame="parent") # "world" | "base" | "parent"
lt.names # ['base_link', 'front_left_axle_carrier', ...]
lt.parent_local # parent link's local index per link, -1 for the root
lt.pos # (n_envs, n_links, 3)
lt.quat # (n_envs, n_links, 4) wxyz
lt.matrices() # (n_envs, n_links, 4, 4)frame |
each link relative to |
|---|---|
"world" |
world (raw Genesis output) |
"base" |
the entity base/root link |
"parent" (default) |
its immediate URDF parent link (hierarchy-local; root → world) |
Vectorised over n_envs and n_links — no per-link Python loop.
Four ready-to-use VehicleConfig builders:
| Function | Wheels | Steering | Drivetrain | Coupling |
|---|---|---|---|---|
car_4w_fwd_ackermann |
4 | Ackermann front | FWD | Independent |
car_4w_rwd_ackermann |
4 | Ackermann front | RWD | Independent |
car_4w_awd_ackermann |
4 | Ackermann front | AWD | Independent |
truck_6w_partial_ackermann |
6 | Ackermann on axle 0 | Drive on axles 1+2 (mid + rear) | Independent |
tank_skid_belt |
any (validated 10 / 14) | SkidSteer | PerSide (gear cap 0.3) | SameSideBelt |
These cover the reference patterns the SDK was built from; copy and tweak
for your own topology. Every preset takes a keyword-only
stability="control" | "raw" | "research" argument (default "control"
— see docs/stability-profiles.md).
20 runnable sample programs under samples/ cover the most
common call patterns (counting basis: every samples/*.py with a __main__
entry point, excluding the _hud.py / tank_tuning.py helpers and the
deprecated two_scene_terrain alias). Full catalog with descriptions + viewer
support in samples/README.md. All run as Python modules
right after git pull — they depend only on the SDK and the bundled
samples/urdf/car_4w.urdf; the ten below are the
most common entry points:
python -m genesis_vehicle.samples.quickstart # 1. minimum-viable API
python -m genesis_vehicle.samples.slope_hold # 2. StaticFrictionLock regression
python -m genesis_vehicle.samples.batched_rollout # 3. n_envs > 1 API (L3)
python -m genesis_vehicle.samples.road_loop # 4. multi-vehicle loop (L2)
python -m genesis_vehicle.samples.perf_vectorization # 5. L3 scaling bench (headless)
python -m genesis_vehicle.samples.multi_env_render # 6. all parallel envs in a grid
python -m genesis_vehicle.samples.perf_multi_vehicle # 7. L2 scaling bench (headless)
python -m genesis_vehicle.samples.perf_l2_l3_combined # 8. L2 × L3 scaling bench (headless)
python -m genesis_vehicle.samples.city_traffic_ego # 9. ego + traffic highway (L2 × L3)
python -m genesis_vehicle.samples.bench_raycast_mode # 10. raycast_mode benchmark (fail-closed)Most scripts accept --viewer for an offscreen camera render; the four
benchmarks (perf_vectorization, perf_multi_vehicle, perf_l2_l3_combined,
bench_raycast_mode) are intentionally headless (rendering would distort the
numbers). Since v0.7.14 the samples set
cfg.enable_wheel_joint_internal_sync = args.viewer (it defaults to False now), so
headless runs skip the per-step viewer-joint FK and report pure-physics
timing. The catalog samples/README.md has the
per-sample viewer status, the --viewer ↔ enable_wheel_joint_internal_sync note, and
pointers to which sample is the right visual counterpart for each benchmark.
Detailed docs live under docs/:
| Page | What's in it |
|---|---|
docs/index.md |
Documentation home — full TOC |
samples/README.md |
Catalog of all 20 runnable samples + viewer support matrix |
docs/quickstart.md |
Minimal example, runnable end-to-end |
docs/concepts.md |
Mental model: 5-step pipeline, ISO 8855, hook intuition, batched-by-default |
docs/batching.md |
The L1 / L2 / L3 vectorization axes — when to use which, measured speedups, the L2 × L3 combined pattern |
docs/tire-and-contact.md |
Tire model (Pacejka vs Coulomb) & ground-contact mechanism (raycast wheel etc.) — the two axes, standard names, recommendation |
docs/server.md |
OSC physics server for external clients (Unreal / Unity) — purpose, modes, full wire-schema reference |
docs/api-reference.md |
Full public API surface (every class + function + default) |
docs/pipeline-and-hooks.md |
Hook insertion points in the 5-step pipeline |
docs/stability-profiles.md |
control / raw / research profiles + the "one rule" for MPPI / Real2Sim |
docs/physics-contracts.md |
Brake sign, non-negative N, i_wheel truth policy, steering / coupling order, URDF contracts (§7.9) |
docs/testing.md |
Test inventory + module map |
genesis_vehicle/
├── README.md <-- you are here
├── LICENSE Apache 2.0
├── CHANGELOG.md per-version release notes
├── __init__.py single-import public surface
├── _version.py __version__ / VERSION_INFO source of truth
├── core.py VehiclePhysics — 5-step pipeline orchestrator
├── config.py WheelConfig / VehicleConfig / resolve()
├── inputs.py VehicleInputs + typed inputs
├── urdf.py parse_urdf() + URDFParsedConfig
├── raycast.py WheelRayPattern
├── dynamics.py brake_torque_signed / suspension_normal_force
├── visual.py WheelJointInternalSync (viewer wheel-joint driver)
├── tire_models/ TireModel ABC + Pacejka + Coulomb
├── strategies/ steering / drivetrain / coupling / stability
├── presets.py 4 ready-to-use VehicleConfig builders
├── docs/ the wiki
└── tests/ pure-Python unit tests (no Genesis needed)
From the repo root:
python -m pytest tests/ -v605 tests covering URDF parsing, config resolve, suspension sizing, strategy
math, dynamics primitives, version reporting, stability-profile semantics,
multi-vehicle grouping/input routing, the ray-MISS sentinel and grounded
predicates, the swept-envelope wheel contact (including a bit-identity check on
the DEFAULT contact path), the config-rebuild state carry and the reset
semantics (v1.6.2), the raycast-mode benchmark harness (schedule,
gates, pairing and publication rule, driven through an injected runner), the
build-time config derivations staying live under a post-build() write
(v1.6.4), simulation time being fixed at build() and the server's
sim_options write guard (v1.6.5), the L2 serving counters and the
post-override capture skip gate (v1.6.6), the L2 --send-hz state-send gate
(v1.6.7 — the rate limiter's realised rate, the frozen gate body and the new
[SERVE] keys), the wall-clock [STATS]/[SERVE] window and the benchmark's
pooled weighting (v1.6.8 — the trigger in both modes, a byte-identity freeze of
both [STATS] strings, the bare [SERVE] [L3] tag, the shared
--stats-interval resolver and its loud clamp, and the re-weighting driven on
windows of deliberately unequal length), and the server subpackage surface.
Almost all pure-Python; a handful build a real VehicleScene on the CPU
backend, so genesis-world must be importable. No GPU needed — measured on the
v1.6.8 tree, 605 passed, 3 skipped in 135.07s (CPU/WSL2, genesis-world 1.4.0,
pytest tests/ -q --ignore=tests/test_fusion_probe.py, one run; the 3 skips
are the env-gated live server tests, run them with
GENESIS_VEHICLE_LIVE_SERVER_TEST=1). Earlier:
578 passed, 1 skipped in 137.19s at v1.6.7, 544 passed in 144.45s at
v1.6.6, 519 passed in 144.33s at v1.6.5,
507 passed in 170.53s at v1.6.4, 116 s / 450 tests at
v1.6.2-v1.6.3, 88 s at v1.6.1 — the wall time swings more between runs than
between releases, so treat it as an order of magnitude, not a metric.
Semantic Versioning (pre-1.0, so minor bumps may
break). Current version: genesis_vehicle.__version__. Per-release notes
in CHANGELOG.md.
genesis_vehicle is released under the Apache License 2.0, matching the upstream Genesis physics engine.