A lidar-driven autonomous racing environment for reinforcement learning.
Learn to drive a car around procedurally generated tracks using nothing but range-finder beams โ a compact, fast, fully Gymnasium-compatible environment with a shaped, exploit-resistant reward and a trained PPO baseline.
A PPO agent driving a track it has never seen, from lidar alone.
NeuroDrive is a small, self-contained study in reinforcement-learning environment design: the parts that actually decide whether an agent learns something useful โ the observation, the action interface, and above all a reward that can't be gamed. It is deliberately fast (vectorised NumPy physics and sensing, no heavyweight simulator) so a capable policy trains on a laptop CPU in minutes.
- ๐ฎ Clean Gymnasium API โ passes
gymnasium.utils.env_checker, works out of the box with Stable-Baselines3. - ๐ฐ๏ธ Learn from sensors, not the map โ the agent sees a fan of normalised lidar beams plus its own speed and heading; it never gets the track handed to it.
- ๐ฒ Procedural tracks โ every episode is a new smooth closed circuit (periodic Catmull-Rom spline), so the policy has to generalise, not memorise.
- ๐งฎ Shaped, exploit-resistant reward โ forward progress along the centerline, a monotonic lap bonus that cannot be farmed by oscillating across the line, and a crash penalty. (See the reward design note.)
- ๐ Kinematic bicycle-model physics โ smooth, believable car dynamics in a few lines.
- โ Tested & linted โ behaviour tests (including a reward-hack regression test) and CI on Python 3.10โ3.12.
- ๐ฅ One-command GIF / MP4 recording for any policy.
| Untrained (random actions) | Trained PPO |
|---|---|
![]() |
![]() |
| spins out immediately | completes laps on unseen tracks |
git clone https://github.com/yferc/neurodrive.git
cd neurodrive
pip install -e ".[train,media]"Drive with the trained baseline and watch it live:
import gymnasium as gym
import neurodrive # registers "NeuroDrive-v0"
from stable_baselines3 import PPO
env = gym.make("NeuroDrive-v0", render_mode="human")
model = PPO.load("models/best/best_model.zip")
obs, _ = env.reset()
done = False
while not done:
action, _ = model.predict(obs, deterministic=True)
obs, reward, term, trunc, info = env.step(action)
env.render()
done = term or trunc| Component | Meaning |
|---|---|
[0 : n_beams] |
normalised lidar ranges in [0, 1] (1 = clear to max range) |
[n_beams] |
speed normalised by top speed |
[n_beams + 1] |
sin of heading error to the road direction |
[n_beams + 2] |
cos of heading error to the road direction |
The lidar is cast vectorised: every beam is intersected against every track
boundary segment in one NumPy operation (neurodrive/sensors.py).
[steering, throttle] โ continuous. -1/-1 is full-left / full-brake, +1/+1
is full-right / full-throttle.
+ progress forward metres along the centerline this step
+ lap_bonus once per newly-completed lap (monotonic โ never farmable)
- time_cost small per-step cost, so faster is better
- crash_penalty on leaving the track (episode ends)
The lap bonus is only ever paid when the agent reaches a new highest lap.
An earlier version awarded it whenever the lap counter changed, and PPO promptly
discovered it could vibrate across the start line to farm bonuses forever โ a
textbook reward hack, now covered by a regression test (test_no_phantom_laps_when_idle).
python scripts/train.py --timesteps 1200000 --n-envs 8
tensorboard --logdir logs # optional: watch it learnPPO (Stable-Baselines3) with an MLP policy trains a competent driver on CPU.
The best policy by evaluation reward is saved to models/best/.
python scripts/record.py --model models/best/best_model.zip --out docs/media/trained
python scripts/record.py --random --out docs/media/randomPPO trained for 1.2M timesteps (8 parallel envs, ~11 minutes on a laptop CPU), then evaluated with a deterministic policy on 20 random procedurally-generated tracks:
| Metric | Value |
|---|---|
| Mean episode reward | 1128 ยฑ 11 |
| Mean laps per episode | 3.05 (max 4) |
| Mean distance driven | 1097 units / episode |
| Crash rate | 0 / 20 (0%) |
| Full-episode completion | 20 / 20 |
The agent drives every unseen track to the time limit without crashing โ
it generalises across track shapes from lidar alone, rather than memorising a
single circuit. Reproduce with python scripts/train.py then the evaluation
snippet, or just load models/best/best_model.zip.
neurodrive/
โโโ neurodrive/
โ โโโ env.py # NeuroDriveEnv โ the Gymnasium environment
โ โโโ car.py # kinematic bicycle-model dynamics
โ โโโ track.py # procedural closed-track generation + geometry cache
โ โโโ sensors.py # vectorised lidar ray-casting
โ โโโ render.py # pygame rendering (human + rgb_array)
โโโ scripts/
โ โโโ train.py # PPO training
โ โโโ record.py # roll out a policy โ GIF + MP4
โโโ tests/ # behaviour + reward-hack regression tests
โโโ .github/workflows/ci.yml
pip install -e ".[dev]"
pytest -qMIT โ see LICENSE.
