Label each station for each number of legs - #25
Merged
Merged
Conversation
Walking on from each footpath as it was taken reached stations through a neighbour, walked on from them, then reached them sooner directly and walked on from them all over again. Over the GB rail benchmark queries each footpath was set about four and a half times a scan. Taking them all first gives the same journeys and scans 12-25% faster. Claude-Session: https://claude.ai/code/session_01Pqqed3uofhXmvwhRLgu8sS
A station kept only its earliest arrival, with legs breaking ties, so an arrival a few minutes later in fewer legs was thrown away even when it made the same onward train. Each station now keeps the soonest it is reached in at most each number of legs, up to maxLegs with the last holding that many or more, and a trip is boarded from the fewest legs in time for it. A journey is walked back through the labels, and each destination's is its earliest arrival in the fewest legs that arrive then. The labels are the factory's and reused by every scan, as the trip arrays already were: allocating them for each scan cost about 15%. Their times are when a passenger can board, arrival plus interchange, so rejecting a connection reads one number. Over the benchmark queries the arrival at every destination is unchanged, 172 of 5,643 journeys take fewer legs and none more, and getCompactedLegs and getStraightenedLegs change none of them. Claude-Session: https://claude.ai/code/session_01Pqqed3uofhXmvwhRLgu8sS
getCompactedLegs and getStraightenedLegs made up for the scan only knowing the legs of each station's earliest arrival. With a label per number of legs a journey is already on the fewest legs and changes at the latest call, and over the benchmark queries neither changed any journey. Claude-Session: https://claude.ai/code/session_01Pqqed3uofhXmvwhRLgu8sS
Every label of an origin held its departure time, but walking out of an origin is charged its interchange time where boarding a train there is not. Reaching the origin some other way within that time was discarded as no sooner, even when walking on from it would be. Only the origin's label of no legs now holds its departure time. With its other labels open, a footpath from an origin and back could leave each station reached by the other's footpath, and walking on from them went back and forth for ever. A station is only walked on from by a footpath in the legs its label was reached in. Claude-Session: https://claude.ai/code/session_01Pqqed3uofhXmvwhRLgu8sS
The last label held that many legs or more without saying how many, so boarding a trip again from a call it had carried the passenger to cost no extra leg. The tie went to the later call, and one ride became two legs, which getCompactedLegs used to merge back. Past the last label an equal arrival also could not be told apart on legs. The last label now keeps its real number of legs, trips and footpaths count legs past maxLegs, and an equal arrival in fewer legs replaces the last label, as the one label per station of 3.0.1 did. Over the benchmark queries maxLegs of 8 gives the same journeys as before, 2 no longer splits 40 rides or adds legs to 376 journeys, and 1 gives exactly 3.0.1's journeys. Claude-Session: https://claude.ai/code/session_01Pqqed3uofhXmvwhRLgu8sS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The scan kept one label per station: its earliest arrival, with legs only breaking ties between equal
arrivals. An arrival a few minutes later in fewer legs was thrown away even when it made the same
onward train, and every journey on from it inherited the extra legs.
getCompactedLegsandgetStraightenedLegsrepaired some of those journeys afterwards, but couldn't recover a label the scannever kept.
For example, KDG → SEA on 2026-09-15 departing 14:03 returned
KDG>MAN | MAN 15:00>HUD 15:41 [C24728] | HUD 15:46>EAG 17:22 [C24619] | EAG 17:34>SEA [Y14852].The scan reached YRK at 16:37 in 3 legs via HUD, so it discarded C24728's 16:41 arrival there in 2.
Y14852 could then be boarded at YRK or EAG in 3 legs, and the tie went to the later call. Straightening
couldn't help, because C24728 never reaches EAG. The same 18:10 arrival is possible in 3 legs:
KDG>MAN | MAN 15:00>YRK 16:41 [C24728] | YRK 16:52>SEA [Y14852].Fix
A label per number of legs
maxLegslegs (8 by default, set onScanResultsFactory). The last label holds that many legs or more and keeps how many, so pastmaxLegsa station still has its earliest arrival in the fewest legs, as 3.0.1's single label did.Trips and footpaths count real legs, so boarding a trip again from a call it carried the passenger to
always costs a leg more than staying aboard. With
maxLegsof 1 the journeys are 3.0.1's.but walking out of an origin is charged its interchange time, so the origin may still be reached
another way in time to walk on sooner (only possible when origins set off at different times).
whose label is in time for it, and a label is only kept if it is sooner than every label with as many
legs or more.
origin. Rejecting a connection reads one number.
(equal arrival, equal legs, stay aboard) still applies.
station is only walked on from by a footpath in the legs its label was reached in; otherwise a
footpath from an origin and back can walk back and forth for ever.
Journeys
JourneyFactorywalks back through the labels. Each label is one leg, and the label before it is thefewest legs its station was reached in, in time for that leg.
getJourneysreturns one journey per destination: the earliest arrival, in the fewest legs thatarrive then.
getCompactedLegsandgetStraightenedLegsare removed. Over the benchmark queries neither changeda single journey once the scan kept labels per leg count.
Stopping
destination (later but fewer legs) needs the scan to run on until every destination has a 1-leg
journey. That is usually the end of the timetable, and was 3.8× slower. A scan without destinations
(transfer-pattern-planner) runs to the end anyway, so it can read every label.
Speed
taken reached stations through a neighbour, walked on, then reached them sooner directly and walked
on all over again: about 4.5 sets per footpath per scan. This commit gives identical journeys on its
own.
ScanResultsFactoryand are filled afresh per scan, like the trip arrays.Allocating them per scan cost about 15%. The index a scan returns is only good until the next scan.
Verification
test/performance.ts's 24 queries at 46 departure times (every 30 minutes from 00:00) on 2026-09-15over the GB rail feed, 1,104 queries, 5,643 journeys:
getCompactedLegs/getStraightenedLegs(before removing them)KDG → SEA at 14:03 now returns the 3-leg journey above.
With a lower
maxLegs, against 3.0.1:maxLegsSpeed: all variants ran at once, each pinned to its own core, best of 3 passes, 3 repeats. Sequential
runs varied by ±10% on this machine.
The labels cost about 3% on point-to-point scans and 4–9% on full scans, against footpaths only.
Point-to-point scans end up about 25% faster than 3.0.1, and full scans about level with it.
The origin and last-label fixes, measured in a separate run of the same kind: before 2.35–2.39 s and
9.77–10.07 s, after 2.36–2.55 s and 9.71–10.12 s, with 3.0.1 at 2.94–3.13 s and 9.28–9.89 s.
Tests, the planning ones failing on 3.0.1:
maxLegs.the exact-legs check).
ScanResults: a later arrival in fewer legs kept alongside a sooner one, a label no sooner in morelegs rejected, transfers per number of legs, whether a station is still reached by a transfer.
Notes:
ConnectionIndexis now{ levels, boardingTimes, connections }indexedby
station * levels + legs.ScanResults.setConnectionreturns legs, andisTransferBetter,setTransferandisReachedByTransfertake the legs walked from. By.changeset/README.mdan exportchange is breaking, so this could be a major instead.
old index, and needs updating. Reading every label rather than one per station is what gives it the
fewer-change patterns raptor finds.
https://claude.ai/code/session_01Pqqed3uofhXmvwhRLgu8sS