Machine-learning decision-making for autonomous traffic agents. Drivelink trains lightweight RandomForest models that decide when a car should change lanes, take an exit, or negotiate a merge, and serves those decisions in real time over WebSocket to a Godot driving simulation.
The repo has two halves:
- Real-time ML backend (repo root) — trains the models, loads them, and exposes them to the Godot game client over a simple WebSocket protocol. This is what runs during gameplay.
- Research simulation (
sim/) — runs the same models as a vehicle policy inside SUMO (validated IDM + LC2013 dynamics) and measures how realistic the resulting traffic is against real-world trajectory data. This is the "defensible realism" path for research/product claims.
┌──────────────────────────── REAL-TIME PATH (gameplay) ───────────────────────────┐
train_models.py ──► models/*.pkl ──► model_manager.py ──► ml_server.py ──► Godot client
(generate data, (persisted (load models, (WebSocket (Car.gd / MLClient.gd
fit & save once) RandomForests) validate input, server on send car_state, act on
predict) ws://:8765) MERGE / WAIT decisions)
└──────────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────── RESEARCH PATH (validation) ──────────────────────────┐
SUMO (IDM + LC2013) ──► sim/state_mapping ──► sim/policy (reuses ModelManager) ──► sim/runner
realistic dynamics SUMO vehicle → car_state → MERGE / WAIT steps sim,
Drivelink car_state records trajectories
│
sim/metrics + sim/validate ◄───────────────────────────┘
realism metrics vs real data (HighD / NGSIM)
└──────────────────────────────────────────────────────────────────────────────────┘
The ML models, model_manager.py, and the WebSocket server are shared
unchanged between both paths — the research harness imports ModelManager
directly rather than reimplementing inference.
All three models are RandomForestClassifiers that take the same 5 features
and output a binary act / don't-act decision plus a calibrated confidence.
| Model | File | Decision | Trained on |
|---|---|---|---|
| Lane change | models/model_lane_change.pkl |
MERGE / WAIT |
general lane-changing in a multi-lane flow |
| Turning (exit) | models/model_turning.pkl |
MERGE / WAIT |
reaching an exit lane before the exit |
| V2V chat | models/model_v2v_chat.pkl |
NEGOTIATE_MERGE / HOLD_POSITION |
cooperative merge where the neighbour yields a slightly smaller gap |
Rather than learning from hidden ground truth, the models are trained on what a
car actually perceives — a coherent perception-based agent. Each model is
fit on a small micro-simulation (train_models.py) where vehicles decide using
the rule:
Move toward the target lane when the move is needed (
rel_target ≠ 0), the perceived gap is large enough for this driver'saggressiveness, and there is enoughurgency(or the driver is very aggressive).
perceived_gap is a continuous distance in metres matching exactly what the
Godot client (Car.gd) sends at runtime, with a small Gaussian perception
jitter so the returned confidence stays smooth and well-calibrated. This
removes the artificial noise ceiling of the old random-data approach and yields a
clean decision boundary (~0.99 held-out accuracy).
| Feature | Type | Range | Description |
|---|---|---|---|
speed |
float | 0–25 m/s | Current velocity |
urgency |
float | 0–1 | How critical the maneuver is (0 = relaxed, 1 = critical) |
aggressiveness |
float | 0–1 | Driver personality (0 = cautious, 1 = aggressive) |
rel_target |
int | −2…+2 | Lanes to target relative to current (−1 = left, +1 = right) |
perceived_gap |
float | 0–40 m | Distance to nearest car in the target lane. ~5 m = tight, >10 m = comfortable, 40 = clear road. Larger ⇒ more likely to merge. |
Missing features are filled with safe defaults and coerced to numeric, so partial or loosely-typed payloads from the game client never crash the server.
# 1. Install backend dependencies
pip install -r requirements.txt
# 2. Train and persist all three models (one-time; writes models/*.pkl)
python train_models.py
# 3. Start the WebSocket server for Godot
python ml_server.py
# → Drivelink ML WebSocket Server started on ws://0.0.0.0:8765
# 4. (optional) Verify it works from another terminal
python test_client.py| Variable | Default | Purpose |
|---|---|---|
DRIVELINK_HOST |
0.0.0.0 |
Interface to bind |
DRIVELINK_PORT |
8765 |
Port to listen on |
DRIVELINK_PORT=9999 python ml_server.pyThe server logs structured messages, handles each message in isolation (one bad
request never kills the connection), and shuts down cleanly on Ctrl+C /
SIGTERM.
Connect to ws://<host>:<port> and exchange JSON messages. The wire protocol is
stable so the existing Godot MLClient keeps working.
Request
{
"type": "predict_lane_change",
"car_state": {
"speed": 15.5,
"urgency": 0.75,
"aggressiveness": 0.85,
"rel_target": 1,
"perceived_gap": 15
}
}type may be predict_lane_change, predict_turning, or predict_v2v.
Response
{
"type": "prediction",
"scenario": "lane_change",
"result": {
"decision": "MERGE",
"confidence": 0.92,
"model": "lane_change",
"raw_prediction": 1,
"target_lane": 3
}
}{
"type": "batch_predict",
"scenario": "lane_change",
"car_states": [
{"speed": 12, "urgency": 0.3, "aggressiveness": 0.7, "rel_target": 1, "perceived_gap": 18},
{"speed": 18, "urgency": 0.8, "aggressiveness": 0.9, "rel_target": -1, "perceived_gap": 3}
]
}| Request | Response | Purpose |
|---|---|---|
{"type": "get_models"} |
{"type": "models_info", "models": {...}} |
Metadata about loaded models |
{"type": "ping"} |
{"type": "pong", "status": "alive"} |
Health check |
Any unknown or malformed request returns {"error": "..."} instead of dropping
the connection.
For full GDScript integration examples, see GODOT_INTEGRATION.md.
Runs the Drivelink models as a decision policy on top of validated SUMO dynamics and validates the result against real trajectory data. See sim/README.md for full details.
pip install -r sim/requirements.txt # SUMO ships in the eclipse-sumo wheel
# Highway forced-merge with the ML policy, validated against the SUMO baseline
python -m sim.run --scenario highway --mode ml --validate
# Urban signalised grid, ML turning policy
python -m sim.run --scenario urban --mode ml --validate
# Watch it live
python -m sim.run --scenario urban --mode ml --gui| Scenario | Network | Models exercised |
|---|---|---|
highway |
3-lane freeway dropping to 2 lanes (forced merging) | lane-change |
urban |
signalised 3×3 grid with turning demand | turning |
Outputs land in sim/runs/: trajectory CSVs, a JSON realism report, and a
figure overlaying simulated vs reference distributions. Realism is scored with
KS statistic and Wasserstein distance across speed, time headway,
time-to-collision, gap acceptance, and the fundamental diagram. To validate
against real data (HighD / NGSIM), pass --reference highd --reference-path /path/to/tracks.csv.
.
├── train_models.py # Generate data, train & persist all 3 models
├── model_manager.py # Load models, validate input, run inference
├── ml_server.py # WebSocket server (env-configurable host/port)
├── test_client.py # Python client exercising every request type
├── requirements.txt # Backend dependencies
├── models/ # Persisted RandomForests + feature lists (*.pkl)
├── GODOT_INTEGRATION.md # GDScript usage, feature reference, troubleshooting
│
├── sim/ # SUMO research simulation + real-data validation
│ ├── run.py # CLI entry point (python -m sim.run ...)
│ ├── runner.py # steps SUMO, applies decisions, records trajectories
│ ├── policy.py # reuses ModelManager as the lane-change policy
│ ├── state_mapping.py # SUMO vehicle → Drivelink car_state
│ ├── metrics.py # realism metrics (KS / Wasserstein)
│ ├── validate.py # compares simulated vs reference trajectories
│ ├── datasets.py # HighD / NGSIM column mappings
│ ├── scenarios/ # highway + urban SUMO networks (+ builder)
│ └── requirements.txt # SUMO + scipy + matplotlib (+ core ML deps)
│
├── Scripts/, Scripts_All/ # Godot-side GDScript (Car.gd, MLClient.gd, HUD.gd, …)
└── traffic_*.py, dataset_lane.py # Legacy standalone experiments (superseded
# by train_models.py; kept for reference)
- Python 3.9+
- Backend:
numpy,pandas,scikit-learn,joblib,websockets(seerequirements.txt) - Research sim (optional):
eclipse-sumo,libsumo,traci,sumolib,scipy,matplotlib(seesim/requirements.txt) — SUMO ships inside theeclipse-sumowheel, so no system install is needed.
| Symptom | Fix |
|---|---|
No models loaded on startup |
Run python train_models.py first; confirm models/*.pkl exist |
| Port already in use | Start with a different port: DRIVELINK_PORT=9999 python ml_server.py |
| Godot can't connect | Ensure the server is running and the URL matches; use the host IP instead of localhost if the game runs on another machine |
sklearn feature-name warnings |
Already fixed — models are trained and served on named DataFrame columns |
Note on line endings: the tracked Python files use CRLF. When applying patches to this repo, prefer
git applywith the original (CRLF) patch —git amstrips the carriage returns during mail parsing and the patch will fail to apply.