Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/label-each-number-of-legs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"connection-scan-algorithm": minor
---

Return each destination's earliest arrival in the fewest legs. The scan kept one label per station,
its earliest arrival, so 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 took the extra legs. A station now keeps a
label for each number of legs, up to `maxLegs` (8 unless `ScanResultsFactory` is given another, the
last holding that many or more and keeping how many), and a trip is boarded from the fewest legs that
are in time for it. With `maxLegs` of 1 the journeys are 3.0.1's.
Over the GB rail benchmark queries the arrivals are unchanged and 172 of 5,643 journeys take fewer
legs. `JourneyFactory` no longer rewrites the legs of a journey after the scan.

Footpaths out of a station are all taken before any is walked on from, and the labels are allocated
once per `ScanResultsFactory` rather than per scan, so point to point scans are about 25% faster.

`ConnectionIndex` is now the labels: `levels` per station, with `boardingTimes` and `connections`
at `station * levels + legs`. It is reused by the next scan from the same `ScanResultsFactory`.
`ScanResults.setConnection` returns the legs a station was reached sooner in, or 0.
`isTransferBetter`, `setTransfer` and `isReachedByTransfer` take the legs of the station the footpath
is walked from.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,21 @@ The algorithm is the paper's: connections sorted by arrival, read in order, each
be reached and gets somewhere sooner. What makes it quick is what it reads.

`toGtfsData` numbers the stations, and holds the connections as parallel arrays of those numbers and
times rather than as an object each. `ScanResults` keeps its earliest arrivals and the connection
achieving each in arrays indexed by station, and a connection or footpath is its index, so every
times rather than as an object each. `ScanResults` keeps a label for each station and number of legs:
the soonest the station is reached in at most that many legs, and the connection achieving it. They
are held in arrays indexed by station and legs, and a connection or footpath is its index, so every
question the scan asks is a few array reads. A scan starts at the first connection arriving after the
departure time and stops once every destination has been reached before the connection it is on
arrives, and whether each trip runs is worked out once per date rather than asked of every connection.

A label per number of legs is what finds the journey with the fewest changes. An arrival a few minutes
later in fewer legs can still make the same onward train, so a station keeps it alongside the sooner
arrival, and a train is boarded from the fewest legs that are in time for it. `JourneyFactory`
returns each destination's earliest arrival, in the fewest legs that arrive then. The labels go up to
`maxLegs`, 8 unless `ScanResultsFactory` is given another. The last holds that many legs or more and
keeps how many, so a longer journey is still found in the fewest legs that arrive as soon, but past
`maxLegs` a later arrival in fewer legs is not kept.

### Stations and platforms

A connection runs between stations, because that is where interchange time and footpaths are
Expand Down
59 changes: 44 additions & 15 deletions src/csa/ConnectionScanAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class ConnectionScanAlgorithm {
}

/**
* Return an index of connections that achieve the earliest arrival time at each station.
* Return an index of how each station was reached soonest in each number of legs.
*/
public scan(
origins: OriginDepartureTimes,
Expand All @@ -37,16 +37,18 @@ export class ConnectionScanAlgorithm {
const departureTime = Math.min(...Object.values(origins));

for (const origin of results.getOrigins()) {
this.scanTransfers(results, origin);
this.scanTransfers(results, origin, 0);
}

for (let c = firstArrivingAt(this.connections, departureTime); c < this.connections.length; c++) {
if (results.isFinished(c)) {
break;
}
if (running[this.connections.trip[c]] && results.isReachable(c) && results.isBetter(c)) {
if (results.setConnection(c)) {
this.scanTransfers(results, this.connections.arrivalStation[c]);
const legs = results.setConnection(c);

if (legs !== 0) {
this.scanTransfers(results, this.connections.arrivalStation[c], legs);
}
}
}
Expand All @@ -55,27 +57,54 @@ export class ConnectionScanAlgorithm {
}

/**
* Walk every footpath out of a station whenever it is reached earlier or in fewer legs than it
* was, not only the first time: a station first reached on foot is often then reached sooner by
* Walk every footpath out of a station whenever it is reached sooner in some number of legs than
* it was, not only the first time: a station first reached on foot is often then reached sooner by
* train, and the footpaths onwards from it have to start from the earlier time and count on from
* the fewer legs.
* the legs it was reached in.
*
* Every footpath out of the station is taken before any is walked on from. Walking on from each as
* it is taken would walk on from a station reached through a neighbour, only to reach it sooner
* directly and walk on from it all over again.
*/
private scanTransfers(results: ScanResults, origin: StopIdx): void {
for (let t = this.transfers.offsets[origin]; t < this.transfers.offsets[origin + 1]; t++) {
if (results.isTransferBetter(t)) {
results.setTransfer(t);
this.scanTransfers(results, this.transfers.destination[t]);
private scanTransfers(results: ScanResults, origin: StopIdx, legs: number): void {
const start = this.transfers.offsets[origin];
const end = this.transfers.offsets[origin + 1];

for (let t = start; t < end; t++) {
if (results.isTransferBetter(t, legs)) {
results.setTransfer(t, legs);
}
}

for (let t = start; t < end; t++) {
if (results.isReachedByTransfer(t, legs)) {
this.scanTransfers(results, this.transfers.destination[t], results.getLegsAfterWalking(legs));
}
}
}

}

/**
* How the earliest arrival at each station was made, by station index: the connection its trip was
* boarded from, or the footpath, or NO_CONNECTION where nothing reaches it. Each is one leg.
* How each station was reached soonest in each number of legs. A station has a row of `levels` labels,
* so the label of a station in some legs is at `station * levels + legs`: the soonest it was reached
* in at most that many. The first label is the origins, reached in none, and the only label an origin's
* departure time is in. The last holds that many legs or more.
*/
export type ConnectionIndex = Int32Array;
export interface ConnectionIndex {
levels: number;
/**
* When a passenger reaching the station by the label can board a trip there: the arrival plus the
* station's interchange time, or the departure time at an origin. NOT_REACHED where nothing reaches
* it in so few legs.
*/
boardingTimes: Int32Array;
/**
* The connection the last leg's trip was boarded from, or the footpath, or NO_CONNECTION at an origin
* or where nothing reaches the station. Each is one leg.
*/
connections: Int32Array;
}

/**
* Index of departure stations and their departure time
Expand Down
Loading