Skip to content

Feat/early pruning footpaths - #369

Open
andrii-rohovyi wants to merge 3 commits into
motis-project:masterfrom
andrii-rohovyi:feat/early-pruning-footpaths
Open

Feat/early pruning footpaths#369
andrii-rohovyi wants to merge 3 commits into
motis-project:masterfrom
andrii-rohovyi:feat/early-pruning-footpaths

Conversation

@andrii-rohovyi

Copy link
Copy Markdown

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.

seed baseline (mean of 2 runs) early pruning speedup baseline run-to-run noise
42 401.8 ms 361.2 ms 10.1 % 7.9 %
100 396.2 ms 339.7 ms 14.3 % 2.7 %
200 444.0 ms 397.5 ms 10.5 % 1.9 %
mean 414.0 ms 366.1 ms 11.6 %

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

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).
@felixguendling

Copy link
Copy Markdown
Member

Our footpaths are not transitive. Isn’t this a precondition?

@andrii-rohovyi

Copy link
Copy Markdown
Author

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

@felixguendling

Copy link
Copy Markdown
Member

Thanks for the explanation.

Why isn’t it enabled also for vias > 0?

@felixguendling

Copy link
Copy Markdown
Member

Aachen network isn’t really relevant for benchmarking. How does it look like on Transitous data or at least Germany?

@andrii-rohovyi

Copy link
Copy Markdown
Author

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 feeds/de.json), 690,637 stops.

nigiri-benchmark, n = 1000 queries/run, 3 seeds, default settings (max_footpath_length = 20).

seed baseline (mean of 2 runs) early pruning speedup baseline noise
42 11507 ms 10167 ms 11.6 % 9.7 %
100 11492 ms 10469 ms 8.9 % 2.2 %
200 10886 ms 9815 ms 9.8 % 2.5 %
mean 11295 ms 10150 ms 10.1 %

Please let me know what you think about it.

@andrii-rohovyi

Copy link
Copy Markdown
Author

Thanks for the explanation.

Why isn’t it enabled also for vias > 0?

The EP relies on fp_target_time being monotonic with footpath duration: footpaths are sorted in ascending order by duration, so once one can't beat the destination bound, one can't beat it anymore, which is what makes it safe to stop scanning.

As I understand it from the codebase, monotonicity holds only for Vias == 0. Please correct me if I am wrong.

@mority

mority commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

I ran an a quick experiment on a transitous dataset from March 2026. max_footpath_length: 12 like we currently use in production. Average speedup is ~1.5%. Some quantiles faster, some slower. Batch was run with maximum parallelism, so might be jitter.

mority@turing:~/data/transitous$ ./motis generate -b europe -n 500
date range: [2026-03-12 00:00:00, 2026-03-26 00:00:00], tt=[2026-03-12 00:00, 2027-03-13 00:00[
station-to-station
in bounds: 3514609/4957839 stops
from and to pairings by lower bounds rank
generating 500 queries: [■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ ] 100%

mority@turing:~/data/transitous$ ./motis batch -r responses_baseline.txt

response_time
=============

response_time
      average:       11,064.71
          max:       97,842
  99 quantile:       73,339
  90 quantile:       32,778
  80 quantile:       15,183
  50 quantile:        4,122
          min:            2

mority@turing:~/data/transitous$ ./motis batch -r responses_early_pruning.txt

response_time
=============

response_time
      average:       10,906.86
          max:      100,396
  99 quantile:       74,001
  90 quantile:       32,503
  80 quantile:       14,856
  50 quantile:        3,864
          min:            1

mority@turing:~/data/transitous$ ./motis compare -q queries.txt -r responses_baseline.txt -r responses_early_pruning.txt 
fail is not a directory, not writing fails
consumed: 500
   equal: 500

@andrii-rohovyi

Copy link
Copy Markdown
Author

I ran an a quick experiment on a transitous dataset from March 2026. max_footpath_length: 12 like we currently use in production. Average speedup is ~1.5%. Some quantiles faster, some slower. Batch was run with maximum parallelism, so might be jitter.

mority@turing:~/data/transitous$ ./motis generate -b europe -n 500
date range: [2026-03-12 00:00:00, 2026-03-26 00:00:00], tt=[2026-03-12 00:00, 2027-03-13 00:00[
station-to-station
in bounds: 3514609/4957839 stops
from and to pairings by lower bounds rank
generating 500 queries: [■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ ] 100%

mority@turing:~/data/transitous$ ./motis batch -r responses_baseline.txt

response_time
=============

response_time
      average:       11,064.71
          max:       97,842
  99 quantile:       73,339
  90 quantile:       32,778
  80 quantile:       15,183
  50 quantile:        4,122
          min:            2

mority@turing:~/data/transitous$ ./motis batch -r responses_early_pruning.txt

response_time
=============

response_time
      average:       10,906.86
          max:      100,396
  99 quantile:       74,001
  90 quantile:       32,503
  80 quantile:       14,856
  50 quantile:        3,864
          min:            1

mority@turing:~/data/transitous$ ./motis compare -q queries.txt -r responses_baseline.txt -r responses_early_pruning.txt 
fail is not a directory, not writing fails
consumed: 500
   equal: 500

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.

@mority

mority commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

pong exceptions when using osr routed transfers, needs further investigation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants