All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentripplanner.profile.RaptorWorker Maven / Gradle / Ivy

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 - 2024 Weber Informatics LLC | Privacy Policy