Projects / RailPredict — UK Rail Engine

RailPredict — UK Rail Engine

activeRustlive · Darwin firehose

Real-time UK rail data engine in Rust (v1.20.0). Subscribes directly to National Rail's Darwin Push Port STOMP firehose, tracks every train in a lock-free in-memory registry, and predicts delays with a LightGBM model — answering the vast majority of queries from local state and prediction without ever polling the live GBR API.

STATUSactive
LANGUAGERust
METRIClive · Darwin firehose
STACKRust · Darwin Push Port · STOMP · Postgres · sqlx · axum · htmx · maud · LightGBM · ONNX
COMMITS327
STARS0
327 commits‹› Code ↗
M
miki-przygodaMerge pull request #5 from miki-przygoda/feat/os-premium-polish5183da92 weeks ago
·.dockerignore
·.env.example
·.gitattributes
.github
·.gitignore
·CHANGELOG.md
·CLAUDE.md
·Dockerfile
·LICENSE
·Makefile
·README.md
RailPredict
·SECURITY.md
·TODO.md
·deny.toml
deploy
·docker-compose.yml
docs
imports
logs
migrations
models
📄
RailPredict

A UK rail data engine written in Rust. RailPredict subscribes directly to the Darwin Push Port — National Rail's STOMP-based firehose of every train movement in the country — and uses that stream to build an intelligent buffer between users and the Great British Railways API. The vast majority of queries are answered from local state, in-memory cache, and statistical prediction; the only call that ever hits GBR directly is the one that genuinely requires it: final ticket purchase.

The Problem

The GBR API is slow, rate-limited, and expensive to call repeatedly. A naive rail app that hits the live API for every search, every page load, and every status check will feel sluggish and burn through its quota the moment traffic spikes.

Most rail apps are dumb mirrors: ask GBR, show the result, repeat. RailPredict is built on the premise that the vast majority of what a user needs during a journey search can be answered locally — without a single outbound request.

Architecture: Three-Tier Data Model

Rail data is separated into tiers based on how frequently it actually needs to change:

| Tier  | Data                                               | Staleness tolerance | Source                    |
|:------|:---------------------------------------------------|:--------------------|:--------------------------|
| **A** | Timetables, station names, base fares              | Days–weeks          | GTFS static feed          |
| **B** | Predicted delay, likely platform, fare range       | Minutes–hours       | Local history + inference |
| **C** | Live position, seat availability, final price lock | Seconds             | GBR Retail API (live)     |

The goal: by the time a user reaches checkout, all Tier A and Tier B data is already loaded. The single Tier C call happens only when they confirm payment.

How It Stays Efficient
Darwin Push Port (zero-poll reads)

Instead of polling GBR for train positions, RailPredict subscribes to the Darwin Push Port — National Rail's STOMP-based firehose of every train movement in the UK. Incoming XML messages are parsed, filtered, and written to an in-memory registry keyed by RID. Read-only queries are served entirely from that cache.

Urgency State Machine

Not every train deserves the same attention. Each train in the registry is assigned a state that controls how aggressively the system monitors it:

Dormant → Monitored → Active → Critical → Terminal
| State         | Condition                      | Behaviour                       |
|:--------------|:-------------------------------|:--------------------------------|
| **Dormant**   | Departure > 2 h                | Static data only, no live calls |
| **Monitored** | 30–120 min out                 | Darwin stream, history building |
| **Active**    | 0–30 min out                   | High-frequency updates          |
| **Critical**  | < 5 min or disruption detected | Real-time stream, push alerts   |
| **Terminal**  | Departed                       | Evicted from active monitoring  |

External events — signal failures, weather alerts, route-level disruption flags — can force emergency promotion regardless of departure time.

Request Coalescing

If 50 users are watching the same train, one outbound call is made and the result is fanned out to all 50. The coalescer deduplicates concurrent in-flight requests by key and wires late arrivals directly onto the pending future.

Circuit Breaker

If GBR starts returning errors, the system enters Cache Only mode and stops sending requests until GBR recovers. Callers see cached data; GBR sees no additional load.

ABOUT

Real-time UK rail data engine in Rust - Darwin Push Port firehose ingestion, LightGBM delay prediction, and a local-first cache that answers most queries without hitting the live API.

axumdarwin-push-porthtmxlightgbmmachine-learningonyxprometheusreal-timeruststompuk-rail
ACTIVITY
Stars0
Forks0
Commits327
LicenseNOASSERTION
LANGUAGES
Rust69.2%
Python9.8%
HTML8.9%
JavaScript5.9%
CSS5.4%
Makefile0.3%
ARCHITECTURE
  • Zero-poll ingestion — a custom tokio STOMP/TLS client subscribes to the Darwin Push Port firehose (every UK train movement); messages are parsed with quick-xml and filtered by RID into a lock-free DashMap registry with sub-millisecond reads.
  • Two-tier local data model: Tier A (GTFS/RDS static timetables, days–weeks staleness) and Tier B (local statistical + ML delay prediction, minutes–hours) — the vast majority of queries are answered entirely from local state and prediction.
  • Five-state urgency machine per train (Dormant → Monitored → Active → Critical → Terminal); weather volatility (Open-Meteo) and external events trigger emergency promotions.
  • Request coalescer — 50 users watching one train collapse to a single outbound call, fanned back to all 50 via oneshot channels; a typed circuit breaker drops to Cache-Only mode on GBR errors.
  • axum + maud server-rendered HTML with htmx (no JS framework, no build step); SSE live stream at /trains/:rid/live; Prometheus metrics and tower_governor rate limiting (60 req/s per IP).
PREDICTION & ML
  • Two-model LightGBM cascade exported to ONNX: a day-ahead model (no live signal) and a real-time model that folds in the current reported delay — real-time MAE 4.09 min, 72.5% of predictions within ±5 min, near-zero bias.
  • Trained on 3,036,926 real observations from the live Darwin feed (2,662 TIPLOCs), with preceding-service delay correlation as a feature and a trimmed-mean statistical baseline as fallback for unseen stations.
  • Documented bias handling — equal-tier sample weighting plus a measured post-inference offset for the day-ahead model's underestimation; a signed prediction-error histogram and daily MAE trend are surfaced in a /predictions explorer.
ENGINEERING & RIGOR
  • Compile-time correctness — every SQL query is checked against the live Postgres schema (sqlx) and every HTML template is type-checked Rust (maud); mistakes are build errors, not runtime crashes.
  • 357 tests (224 unit + integration suites over a live Postgres sidecar); CI gates on cargo-deny (license + vulnerability), clippy -D warnings, the test suite, and a release build.
  • Full-journey capture — per-stop arrival/recovery deltas accumulated across partial STOMP frames and persisted on deactivation; sticky cancellation detection; ~2,751 GB stations and 81 operators tracked.
  • RailPredict OS — a self-contained, fully offline desktop-style demo (Live Map, Operators, Predictions, Reliability, Replay) that bakes real historical data into a single HTML file.