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

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

package org.opentripplanner.profile;

import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.opentripplanner.analyst.cluster.TaskStatistics;
import org.opentripplanner.analyst.scenario.AddTripPattern;
import org.opentripplanner.analyst.scenario.Scenario;
import org.opentripplanner.analyst.scenario.TransferRule;
import org.opentripplanner.analyst.scenario.TripFilter;
import org.opentripplanner.routing.edgetype.TripPattern;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.trippattern.FrequencyEntry;
import org.opentripplanner.routing.trippattern.TripTimes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * A RaptorWorkerTimetable is used by a RaptorWorker to perform large numbers of RAPTOR searches very quickly
 * within a specific time window. It is used heavily in one-to-many profile routing, where we're interested in seeing
 * how access to opportunities varies over time.
 *
 * This is an alternative representation of all the TripTimes in a single TripPattern that are running during a
 * particular time range on a particular day. It packs the times a bit closer together and pre-filters the TripTimes based on
 * whether they are running at all during the time window eliminates run-time checks that need to be performed when we
 * search for the soonest departure. Note: we're not sure this actually provides a major speedup compared to operating
 * directly on TripPatterns and TripTimes.
 *
 * Unlike in "normal" OTP searches, we assume non-overtaking (FIFO) vehicle behavior within a single TripPattern,
 * which is generally the case in clean input data. One key difference here is that profile routing and spatial analysis
 * do not need to take real-time (GTFS-RT) updates into account since they are intended to be generic results
 * describing a scenario in the future.
 */
public class RaptorWorkerTimetable implements Serializable {

    private static final Logger LOG = LoggerFactory.getLogger(RaptorWorkerTimetable.class);

    /* Times for schedule-based trips/patterns are stored in a 2D array. */

    int nTrips, nStops;

    /* For each trip on this pattern, an packed array of (arrival, departure) time pairs. */
    public int[][] timesPerTrip;

    /* Times for frequency-based trips are stored in parallel arrays (a column store). */

    /** Times (0-based) for frequency trips */
    int[][] frequencyTrips;

    /** Headways (seconds) for frequency trips, parallel to above. Note that frequency trips are unsorted. */
    int[] headwaySecs;

    /** Start times (seconds since noon - 12h) for frequency trips */
    int[] startTimes;

    /** End times for frequency trips */
    int[] endTimes;

    /** Indices of stops in parent data */
    public int[] stopIndices;

    /** parent raptorworkerdata of this timetable */
    public RaptorWorkerData raptorData;

    /** Mode of this pattern, see constants in com.conveyal.gtfs.model.Route */
    public int mode;

    /** Index of this pattern in RaptorData */
    public int dataIndex;

    /** for debugging, the ID of the route this represents */
    public transient String routeId;

    /** slack required when boarding a transit vehicle */
    public static final int MIN_BOARD_TIME_SECONDS = 60;

    public RaptorWorkerTimetable(int nTrips, int nStops) {
        this.nTrips = nTrips;
        this.nStops = nStops;
        timesPerTrip = new int[nTrips][];
    }

    /**
     * Return the trip index within the pattern of the soonest departure at the given stop number, requiring at least
     * MIN_BOARD_TIME_SECONDS seconds of slack. 
     */
    public int findDepartureAfter(int stop, int time) {
        for (int trip = 0; trip < timesPerTrip.length; trip++) {
            if (getDeparture(trip, stop) > time + MIN_BOARD_TIME_SECONDS) {
                return trip;
            }
        }
        return -1;
    }

    public int getArrival (int trip, int stop) {
        return timesPerTrip[trip][stop * 2];
    }

    public int getDeparture (int trip, int stop) {
        return timesPerTrip[trip][stop * 2 + 1];
    }

    public int getFrequencyDeparture (int trip, int stop, int time, int previousPattern, FrequencyRandomOffsets offsets) {
        return getFrequencyDeparture(trip, stop, time, previousPattern, offsets, null);
    }

    /**
     * Get the departure on frequency trip trip at stop stop after time time,
     * with the given boarding assumption. (Note that the boarding assumption specified may be overridden
     * by transfer rules).
     */
    public int getFrequencyDeparture (int trip, int stop, int time, int previousPattern, FrequencyRandomOffsets offsets, BoardingAssumption assumption) {
        int timeToReachStop = frequencyTrips[trip][stop * 2 + 1];

        // figure out if there is an applicable transfer rule
        TransferRule transferRule = null;

        if (previousPattern != -1) {
            // this is a transfer

            // stop index in Raptor data
            int stopIndex = stopIndices[stop];

            // first check for specific rules
            // calling containsKey can be expensive so first check if list is empty
            if (!raptorData.transferRules.isEmpty() && raptorData.transferRules.containsKey(stopIndex)) {
                for (TransferRule tr : raptorData.transferRules.get(stopIndex)) {
                    if (tr.matches(raptorData.timetablesForPattern.get(previousPattern), this)) {
                        transferRule = tr;
                        break;
                    }
                }
            }

            if (transferRule == null && !raptorData.baseTransferRules.isEmpty()) {
                // look for global rules
                // using declarative for loop because constructing a stream and doing a filter is
                // slow.
                for (TransferRule tr : raptorData.baseTransferRules) {
                    if (tr.matches(raptorData.timetablesForPattern.get(previousPattern), this)) {
                        transferRule = tr;
                        break;
                    }
                }
            }
        }

        if (assumption == null)
            assumption = raptorData.boardingAssumption;

        // if there is an applicable transfer rule, override anything that was specified elsewhere
        if (transferRule != null)
            assumption = transferRule.assumption;

        if (assumption == BoardingAssumption.RANDOM) {
            // We treat every frequency-based trip as a scheduled trip on each iteration of the Monte Carlo
            // algorithm. The reason for this is thus: consider something like the Portland Transit Mall.
            // There are many opportunities to transfer between vehicles. The transfer times between
            // Line A and Line B are not independently random at each stop but rather are correlated
            // along each line. Thus we randomize each pattern independently, not each boarding.

            // Keep in mind that there could also be correlation between patterns, especially for rail
            // vehicles that share trackage. Consider, for example, the Fredericksburg and Manassus
            // lines on the Virginia Railway Express, at Alexandria. These are two diesel heavy-rail
            // lines that share trackage. Thus the minimum transfer time between them is always at least
            // 5 minutes or so as that's as close as you can run diesel trains to each other.
            int minTime = startTimes[trip] + timeToReachStop + offsets.offsets.get(this.dataIndex)[trip];

            // move time forward to an integer multiple of headway after the minTime
            if (time < minTime)
                time = minTime;
            else
                // add enough to time to make time - minTime an integer multiple of headway
                // if the bus comes every 30 minutes and you arrive at the stop 35 minutes
                // after it first came, you have to wait 25 minutes, or headway - time already elapsed
                // time already elapsed is 35 % 30 = 5 minutes in this case.
                time += headwaySecs[trip] - (time - minTime) % headwaySecs[trip];
        }

        else {
            // move time forward if the frequency has not yet started.
            // we do this before we add the wait time, because this is the earliest possible
            // time a vehicle could reach the stop. The assumption is that the vehicle leaves
            // the terminal between zero and headway_seconds seconds after the start_time of the
            // frequency
            if (timeToReachStop + startTimes[trip] > time)
                time = timeToReachStop + startTimes[trip];

            switch (assumption) {
            case BEST_CASE:
                // do nothing
                break;
            case WORST_CASE:
                time += headwaySecs[trip];
                break;
            case HALF_HEADWAY:
                time += headwaySecs[trip] / 2;
                break;
            case FIXED:
                if (transferRule == null) throw new IllegalArgumentException("Cannot use boarding assumption FIXED without a transfer rule");
                time += transferRule.transferTimeSeconds;
                break;
            case PROPORTION:
                if (transferRule == null) throw new IllegalArgumentException("Cannot use boarding assumption PROPORTION without a transfer rule");
                time += (int) (transferRule.waitProportion * headwaySecs[trip]);
                break;
            }
        }

        if (time > timeToReachStop + endTimes[trip])
            return -1;
        else
            return time;
    }

    /**
     * Get the travel time (departure to arrival) on frequency trip trip, from stop from to stop to.  
     */
    public int getFrequencyTravelTime (int trip, int from, int to) {
        return frequencyTrips[trip][to * 2] - frequencyTrips[trip][from * 2 + 1];
    }

