Feat/early pruning footpaths - #369
Conversation
The footpath list returned by locations_.footpaths_out_/_in_ is sorted
ascending by duration. For Vias==0 (the common case), the projected
arrival time at a footpath's target is monotone in fp.duration():
fp_target_time = tmp_[i][0] + adjusted_transfer_time(fp.duration())
Once this projected time can no longer beat time_at_dest_[k], no later
(longer-duration) footpath can either — we can break the loop.
The check is gated by `if constexpr (Vias == 0)` so via routing is
unaffected. A new stat counter `n_footpaths_pruned_by_ep` is exposed
to measure how often the pruning fires.
Benchmark (Aachen GTFS, 1000 queries x 5 seeds, station-to-station):
Baseline: 336.6 ms/query (mean across seeds)
EP: 325.5 ms/query (mean)
Speedup: ~3.3% (with high seed-to-seed variance; effect scales with
network size and footpath fan-out per stop).
|
Our footpaths are not transitive. Isn’t this a precondition? |
Thanks for the comment, @felixguendling. No, transitive closure is not a precondition here. The pruning is a local early break inside the per-stop footpath loop, so it behaves the same on non-transitive footpaths and produces identical results to the baseline. P.S. The effect is usually bigger in multimodal setups like MOTIS and on large networks. You can see how it played out in OTP for Germany after June 1st, where it brought the query time down from around 4s to 2.3s: https://otp-performance.leonard.io/d/9sXJ43gVk/otp-performance?orgId=1&from=now-1y&to=now&timezone=browser&var-category=transit&var-location=germany&var-branch=dev-2.x |
|
Thanks for the explanation. Why isn’t it enabled also for vias > 0? |
|
Aachen network isn’t really relevant for benchmarking. How does it look like on Transitous data or at least Germany? |
Good point, thank you. I re-ran on the nationwide German network: imported the DELFI Germany GTFS (the source Transitous ingests via
Please let me know what you think about it. |
The EP relies on As I understand it from the codebase, monotonicity holds only for |
|
I ran an a quick experiment on a transitous dataset from March 2026. |
Thank you for the test, @mority. It is a very interesting observation. I checked that I was running the experiment inside an x86-emulated VM by default. Recompiling and running natively dropped my numbers right down to yours. So yeah, it seems that the wall-clock speed-up is highly machine-dependent. Despite that, there still seem to be positive results: early pruning consistently visits ~4% fewer footpaths (n_footpaths_visited). I also do notice your median improved a bit more (+6.3%) than the mean. |
|
pong exceptions when using osr routed transfers, needs further investigation |
Summary
RAPTOR's transfer relaxation phase iterates over all outgoing transfer edges from each updated stop. When the transfer graph is dense (e.g., with long maximum transfer durations like 30–60 minutes), this becomes a significant bottleneck — transfer iteration can dominate query time.
Goal / high-level use-case
Reduce RAPTOR query time by pruning unnecessary transfer edge iterations during the transfer relaxation phase, without requiring additional preprocessing beyond a one-time sort of transfer edges by duration.
Solution
Implement the Early Pruning optimization from our WCTR 2026 paper (https://arxiv.org/abs/2603.12592)
Benchmark Results
Measured on the Aachen network with
nigiri-benchmark, 1,000 queries per run, three seeds. To separate signal from timing noise, each seed runs the baseline binary twice and the early-pruning binary once, interleaved; the "baseline noise" column is the spread between the two baseline runs.Early pruning is faster than both baseline runs in every seed, by more than the baseline's own run-to-run variance — so the ~12 % improvement is a real effect, not measurement noise.
Paper
Rohovyi, Abuaisha, Walsh — "Early Pruning for Public Transport Routing", accepted at WCTR 2026.
https://arxiv.org/abs/2603.12592