Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
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);
}
}
}