    /**
     * Get the number of frequency trips on this pattern (i.e. the number of trips in trips.txt, not the number of trips by vehicles)
     */
    public int getFrequencyTripCount () {
        return headwaySecs.length;
    }

    /** Does this timetable have any frequency trips? */
    public boolean hasFrequencyTrips () {
        return this.headwaySecs != null && this.headwaySecs.length > 0;
    }

    /** does this timetable have any scheduled trips? */
    public boolean hasScheduledTrips () {
        return this.timesPerTrip != null && this.timesPerTrip.length > 0;
    }

    /**
     * This is a factory function rather than a constructor to avoid calling the super constructor for rejected patterns.
     * BannedRoutes is formatted as agencyid_routeid.
     */
    public static RaptorWorkerTimetable forPattern (Graph graph, TripPattern pattern, TimeWindow window, Scenario scenario, TaskStatistics ts) {

        // Filter down the trips to only those running during the window
        // This filtering can reduce number of trips and run time by 80 percent
        BitSet servicesRunning = window.servicesRunning;
        List tripTimes = Lists.newArrayList();
        TT: for (TripTimes tt : pattern.scheduledTimetable.tripTimes) {
            if (servicesRunning.get(tt.serviceCode) &&
                    tt.getArrivalTime(0) < window.to &&
                    tt.getDepartureTime(tt.getNumStops() - 1) >= window.from) {

                // apply scenario
                // TODO: need to do this before filtering based on window!
                if (scenario != null && scenario.modifications != null) {
                    for (TripFilter filter : Iterables.filter(scenario.modifications, TripFilter.class)) {
                        tt = filter.apply(tt.trip, pattern, tt);

                        if (tt == null)
                            continue TT;
                    }
                }

                tripTimes.add(tt);
            }
        }

        // find frequency trips
        List freqs = Lists.newArrayList();
        FREQUENCIES: for (FrequencyEntry fe : pattern.scheduledTimetable.frequencyEntries) {
            if (servicesRunning.get(fe.tripTimes.serviceCode) &&
                    fe.getMinDeparture() < window.to &&
                    fe.getMaxArrival() > window.from
                    ) {
                // this frequency entry has the potential to be used

                if (fe.exactTimes) {
                    LOG.warn("Exact-times frequency trips not yet supported");
                    continue;
                }

                if (scenario != null && scenario.modifications != null) {
                    for (TripFilter filter : Iterables.filter(scenario.modifications, TripFilter.class)) {
                        fe = filter.apply(fe.tripTimes.trip, pattern, fe);

                        if (fe == null)
                            continue FREQUENCIES;
                    }
                }

                freqs.add(fe);
            }
        }

        if (tripTimes.isEmpty() && freqs.isEmpty()) {
            return null; // no trips active, don't bother storing a timetable
        }


        // Sort the trip times by their first arrival time
        Collections.sort(tripTimes, new Comparator() {
            @Override
            public int compare(TripTimes tt1, TripTimes tt2) {
                return (tt1.getArrivalTime(0) - tt2.getArrivalTime(0));
            }
        });

        // Copy the times into the compacted table
        RaptorWorkerTimetable rwtt = new RaptorWorkerTimetable(tripTimes.size(), pattern.getStops().size());
        int t = 0;
        for (TripTimes tt : tripTimes) {
            int[] times = new int[rwtt.nStops * 2];
            for (int s = 0; s < pattern.getStops().size(); s++) {
                int arrival = tt.getArrivalTime(s);
                int departure = tt.getDepartureTime(s);
                times[s * 2] = arrival;
                times[s * 2 + 1] = departure;
            }
            rwtt.timesPerTrip[t++] = times;
        }

        ts.scheduledTripCount += rwtt.timesPerTrip.length;

        // save frequency times
        rwtt.frequencyTrips = new int[freqs.size()][pattern.getStops().size() * 2];
        rwtt.endTimes = new int[freqs.size()];
        rwtt.startTimes = new int[freqs.size()];
        rwtt.headwaySecs = new int[freqs.size()];

        {
            int i = 0;
            for (FrequencyEntry fe : freqs) {
                rwtt.headwaySecs[i] = fe.headway;
                rwtt.startTimes[i] = fe.startTime;
                rwtt.endTimes[i] = fe.endTime;

                ts.frequencyTripCount += fe.numTrips();

                int[] times = rwtt.frequencyTrips[i];

                // It's generally considered good practice to have frequency trips start at midnight, however that is
                // not always the case, and we need to preserve the original times so that we can update them in
                // real time.
                int startTime = fe.tripTimes.getArrivalTime(0);

                for (int s = 0; s < fe.tripTimes.getNumStops(); s++) {
                    times[s * 2] = fe.tripTimes.getArrivalTime(s) - startTime;
                    times[s * 2 + 1] = fe.tripTimes.getDepartureTime(s) - startTime;
                }

                i++;
            }
        }

        ts.frequencyEntryCount += rwtt.getFrequencyTripCount();

        rwtt.mode = pattern.route.getType();

        return rwtt;
    }

    /** Create a raptor worker timetable for an added pattern */
    public static RaptorWorkerTimetable forAddedPattern(AddTripPattern atp, TimeWindow window, TaskStatistics ts) {
        if (atp.temporaryStops.length < 2 || atp.timetables.isEmpty())
            return null;

        // filter down the timetable to only those running
        Collection running = atp.timetables.stream()
                // getValue yields one-based ISO days of week (monday = 1); convert to 0-based format
                .filter(t -> t.days.get(window.dayOfWeek.getValue() - 1))
                .collect(Collectors.toList());

        if (running.isEmpty())
            return null;

        // TODO: filter with timewindow
        Collection frequencies = running.stream()
                .filter(t -> t.frequency)
                .collect(Collectors.toList());

        Collection timetables = running.stream()
                .filter(t -> !t.frequency)
                .sorted((t1, t2) -> t1.startTime - t2.startTime)
                .collect(Collectors.toList());

        RaptorWorkerTimetable rwtt = new RaptorWorkerTimetable(timetables.size(), atp.temporaryStops.length);

        // create timetabled trips
        int t = 0;
        for (AddTripPattern.PatternTimetable pt : timetables) {
            rwtt.timesPerTrip[t++] = timesForPatternTimetable(atp, pt);
        }

        ts.scheduledTripCount += rwtt.timesPerTrip.length;

        // create frequency trips
        rwtt.frequencyTrips = new int[frequencies.size()][atp.temporaryStops.length * 2];
        rwtt.endTimes = new int[frequencies.size()];
        rwtt.startTimes = new int[frequencies.size()];
        rwtt.headwaySecs = new int[frequencies.size()];

        t = 0;
        for (AddTripPattern.PatternTimetable pt : frequencies) {
            rwtt.frequencyTrips[t] = timesForPatternTimetable(atp, pt);
            rwtt.startTimes[t] = pt.startTime;
            rwtt.endTimes[t] = pt.endTime;
            rwtt.headwaySecs[t++] = pt.headwaySecs;

            ts.frequencyTripCount += (pt.endTime - pt.startTime) / pt.headwaySecs;
        }

        ts.frequencyEntryCount += frequencies.size();

        rwtt.mode = atp.mode;
        rwtt.routeId = atp.name;

        return rwtt;
    }

    private static int[] timesForPatternTimetable (AddTripPattern atp, AddTripPattern.PatternTimetable pt) {
        int[] times = new int[atp.temporaryStops.length * 2];
        for (int s = 0; s < atp.temporaryStops.length; s++) {
            // for a timetable route,
            // arrival time is start time if it's the first stop, or the previous departure time plus the hop time
            // For a frequency route the first departure is always 0
            if (s == 0)
                times[s * 2] = pt.frequency ? 0 : pt.startTime;
            else
                times[s * 2] = times[s * 2 - 1] + pt.hopTimes[s - 1];

            times[s * 2 + 1] = times[s * 2] + pt.dwellTimes[s];
        }
        return times;
    }

    /** The assumptions made when boarding a frequency vehicle: best case (no wait), worst case (full headway) and half headway (in some sense the average). */
    public static enum BoardingAssumption {
        BEST_CASE, WORST_CASE, HALF_HEADWAY, FIXED, PROPORTION, RANDOM;

        public static final long serialVersionUID = 1;
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy