org.opentripplanner.profile.RaptorWorker Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otp Show documentation
Show all versions of otp Show documentation
The OpenTripPlanner multimodal journey planning system
package org.opentripplanner.profile;
import com.google.protobuf.CodedOutputStream;
import gnu.trove.iterator.TIntIntIterator;
import gnu.trove.map.TIntIntMap;
import gnu.trove.map.hash.TIntIntHashMap;
import org.opentripplanner.analyst.cluster.TaskStatistics;
import org.opentripplanner.routing.graph.Graph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
import java.util.stream.IntStream;
/**
* A RaptorWorker carries out RAPTOR searches on a pre-filtered, compacted representation of all the trips running
* during a given time window. It originated as a rewrite of our RAPTOR code that would use "thin workers", allowing
* computation by a generic function-execution service like AWS Lambda. The gains in efficiency were significant enough
* that RaptorWorkers are now used in the context of a full-size OTP server executing spatial analysis tasks.
*
* We can imagine that someday all OTP searches, including simple point-to-point searches, may be carried out on such
* compacted tables, including both the transit and street searches (via a compacted street network in a column store
* or struct-like byte array using something like the FastSerialization library).
*
* This implements the RAPTOR algorithm; see http://research.microsoft.com/pubs/156567/raptor_alenex.pdf
*/
public class RaptorWorker {
private static final Logger LOG = LoggerFactory.getLogger(RaptorWorker.class);
public static final int UNREACHED = Integer.MAX_VALUE;
/**
* Destinations with travel time above this threshold are considered unreachable. Note that this will cut off half of the
* trips to a particular location if half the time it takes less than the cutoff and half the time more. Use in combination
* with the reachability threshold in ProfileRequest to get desired results.
*
* The fact that there are edge effects here implies a problem with our methodology. We should be taking the approach that
* the Accessibility Observatory has taken in which accessibility (rather than travel time) is calculated at each minute.
* Alternatively, we could calculate the full OD matrix for each minute and save that to determine whether a particular destination
* is reachable in less than x minutes on median for any arbitrary cutoff.
*
* This should be a number beyond which people would generally consider transit to be completely unreasonable.
*/
static final int MAX_DURATION = 120 * 60;
/**
* The number of randomized frequency schedule draws to take for each minute of the search.
*
* We loop over all departure minutes and do a schedule search, and then run this many Monte Carlo
* searches with randomized frequency schedules within each minute. It does not need to be particularly
* high as it happens each minute, and there is likely a lot of repetition in the scheduled service
* (i.e. many minutes look like each other), so several minutes' monte carlo draws are effectively
* pooled.
*/
public static final int MONTE_CARLO_COUNT_PER_MINUTE = 2;
/** If there are no schedules, the number of Monte Carlo draws to take */
public static final int TOTAL_MONTE_CARLO_COUNT = 99;
int max_time = 0;
int round = 0;
List timesPerStopPerRound;
int[] timesPerStop;
int[] bestTimes;
/**
* The previous pattern used to get to this stop, parallel to bestTimes. Used to apply transfer rules. This is conceptually
* similar to the "parent pointer" used in the RAPTOR paper to allow reconstructing paths. This could
* be used to reconstruct a path (although potentially not the one that was used to get to a particular
* location, as a later round may have found a faster but more-transfers way to get there). A path
* reconstructed this way will tbus be optimal in the earliest-arrival sense but may not have the
* fewest transfers; in fact, it will tend not to.
*
* Consider the case where there is a slower one-seat ride and a quicker route with a transfer
* to get to a transit center. At the transit center you board another vehicle. If it turns out
* that you still catch that vehicle at the same time regardless of which option you choose,
* general utility theory would suggest that you would choose the one seat ride due to a) the
* inconvenience of the transfer and b) the fact that most people have a smaller disutility for
* in-vehicle time than waiting time, especially if the waiting is exposed to the elements, etc.
*
* However, this implementation will find the more-transfers trip because it doesn't know where you're
* going from the transit center, whereas true RAPTOR would find both. It's not non-optimal in the
* earliest arrival sense, but it's also not the only optimal option.
*
* All of that said, we could reconstruct paths simply by storing one more parallel array with
* the index of the stop that you boarded a particular pattern at. Then we can do the typical
* reverse-optimization step.
*/
int[] previousPatterns;
/** The best times for reaching stops via transit rather than via a transfer from another stop */
int[] bestNonTransferTimes;
RaptorWorkerData data;
/** stops touched this round */
BitSet stopsTouched;
/** stops touched any round this minute */
BitSet allStopsTouched;
BitSet patternsTouched;
private ProfileRequest req;
private long totalPropagationTime = 0;
private FrequencyRandomOffsets offsets;
public RaptorWorker(RaptorWorkerData data, ProfileRequest req) {
this.data = data;
// these should only reflect the results of the (deterministic) scheduled search
this.bestTimes = new int[data.nStops];
this.bestNonTransferTimes = new int[data.nStops];
this.previousPatterns = new int[data.nStops];
Arrays.fill(previousPatterns, -1);
allStopsTouched = new BitSet(data.nStops);
stopsTouched = new BitSet(data.nStops);
patternsTouched = new BitSet(data.nPatterns);
this.req = req;
Arrays.fill(bestTimes, UNREACHED); // initialize once here and reuse on subsequent iterations.
Arrays.fill(bestNonTransferTimes, UNREACHED);
offsets = new FrequencyRandomOffsets(data);
}
public void advance () {
round++;
// timesPerStop = new int[data.nStops];
// Arrays.fill(timesPerStop, UNREACHED);
// timesPerStopPerRound.add(timesPerStop);
// uncomment to disable range-raptor
//Arrays.fill(bestTimes, UNREACHED);
}
/**
* @param accessTimes a map from transit stops to the time it takes to reach those stops
* @param nonTransitTimes the time to reach all targets without transit. Targets can be vertices or points/samples.
*/
public PropagatedTimesStore runRaptor (Graph graph, TIntIntMap accessTimes, int[] nonTransitTimes, TaskStatistics ts) {
long beginCalcTime = System.currentTimeMillis();
TIntIntMap initialStops = new TIntIntHashMap();
TIntIntIterator initialIterator = accessTimes.iterator();
while (initialIterator.hasNext()) {
initialIterator.advance();
int stopIndex = initialIterator.key();
int accessTime = initialIterator.value();
initialStops.put(stopIndex, accessTime);
}
PropagatedTimesStore propagatedTimesStore = new PropagatedTimesStore(graph, this.req, data.nTargets);
// optimization: if no schedules, only run Monte Carlo
int fromTime = req.fromTime;
int monteCarloDraws = MONTE_CARLO_COUNT_PER_MINUTE;
if (!data.hasSchedules) {
// only do one iteration
fromTime = req.toTime - 60;
monteCarloDraws = TOTAL_MONTE_CARLO_COUNT;
}
// if no frequencies, don't run Monte Carlo
int iterations = (req.toTime - fromTime - 60) / 60 + 1;
// if we do Monte Carlo, we do more iterations. But we only do monte carlo when we have frequencies.
// So only update the number of iterations when we're actually going to use all of them, to
// avoid uninitialized arrays.
// if we multiply when we're not doing monte carlo, we'll end up with too many iterations.
if (data.hasFrequencies)
// we add 2 because we do two "fake" draws where we do min or max instead of a monte carlo draw
iterations *= (monteCarloDraws + 2);
ts.searchCount = iterations;
// Iterate backward through minutes (range-raptor) taking a snapshot of router state after each call
int[][] timesAtTargetsEachIteration = new int[iterations][data.nTargets];
// for each iteration, whether it is the result of a schedule or Monte Carlo search, or whether it is an extrema.
// extrema are not included in averages.
boolean[] includeIterationInAverages = new boolean[iterations];
Arrays.fill(includeIterationInAverages, true);
// TODO don't hardwire timestep below
ts.timeStep = 60;
// times at targets from scheduled search
int[] scheduledTimesAtTargets = new int[data.nTargets];
Arrays.fill(scheduledTimesAtTargets, UNREACHED);
// current iteration
int iteration = 0;
// FIXME this should be changed to tolerate a zero-width time range
for (int departureTime = req.toTime - 60, n = 0; departureTime >= fromTime; departureTime -= 60, n++) {
if (n % 15 == 0) {
LOG.info("minute {}", n);
}
// run the scheduled search
this.runRaptorScheduled(initialStops, departureTime);
this.doPropagation(bestNonTransferTimes, scheduledTimesAtTargets, departureTime);
// pop in the walk only times; we don't want to force people to ride transit instead of
// walking a block
for (int i = 0; i < scheduledTimesAtTargets.length; i++) {
if (nonTransitTimes[i] != UNREACHED && nonTransitTimes[i] + departureTime < scheduledTimesAtTargets[i])
scheduledTimesAtTargets[i] = nonTransitTimes[i] + departureTime;
}
// run the frequency searches
if (data.hasFrequencies) {
for (int i = 0; i < monteCarloDraws + 2; i++) {
// make copies for just this search. We need copies because we can't use dynamic
// programming/range-raptor with randomized schedules
int[] bestTimesCopy = Arrays.copyOf(bestTimes, bestTimes.length);
int[] bestNonTransferTimesCopy = Arrays
.copyOf(bestNonTransferTimes, bestNonTransferTimes.length);
int[] previousPatternsCopy = Arrays
.copyOf(previousPatterns, previousPatterns.length);
// special cases: calculate the best and the worst cases as well
// Note that this (intentionally) does not affect searches where the user has requested
// an assumption other than RANDOM, or stops with transfer rules.
RaptorWorkerTimetable.BoardingAssumption requestedBoardingAssumption = req.boardingAssumption;
if (i == 0 && req.boardingAssumption == RaptorWorkerTimetable.BoardingAssumption.RANDOM) {
req.boardingAssumption = RaptorWorkerTimetable.BoardingAssumption.WORST_CASE;
// don't include extrema in averages
includeIterationInAverages[iteration] = false;
}
else if (i == 1 && req.boardingAssumption == RaptorWorkerTimetable.BoardingAssumption.RANDOM) {
req.boardingAssumption = RaptorWorkerTimetable.BoardingAssumption.BEST_CASE;
// don't include extrema in averages
includeIterationInAverages[iteration] = false;
}
else if (requestedBoardingAssumption == RaptorWorkerTimetable.BoardingAssumption.RANDOM)
// use a new Monte Carlo draw each time
// included in averages by default
offsets.randomize();
this.runRaptorFrequency(departureTime, bestTimesCopy, bestNonTransferTimesCopy,
previousPatternsCopy);
req.boardingAssumption = requestedBoardingAssumption;
// do propagation
int[] frequencyTimesAtTargets = timesAtTargetsEachIteration[iteration++];
System.arraycopy(scheduledTimesAtTargets, 0, frequencyTimesAtTargets, 0,
scheduledTimesAtTargets.length);
// updates timesAtTargetsEachIteration directly because it has a reference into the array.
this.doPropagation(bestNonTransferTimesCopy, frequencyTimesAtTargets,
departureTime);
// convert to elapsed time
for (int t = 0; t < frequencyTimesAtTargets.length; t++) {
if (frequencyTimesAtTargets[t] != UNREACHED)
frequencyTimesAtTargets[t] -= departureTime;
}
}
} else {
final int dt = departureTime;
timesAtTargetsEachIteration[iteration++] = IntStream.of(scheduledTimesAtTargets)
.map(i -> i != UNREACHED ? i - dt : i)
.toArray();
}
}
// make sure we filled the array, otherwise results are garbage.
// This implies a bug in OTP, but it has happened in the past when we did
// not set the number of iterations correctly.
// iteration should be incremented past end of array by ++ in assignment above
if (iteration != iterations)
throw new IllegalStateException("Iterations did not completely fill output array");
long calcTime = System.currentTimeMillis() - beginCalcTime;
LOG.info("calc time {}sec", calcTime / 1000.0);
LOG.info(" propagation {}sec", totalPropagationTime / 1000.0);
LOG.info(" raptor {}sec", (calcTime - totalPropagationTime) / 1000.0);
ts.propagation = (int) totalPropagationTime;
ts.transitSearch = (int) (calcTime - totalPropagationTime);
//dumpVariableByte(timesAtTargetsEachMinute);
// we can use min_max here as we've also run it once with best case and worst case board,
// so the best and worst cases are meaningful.
propagatedTimesStore.setFromArray(timesAtTargetsEachIteration, includeIterationInAverages,
PropagatedTimesStore.ConfidenceCalculationMethod.MIN_MAX);
return propagatedTimesStore;
}
public void dumpVariableByte(int[][] array) {
try {
FileOutputStream fos = new FileOutputStream("/Users/abyrd/results.dat");
CodedOutputStream cos = CodedOutputStream.newInstance(fos);
cos.writeUInt32NoTag(array.length);
for (int[] subArray : array) {
cos.writeUInt32NoTag(subArray.length);
for (int val : subArray) {
cos.writeInt32NoTag(val);
}
}
fos.close();
} catch (FileNotFoundException e) {
LOG.error("File not found for dumping raptor results", e);
} catch (IOException e) {
LOG.error("IOException dumping raptor results", e);
}
}
/** Run a raptor search not using frequencies */
public void runRaptorScheduled (TIntIntMap initialStops, int departureTime) {
// Arrays.fill(bestTimes, UNREACHED); hold on to old state
max_time = departureTime + MAX_DURATION;
round = 0;
advance(); // go to first round
patternsTouched.clear(); // clear patterns left over from previous calls.
allStopsTouched.clear();
stopsTouched.clear();
// Copy initial stops over to the first round
TIntIntIterator iterator = initialStops.iterator();
while (iterator.hasNext()) {
iterator.advance();
int stopIndex = iterator.key();
int time = iterator.value() + departureTime;
// note not setting bestNonTransferTimes here because the initial walk is effectively a "transfer"
bestTimes[stopIndex] = Math.min(time, bestTimes[stopIndex]);
markPatternsForStop(stopIndex);
}
// Anytime a round updates some stops, move on to another round
while (doOneRound(bestTimes, bestNonTransferTimes, previousPatterns, false)) {
advance();
}
}
/** Run a RAPTOR search using frequencies */
public void runRaptorFrequency (int departureTime, int[] bestTimes, int[] bestNonTransferTimes, int[] previousPatterns) {
max_time = departureTime + MAX_DURATION;
round = 0;
advance(); // go to first round
patternsTouched.clear(); // clear patterns left over from previous calls.
allStopsTouched.clear();
stopsTouched.clear();
// we need to mark every reachable stop here, because the network is changing randomly.
// It is entirely possible that the first trip in an itinerary does not change, but trips
// further down do.
IntStream.range(0, bestTimes.length).filter(i -> bestTimes[i] != UNREACHED).forEach(
this::markPatternsForStop);
// Anytime a round updates some stops, move on to another round
while (doOneRound(bestTimes, bestNonTransferTimes, previousPatterns, true)) {
advance();
}
}
public boolean doOneRound (int[] bestTimes, int[] bestNonTransferTimes, int[] previousPatterns, boolean useFrequencies) {
//LOG.info("round {}", round);
stopsTouched.clear(); // clear any stops left over from previous round.
PATTERNS: for (int p = patternsTouched.nextSetBit(0); p >= 0; p = patternsTouched.nextSetBit(p+1)) {
//LOG.info("pattern {} {}", p, data.patternNames.get(p));
int onTrip = -1;
RaptorWorkerTimetable timetable = data.timetablesForPattern.get(p);
int stopPositionInPattern = -1; // first increment will land this at zero
int bestFreqBoardTime = Integer.MAX_VALUE;
int bestFreqBoardStop = -1;
int bestFreq = -1;
// first look for a frequency entry
if (useFrequencies) {
for (int stopIndex : timetable.stopIndices) {
stopPositionInPattern += 1;
// the time at this stop if we remain on board a vehicle we had already boarded
int remainOnBoardTime;
if (bestFreq != -1) {
// we are already aboard a trip, stay on board
remainOnBoardTime = bestFreqBoardTime + timetable
.getFrequencyTravelTime(bestFreq, bestFreqBoardStop,
stopPositionInPattern);
} else {
// we cannot remain on board as we are not yet on board
remainOnBoardTime = Integer.MAX_VALUE;
}
// the time at this stop if we board a new vehicle
if (bestTimes[stopIndex] != UNREACHED) {
for (int trip = 0; trip < timetable.getFrequencyTripCount(); trip++) {
int boardTime = timetable
.getFrequencyDeparture(trip, stopPositionInPattern,
bestTimes[stopIndex], previousPatterns[stopIndex], offsets, req.boardingAssumption);
if (boardTime != -1 && boardTime < remainOnBoardTime) {
// make sure we board the best frequency entry at a stop
if (bestFreqBoardStop == stopPositionInPattern && bestFreqBoardTime < boardTime)
continue;
// board this vehicle
// note: this boards the trip with the lowest headway at the given time.
// if there are overtaking trips all bets are off.
bestFreqBoardTime = boardTime;
bestFreqBoardStop = stopPositionInPattern;
bestFreq = trip;
// note that we do not break the loop in case there's another frequency entry that is better
}
}
}
// save the remain on board time. If we boarded a new trip then we know that the
// remain on board time must be larger than the arrival time at the stop so will
// not be saved; no need for an explicit check.
if (remainOnBoardTime != Integer.MAX_VALUE && remainOnBoardTime < max_time) {
if (bestNonTransferTimes[stopIndex] > remainOnBoardTime) {
bestNonTransferTimes[stopIndex] = remainOnBoardTime;
stopsTouched.set(stopIndex);
allStopsTouched.set(stopIndex);
if (bestTimes[stopIndex] > remainOnBoardTime) {
bestTimes[stopIndex] = remainOnBoardTime;
previousPatterns[stopIndex] = p;
}
}
}
}
// don't mix frequencies and timetables
if (bestFreq != -1)
continue PATTERNS;
}
// perform scheduled search
stopPositionInPattern = -1;
for (int stopIndex : timetable.stopIndices) {
stopPositionInPattern += 1;
if (onTrip == -1) {
// We haven't boarded yet
if (bestTimes[stopIndex] == UNREACHED) {
continue; // we've never reached this stop, we can't board.
}
// Stop has been reached before. Attempt to board here.
onTrip = timetable.findDepartureAfter(stopPositionInPattern, bestTimes[stopIndex]);
continue; // boarded or not, we move on to the next stop in the sequence
} else {
// We're on board a trip.
int arrivalTime = timetable.getArrival(onTrip, stopPositionInPattern);
if (arrivalTime < max_time && arrivalTime < bestNonTransferTimes[stopIndex]) {
bestNonTransferTimes[stopIndex] = arrivalTime;
stopsTouched.set(stopIndex);
allStopsTouched.set(stopIndex);
if (arrivalTime < bestTimes[stopIndex]) {
bestTimes[stopIndex] = arrivalTime;
previousPatterns[stopIndex] = p;
}
}
// Check whether we can back up to an earlier trip. This could be due to an overtaking trip,
// or (more likely) because there was a faster way to get to a stop further down the line.
while (onTrip > 0) {
int departureOnPreviousTrip = timetable.getDeparture(onTrip - 1, stopPositionInPattern);
// use bestTime not bestNonTransferTimes to allow transferring to this trip later on down the route
if (departureOnPreviousTrip > bestTimes[stopIndex]) {
onTrip--;
} else {
break;
}
}
}
}
}
doTransfers(bestTimes, bestNonTransferTimes, previousPatterns);
return !patternsTouched.isEmpty();
}
/**
* Apply transfers.
* Mark all the patterns passing through these stops and any stops transferred to.
*/
private void doTransfers(int[] bestTimes, int[] bestNonTransferTimes, int[] previousPatterns) {
patternsTouched.clear();
for (int stop = stopsTouched.nextSetBit(0); stop >= 0; stop = stopsTouched.nextSetBit(stop + 1)) {
// TODO this is reboarding every trip at every stop.
markPatternsForStop(stop);
int fromTime = bestNonTransferTimes[stop];
int[] transfers = data.transfersForStop.get(stop);
for (int i = 0; i < transfers.length; i++) {
int toStop = transfers[i++]; // increment i
int distance = transfers[i]; // i will be incremented at the end of the loop
int toTime = fromTime + (int) (distance / req.walkSpeed);
if (toTime < max_time && toTime < bestTimes[toStop]) {
bestTimes[toStop] = toTime;
previousPatterns[toStop] = previousPatterns[stop];
markPatternsForStop(toStop);
}
}
}
}
/**
* Propagate from the transit network to the street network.
* Uses allStopsTouched to determine from whence to propagate.
*
* This is valid both for randomized frequencies and for schedules, because the stops that have
* been updated will be in allStopsTouched.
*/
public void doPropagation (int[] timesAtTransitStops, int[] timesAtTargets, int departureTime) {
long beginPropagationTime = System.currentTimeMillis();
// Record distances to each sample or intersection
// We need to propagate all the way to samples (or intersections if there are no samples)
// when doing repeated RAPTOR.
// Consider the situation where there are two parallel transit lines on
// 5th Street and 6th Street, and you live on A Street halfway between 5th and 6th.
// Both lines run at 30 minute headways, but they are exactly out of phase, and for the
// purposes of this conversation both go the same place with the same in-vehicle travel time.
// Thus, even though the lines run every 30 minutes, you never experience a wait of more than
// 15 minutes because you are clever when you choose which line to take. The worst case at each
// transit stop is much worse than the worst case at samples. While unlikely, it is possible that
// a sample would be able to reach these two stops within the walk limit, but that the two
// intersections it is connected to cannot reach both.
// only loop over stops that were touched this minute
for (int s = allStopsTouched.nextSetBit(0); s >= 0; s = allStopsTouched.nextSetBit(s + 1)) {
// it's safe to use the best time at this stop for any number of transfers, even in range-raptor,
// because we allow unlimited transfers. this is slightly different from the original RAPTOR implementation:
// we do not necessarily compute all pareto-optimal paths on (journey time, number of transfers).
int baseTimeSeconds = timesAtTransitStops[s];
if (baseTimeSeconds != UNREACHED) {
int[] targets = data.targetsForStop.get(s);
if (targets == null)
continue;
for (int i = 0; i < targets.length; i++) {
int targetIndex = targets[i++]; // increment i after read
// the cache has time in seconds rather than distance, to avoid costly floating-point divides and integer casts here.
int propagated_time = baseTimeSeconds + targets[i];
if (timesAtTargets[targetIndex] > propagated_time) {
timesAtTargets[targetIndex] = propagated_time;
}
}
}
}
totalPropagationTime += (System.currentTimeMillis() - beginPropagationTime);
}
/** Mark all the patterns passing through the given stop. */
private void markPatternsForStop(int stop) {
int[] patterns = data.patternsForStop.get(stop);
for (int pattern : patterns) {
patternsTouched.set(pattern);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy