diff --git a/.github/workflows/cypress-integration.yml b/.github/workflows/cypress-integration.yml index 81ae3dd62..6258d546f 100644 --- a/.github/workflows/cypress-integration.yml +++ b/.github/workflows/cypress-integration.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v2 with: repository: conveyal/analysis-ui - ref: 62dfe5b8d8e76a095b8f923b6458b75d3e10c2c8 + ref: dee1f13ba891e35b859438c97e5f66a0a7347d38 path: ui - uses: actions/checkout@v2 with: diff --git a/src/main/java/com/conveyal/r5/analyst/cluster/PathResult.java b/src/main/java/com/conveyal/r5/analyst/cluster/PathResult.java index 783eb5c21..edf8ad057 100644 --- a/src/main/java/com/conveyal/r5/analyst/cluster/PathResult.java +++ b/src/main/java/com/conveyal/r5/analyst/cluster/PathResult.java @@ -155,13 +155,18 @@ public static class PathIterations { this.egress = pathTemplate.stopSequence.egress == null ? null : pathTemplate.stopSequence.egress.toString(); this.transitLegs = pathTemplate.transitLegs(transitLayer); this.iterations = iterations.stream().map(HumanReadableIteration::new).collect(Collectors.toList()); + iterations.forEach(pathTemplate.stopSequence::transferTime); // The transferTime method includes an + // assertion that the transfer time is non-negative, i.e. that the access + egress + wait + ride times of + // a specific iteration do not exceed the total travel time. Perform that sense check here, even though + // the transfer time is not reported to the front-end for the human-readable single-point responses. + // TODO add transferTime to HumanReadableIteration? } } /** * Returns human-readable details of path iterations, for JSON representation (e.g. in the UI console). */ - List getPathIterationsForDestination() { + public List getPathIterationsForDestination() { checkState(iterationsForPathTemplates.length == 1, "Paths were stored for multiple " + "destinations, but only one is being requested"); List detailsForDestination = new ArrayList<>(); diff --git a/src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java b/src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java index eeee044bc..833fbfcd1 100644 --- a/src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java +++ b/src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java @@ -1,9 +1,9 @@ package com.conveyal.r5.profile; import com.conveyal.r5.analyst.cluster.AnalysisWorkerTask; -import com.conveyal.r5.api.util.TransitModes; +import com.conveyal.r5.transit.FilteredPattern; +import com.conveyal.r5.transit.FilteredPatterns; import com.conveyal.r5.transit.PickDropType; -import com.conveyal.r5.transit.RouteInfo; import com.conveyal.r5.transit.TransitLayer; import com.conveyal.r5.transit.TripPattern; import com.conveyal.r5.transit.TripSchedule; @@ -24,14 +24,15 @@ import static com.conveyal.r5.profile.FastRaptorWorker.FrequencyBoardingMode.HALF_HEADWAY; import static com.conveyal.r5.profile.FastRaptorWorker.FrequencyBoardingMode.MONTE_CARLO; import static com.conveyal.r5.profile.FastRaptorWorker.FrequencyBoardingMode.UPPER_BOUND; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; /** - * FastRaptorWorker is faster than the old RaptorWorker and made to be more maintainable. - * It is simpler, as it only focuses on the transit network; see the Propagater class for the methods that extend - * the travel times from the final transit stop of a trip out to the individual targets. - * - * The algorithm used herein is described in + * FastRaptorWorker finds stop-to-stop paths through the transit network. + * The PerTargetPropagater extends travel times from the final transit stop of trips out to the individual targets. + * This system also accounts for pure-frequency routes by using Monte Carlo methods (generating randomized schedules). + * There is support for saving paths, so we can report how to reach a destination rather than just how long it takes. + * The algorithm used herein is described in: * * Conway, Matthew Wigginton, Andrew Byrd, and Marco van der Linden. “Evidence-Based Transit and Land Use Sketch Planning * Using Interactive Accessibility Methods on Combined Schedule and Headway-Based Networks.” Transportation Research @@ -40,22 +41,15 @@ * Delling, Daniel, Thomas Pajor, and Renato Werneck. “Round-Based Public Transit Routing,” January 1, 2012. * http://research.microsoft.com/pubs/156567/raptor_alenex.pdf. * - * There is basic support for saving paths, so we can report how to reach a destination rather than just how long it takes. - * - * This class 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 this is now - * the way we do all analysis work. This system also accounts for pure-frequency routes by using Monte Carlo methods - * (generating randomized schedules). * - * TODO rename to remove "fast" and revise above comments, there is only one worker now. - * Maybe just call it TransitRouter. But then there's also McRaptor. + * TODO rename to remove "fast". Maybe just call it TransitRouter, but then there's also McRaptor. */ public class FastRaptorWorker { private static final Logger LOG = LoggerFactory.getLogger(FastRaptorWorker.class); /** - * This value essentially serves as Infinity for ints - it's bigger than every other number. + * This value is used as positive infinity for ints - it's bigger than every other number. * It is the travel time to a transit stop or a target before that stop or target is ever reached. * Be careful when propagating travel times from stops to targets, adding anything to UNREACHED will cause overflow. */ @@ -70,7 +64,7 @@ public class FastRaptorWorker { public static final int SECONDS_PER_MINUTE = 60; /** - * Step for departure times. Use caution when changing this as the functions request.getTimeWindowLengthMinutes + * Step for departure times. Use caution when changing this, as the functions request.getTimeWindowLengthMinutes * and request.getMonteCarloDrawsPerMinute below which assume this value is 1 minute. */ private static final int DEPARTURE_STEP_SEC = 60; @@ -82,6 +76,8 @@ public class FastRaptorWorker { private static final int MINIMUM_BOARD_WAIT_SEC = 60; // ENABLE_OPTIMIZATION_X flags enable code paths that should affect efficiency but have no effect on output. + // They may change results where our algorithm is not perfectly optimal, for example with respect to overtaking + // (see discussion at #708). public static final boolean ENABLE_OPTIMIZATION_RANGE_RAPTOR = true; public static final boolean ENABLE_OPTIMIZATION_FREQ_UPPER_BOUND = true; @@ -111,18 +107,15 @@ public class FastRaptorWorker { /** The routing parameters. */ private final AnalysisWorkerTask request; - /** The indexes of the trip patterns running on a given day with frequency-based trips of selected modes. */ - private final BitSet runningFrequencyPatterns = new BitSet(); - - /** The indexes of the trip patterns running on a given day with scheduled trips of selected modes. */ - private final BitSet runningScheduledPatterns = new BitSet(); - /** Generates and stores departure time offsets for every frequency-based set of trips. */ private final FrequencyRandomOffsets offsets; - /** Services active on the date of the search */ + /** Services active on the date of the search. */ private final BitSet servicesActive; + /** TripPatterns that have been prefiltered for the specific search date and modes. */ + private FilteredPatterns filteredPatterns; + /** * The state resulting from the scheduled search at a particular departure minute. * This state is reused at each departure minute without re-initializing it (this is the range-raptor optimization). @@ -142,6 +135,10 @@ public class FastRaptorWorker { /** If we're going to store paths to every destination (e.g. for static sites) then they'll be retained here. */ public List pathsPerIteration; + /** + * Only fast initialization steps are performed in the constructor. + * All slower work is done in route() so timing information can be collected. + */ public FastRaptorWorker (TransitLayer transitLayer, AnalysisWorkerTask request, TIntIntMap accessStops) { this.transit = transitLayer; this.request = request; @@ -171,7 +168,9 @@ public FastRaptorWorker (TransitLayer transitLayer, AnalysisWorkerTask request, */ public int[][] route () { raptorTimer.fullSearch.start(); - prefilterPatterns(); + raptorTimer.patternFiltering.start(); + filteredPatterns = transit.filteredPatternCache.get(request.transitModes, servicesActive); + raptorTimer.patternFiltering.stop(); // Initialize result storage. Results are one arrival time at each stop, for every raptor iteration. final int nStops = transit.getStopCount(); final int nIterations = iterationsPerMinute * nMinutes; @@ -238,29 +237,6 @@ private void dumpAllTimesToFile(int[][] arrivalTimesAtStopsPerIteration, int max } } - /** - * Before routing, filter the set of patterns down to only the ones that are actually running on the search date. - * We can also filter down to only those modes enabled in the search request, because all trips in a pattern are - * defined to be on same route, and GTFS allows only one mode per route. - */ - private void prefilterPatterns () { - for (int patternIndex = 0; patternIndex < transit.tripPatterns.size(); patternIndex++) { - TripPattern pattern = transit.tripPatterns.get(patternIndex); - RouteInfo routeInfo = transit.routes.get(pattern.routeIndex); - TransitModes mode = TransitLayer.getTransitModes(routeInfo.route_type); - if (pattern.servicesActive.intersects(servicesActive) && request.transitModes.contains(mode)) { - // At least one trip on this pattern is relevant, based on the profile request's date and modes. - if (pattern.hasFrequencies) { - runningFrequencyPatterns.set(patternIndex); - } - // Schedule case is not an "else" clause because we support patterns with both frequency and schedule. - if (pattern.hasSchedules) { - runningScheduledPatterns.set(patternIndex); - } - } - } - } - /** * Create the initial array of states for the latest departure minute in the window, one state for each round. * One state for each allowed transit ride, plus a zeroth round containing the results of the access street search. @@ -289,7 +265,10 @@ private void initializeScheduleState (int departureTime) { /** * Set the departure time in the scheduled search to the given departure time, and prepare for the scheduled * search at the next-earlier minute. This is reusing results from one departure time as an upper bound on - * arrival times for an earlier departure time (i.e. range raptor). + * arrival times for an earlier departure time (i.e. range raptor). Note that this reuse can give riders + * "look-ahead" abilities about trips that will be overtaken, depending on the departure time window; they will + * not board the first feasible departure from a stop if a later one (that has been ridden in a later departure + * minute) will arrive at their destination stop earlier. */ private void advanceScheduledSearchToPreviousMinute (int nextMinuteDepartureTime) { for (RaptorState state : this.scheduleState) { @@ -470,6 +449,62 @@ private static Path[] pathToEachStop(RaptorState state) { return paths; } + + /** + * Starting from a trip we're already riding, step backward through the trips in the supplied filteredPattern to see + * if there is a usable one that departs earlier from the current stop position in the pattern, and if so return its + * index within the filtered pattern. This method assumes there is no overtaking in the FilteredPattern's schedules. + */ + private int checkEarlierScheduledDeparture ( + int departAfter, FilteredPattern filteredPattern, int stopInPattern, int currentTrip + ) { + checkArgument(filteredPattern.noScheduledOvertaking); + int bestTrip = currentTrip; + int candidateTrip = currentTrip; + while (--candidateTrip >= 0) { + // The tripSchedules in the supplied pattern are known to be sorted by departure time at all stops. + TripSchedule candidateSchedule = filteredPattern.runningScheduledTrips.get(candidateTrip); + final int candidateDeparture = candidateSchedule.departures[stopInPattern]; + if (candidateDeparture > departAfter) { + bestTrip = candidateTrip; + } else { + // We are confident of being on the earliest feasible departure. + break; + } + } + return bestTrip; + } + + + /** + * Perform a linear search through the trips in the supplied filteredPattern, finding the one that departs + * earliest from the given stop position in the pattern, and returning its index within the filtered pattern. + */ + private int findEarliestScheduledDeparture ( + int departAfter, FilteredPattern filteredPattern, int stopInPattern + ) { + // Trips are sorted in ascending order by time of departure from first stop + List trips = filteredPattern.runningScheduledTrips; + boolean noOvertaking = filteredPattern.noScheduledOvertaking; + int bestTrip = -1; + int bestDeparture = Integer.MAX_VALUE; + for (int t = 0; t < trips.size(); t++) { + TripSchedule ts = trips.get(t); + final int departure = ts.departures[stopInPattern]; + if (departure > departAfter && departure < bestDeparture) { + bestTrip = t; + bestDeparture = departure; + // No overtaking plus sorting by time of departure from first stop guarantees sorting by time of + // departure at this stop; so we know this is the earliest departure and can break early. + if (noOvertaking) break; + } + } + return bestTrip; + } + + // Chosen to be completely invalid as an array index or time in order to fail fast. + private static final int NONE = -1; + /** * A sub-step in the process of performing a RAPTOR search at one specific departure time (at one specific minute). * This method handles only the routes that have exact schedules. There is another method that handles only the @@ -477,105 +512,68 @@ private static Path[] pathToEachStop(RaptorState state) { */ private void doScheduledSearchForRound (RaptorState outputState) { final RaptorState inputState = outputState.previous; - BitSet patternsToExplore = patternsToExploreInNextRound(inputState, runningScheduledPatterns, true); + BitSet patternsToExplore = patternsToExploreInNextRound( + inputState, filteredPatterns.runningScheduledPatterns, true + ); for (int patternIndex = patternsToExplore.nextSetBit(0); patternIndex >= 0; patternIndex = patternsToExplore.nextSetBit(patternIndex + 1) ) { + FilteredPattern filteredPattern = filteredPatterns.patterns.get(patternIndex); TripPattern pattern = transit.tripPatterns.get(patternIndex); - int onTrip = -1; + // As we scan down the stops of the pattern, we may board a trip, and possibly re-board a different trip. + // Keep track of the index of the currently boarded trip within the list of filtered TripSchedules. + int onTrip = NONE; int waitTime = 0; - int boardTime = 0; - int boardStop = -1; + int boardTime = NONE; + int boardStop = NONE; TripSchedule schedule = null; - - for (int stopPositionInPattern = 0; stopPositionInPattern < pattern.stops.length; stopPositionInPattern++) { - int stop = pattern.stops[stopPositionInPattern]; - - // attempt to alight if we're on board and if drop off is allowed, done above the board search so - // that we don't check for alighting when boarding - if (onTrip > -1 && pattern.dropoffs[stopPositionInPattern] != PickDropType.NONE) { - int alightTime = schedule.arrivals[stopPositionInPattern]; + // Iterate over all stops in the current TripPattern ("scan" down the pattern) + for (int stopInPattern = 0; stopInPattern < pattern.stops.length; stopInPattern++) { + int stop = pattern.stops[stopInPattern]; + // Alight at the current stop in the pattern if drop-off is allowed and we're already on a trip. + // This block is above the boarding search so that we don't alight from the same stop where we boarded. + if (onTrip != NONE && pattern.dropoffs[stopInPattern] != PickDropType.NONE) { + int alightTime = schedule.arrivals[stopInPattern]; int inVehicleTime = alightTime - boardTime; - - // Use checkState instead? - if (waitTime + inVehicleTime + inputState.bestTimes[boardStop] > alightTime) { - LOG.error("Components of travel time are larger than travel time!"); - } - + checkState (alightTime == inputState.bestTimes[boardStop] + waitTime + inVehicleTime, + "Components of travel time are larger than travel time!"); outputState.setTimeAtStop(stop, alightTime, patternIndex, boardStop, waitTime, inVehicleTime, false); } - - // Don't attempt to board if this stop was not reached in the last round or if pick up is not allowed. - // Scheduled searches only care about updates within this departure minute, enabling range-raptor. + // If the current stop was reached in the previous round and allows pick-up, board or re-board a trip. + // Second parameter is true to only look at changes within this departure minute, enabling range-raptor. if (inputState.stopWasUpdated(stop, true) && - pattern.pickups[stopPositionInPattern] != PickDropType.NONE + pattern.pickups[stopInPattern] != PickDropType.NONE ) { int earliestBoardTime = inputState.bestTimes[stop] + MINIMUM_BOARD_WAIT_SEC; - if (onTrip == -1) { - int candidateTripIndex = -1; - for (TripSchedule candidateSchedule : pattern.tripSchedules) { - candidateTripIndex++; - if (!servicesActive.get(candidateSchedule.serviceCode) || candidateSchedule.headwaySeconds != null) { - // frequency trip or not running - continue; - } - if (earliestBoardTime < candidateSchedule.departures[stopPositionInPattern]) { - // board this trip (the earliest trip that can be boarded on this pattern at this stop) - onTrip = candidateTripIndex; - schedule = candidateSchedule; - boardTime = candidateSchedule.departures[stopPositionInPattern]; - waitTime = boardTime - inputState.bestTimes[stop]; - boardStop = stop; - break; - } - } + // Boarding/reboarding search is conditional on previous-round arrival at this stop earlier than the + // current trip in the current round. Otherwise the search is unnecessary and yields later trips. + if (schedule != null && (earliestBoardTime >= schedule.departures[stopInPattern])) { + continue; + } + int newTrip; + if (onTrip != NONE && filteredPattern.noScheduledOvertaking) { + // Optimized reboarding search: Already on a trip, trips known to be sorted by departure time. + newTrip = checkEarlierScheduledDeparture(earliestBoardTime, filteredPattern, stopInPattern, onTrip); } else { - // A specific trip on this pattern could be boarded at an upstream stop. If we are ready to - // depart from this stop before this trip does, it might be preferable to board at this stop - // instead. - if (earliestBoardTime < schedule.departures[stopPositionInPattern]) { - // First, it might be possible to board an earlier trip at this stop. - int earlierTripIdx = onTrip; - while (--earlierTripIdx >= 0) { - // The tripSchedules in a given pattern are sorted by time of departure from the first - // stop. So they are sorted by time of departure at this stop, if the possibility - // of overtaking is ignored. - TripSchedule earlierTripSchedule = pattern.tripSchedules.get(earlierTripIdx); - - if (earlierTripSchedule.headwaySeconds != null || !servicesActive.get(earlierTripSchedule.serviceCode)) { - // This is a frequency trip or it is not running on the day of the search. - continue; - } - // The assertion below is a sanity check, but not a complete check that all the - // tripSchedules are sorted, because later tripSchedules are not considered. - checkState(earlierTripSchedule.departures[0] <= schedule.departures[0], - "Trip schedules not sorted by departure time at first stop of pattern"); - - if (earliestBoardTime < earlierTripSchedule.departures[stopPositionInPattern]) { - // The trip under consideration can be boarded at this stop - onTrip = earlierTripIdx; - schedule = earlierTripSchedule; - boardTime = earlierTripSchedule.departures[stopPositionInPattern]; - waitTime = boardTime - inputState.bestTimes[stop]; - boardStop = stop; - } else { - // The trip under consideration arrives at this stop earlier than one could feasibly - // board. Stop searching, because trips are sorted by departure time within a pattern. - break; - } - } - // Second, if we care about paths or travel time components, check whether boarding at - // this stop instead of the upstream one would allow a shorter access/transfer leg. - // Doing so will not affect total travel time (as long as this is in a conditional - // ensuring we won't miss the trip we're on), but it will affect the breakdown of walk vs. - // wait time. - if (retainPaths && inputState.shorterAccessOrTransferLeg(stop, boardStop)) { - boardTime = schedule.departures[stopPositionInPattern]; - waitTime = boardTime - inputState.bestTimes[stop]; - boardStop = stop; - } - } + // General purpose departure search: not already on a trip or trips are not known to be sorted. + newTrip = findEarliestScheduledDeparture(earliestBoardTime, filteredPattern, stopInPattern); + } + // If we care about paths or travel time components, check whether boarding at this stop instead of + // the upstream one would allow a shorter access/transfer leg. Doing so will not affect total travel + // time (as long as this is in a conditional ensuring we won't miss the trip we're on), but it will + // affect the breakdown of walk vs. wait time. + final boolean reboardForPaths = retainPaths + && (onTrip != NONE) + && inputState.shorterAccessOrTransferLeg(stop, boardStop); + + if ((newTrip != onTrip) || reboardForPaths) { + checkState(newTrip != NONE); // Should never change from being on a trip to on no trip. + onTrip = newTrip; + schedule = filteredPattern.runningScheduledTrips.get(newTrip); + boardTime = schedule.departures[stopInPattern]; + waitTime = boardTime - inputState.bestTimes[stop]; + boardStop = stop; } } } @@ -616,21 +614,18 @@ private void doFrequencySearchForRound (RaptorState outputState, FrequencyBoardi // are applying randomized schedules that are not present in the accumulated range-raptor upper bound state. // Those randomized frequency routes may cascade improvements from updates made at previous departure minutes. final boolean withinMinute = (frequencyBoardingMode == UPPER_BOUND); - BitSet patternsToExplore = patternsToExploreInNextRound(inputState, runningFrequencyPatterns, withinMinute); + BitSet patternsToExplore = patternsToExploreInNextRound( + inputState, filteredPatterns.runningFrequencyPatterns, withinMinute + ); for (int patternIndex = patternsToExplore.nextSetBit(0); patternIndex >= 0; patternIndex = patternsToExplore.nextSetBit(patternIndex + 1) ) { + FilteredPattern filteredPattern = filteredPatterns.patterns.get(patternIndex); TripPattern pattern = transit.tripPatterns.get(patternIndex); - int tripScheduleIndex = -1; // First loop iteration will immediately increment to 0. - for (TripSchedule schedule : pattern.tripSchedules) { + for (TripSchedule schedule : filteredPattern.runningFrequencyTrips) { tripScheduleIndex++; - - // If this trip's service is inactive (it's not running) or it's a scheduled (non-freq) trip, skip it. - if (!servicesActive.get(schedule.serviceCode) || schedule.headwaySeconds == null) { - continue; - } // Loop through all the entries for this trip (time windows with service at a given frequency). for (int frequencyEntryIdx = 0; frequencyEntryIdx < schedule.headwaySeconds.length; diff --git a/src/main/java/com/conveyal/r5/profile/RaptorTimer.java b/src/main/java/com/conveyal/r5/profile/RaptorTimer.java index e4b4504e3..1a6954d91 100644 --- a/src/main/java/com/conveyal/r5/profile/RaptorTimer.java +++ b/src/main/java/com/conveyal/r5/profile/RaptorTimer.java @@ -7,6 +7,8 @@ public class RaptorTimer { public final ExecutionTimer fullSearch = new ExecutionTimer("Full range-Raptor search"); + public final ExecutionTimer patternFiltering = new ExecutionTimer(fullSearch, "Pattern filtering"); + public final ExecutionTimer scheduledSearch = new ExecutionTimer(fullSearch, "Scheduled/bounds search"); public final ExecutionTimer scheduledSearchTransit = new ExecutionTimer(scheduledSearch, "Scheduled search"); diff --git a/src/main/java/com/conveyal/r5/transit/FilteredPattern.java b/src/main/java/com/conveyal/r5/transit/FilteredPattern.java new file mode 100644 index 000000000..5ef398791 --- /dev/null +++ b/src/main/java/com/conveyal/r5/transit/FilteredPattern.java @@ -0,0 +1,65 @@ +package com.conveyal.r5.transit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.List; + +/** + * FilteredPatterns correspond to a single specific TripPattern, indicating all the trips running on a particular day. + * TripPatterns contain all the trips on a route that follow the same stop sequence. This often includes trips on + * different days of the week or special schedules where vehicles travel faster or slower. By filtering down to only + * those trips running on a particular day (a particular set of service codes), we usually get a smaller set of trips + * with no overtaking, which enables certain optimizations and is more efficient for routing. + */ +public class FilteredPattern { + + private static Logger LOG = LoggerFactory.getLogger(FilteredPattern.class); + + /** + * Schedule-based (i.e. not frequency-based) trips running in a particular set of GTFS services, sorted in + * ascending order by time of departure from first stop + */ + public List runningScheduledTrips = new ArrayList<>(); + + /** Frequency-based trips active in a particular set of GTFS services */ + public List runningFrequencyTrips = new ArrayList<>(); + + /** If no active schedule-based trip of this filtered pattern overtakes another. */ + public boolean noScheduledOvertaking; + + /** + * Filter the trips in a source TripPattern, excluding trips not active in the supplied set of services, and + * dividing them into separate scheduled and frequency trip lists. Check the runningScheduledTrips for overtaking. + */ + public FilteredPattern (TripPattern source, BitSet servicesActive) { + for (TripSchedule schedule : source.tripSchedules) { + if (servicesActive.get(schedule.serviceCode)) { + if (schedule.headwaySeconds == null) { + runningScheduledTrips.add(schedule); + } else { + runningFrequencyTrips.add(schedule); + } + } + } + // Check whether any running trip on this pattern overtakes another + noScheduledOvertaking = true; + for (int i = 0; i < runningScheduledTrips.size() - 1; i++) { + if (overtakes(runningScheduledTrips.get(i), runningScheduledTrips.get(i + 1))) { + noScheduledOvertaking = false; + LOG.warn("Overtaking: route {} pattern {}", source.routeId, source.originalId); + break; + } + } + } + + private static boolean overtakes (TripSchedule a, TripSchedule b) { + for (int s = 0; s < a.departures.length; s++) { + if (a.departures[s] > b.departures[s]) return true; + } + return false; + } + +} diff --git a/src/main/java/com/conveyal/r5/transit/FilteredPatternCache.java b/src/main/java/com/conveyal/r5/transit/FilteredPatternCache.java new file mode 100644 index 000000000..712d1d6c3 --- /dev/null +++ b/src/main/java/com/conveyal/r5/transit/FilteredPatternCache.java @@ -0,0 +1,50 @@ +package com.conveyal.r5.transit; + +import com.conveyal.r5.api.util.TransitModes; +import com.conveyal.r5.util.Tuple2; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; + +import java.util.BitSet; +import java.util.EnumSet; + +/** + * Stores the patterns and trips relevant for routing based on the transit modes and date in an analysis request. + * We can't just cache the single most recently used filtered patterns, because a worker might need to simultaneously + * handle two requests for the same scenario on different dates or with different modes. + * + * There are good reasons why this cache is specific to a single TransitLayer (representing one specific scenario). + * To create FilteredPatterns we need the source TransitLayer object. LoadingCaches must compute values based only on + * their keys. So a system-wide FilteredPatternCache would either need to recursively look up TransportNetworks in + * the TransportNetworkCache, or would need to have TransportNetwork or TransitLayer references in its keys. Neither + * of these seems desirable - the latter would impede garbage collection of evicted TransportNetworks. + */ +public class FilteredPatternCache { + + /** + * All FilteredPatterns stored in this cache will be derived from this single TransitLayer representing a single + * scenario, but for different unique combinations of (transitModes, services). + */ + private final TransitLayer transitLayer; + + private final LoadingCache cache; + + public FilteredPatternCache (TransitLayer transitLayer) { + this.transitLayer = transitLayer; + this.cache = Caffeine.newBuilder().maximumSize(2).build(key -> { + return new FilteredPatterns(transitLayer, key.a, key.b); + }); + } + + // TODO replace all keys and tuples with Java 16/17 Records + private static class Key extends Tuple2, BitSet> { + public Key (EnumSet transitModes, BitSet servicesActive) { + super(transitModes, servicesActive); + } + } + + public FilteredPatterns get (EnumSet transitModes, BitSet servicesActive) { + return cache.get(new Key(transitModes, servicesActive)); + } + +} diff --git a/src/main/java/com/conveyal/r5/transit/FilteredPatterns.java b/src/main/java/com/conveyal/r5/transit/FilteredPatterns.java new file mode 100644 index 000000000..59173a0e3 --- /dev/null +++ b/src/main/java/com/conveyal/r5/transit/FilteredPatterns.java @@ -0,0 +1,63 @@ +package com.conveyal.r5.transit; + +import com.conveyal.r5.api.util.TransitModes; +import com.conveyal.r5.util.Tuple2; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.EnumSet; +import java.util.List; + +import static com.conveyal.r5.transit.TransitLayer.getTransitModes; + +/** + * Holds all the FilteredPatterns instances for a particular TransitLayer (scenario) given a particular set of + * filtering criteria (transit modes and active services). There is one FilteredPattern instance for each TripPattern + * that is present in the filtered TransitLayer. Many TripPatterns contain a mixture of trips from different days, + * and those trips appear to overtake one another if we do not filter them down. Filtering allows us to flag more + * effectively which patterns have no overtaking, which is useful because departure time searches can be then optimized + * for patterns with no overtaking. All trips in a TripPattern are defined to be on same route, and GTFS allows only one + * mode per route. + */ +public class FilteredPatterns { + + /** + * List with the same length and indexes as the unfiltered TripPatterns in the input TransitLayer. + * Patterns that do not meet the mode/services filtering criteria are recorded as null. + */ + public final List patterns; + + /** The indexes of the trip patterns running on a given day with frequency-based trips of selected modes. */ + public BitSet runningFrequencyPatterns = new BitSet(); + + /** The indexes of the trip patterns running on a given day with scheduled trips of selected modes. */ + public BitSet runningScheduledPatterns = new BitSet(); + + /** + * Construct FilteredPatterns from the given TransitLayer, filtering for the specified modes and active services. + * It's tempting to use List.of() or Collectors.toUnmodifiableList() but these cause an additional array copy. + */ + public FilteredPatterns (TransitLayer transitLayer, EnumSet modes, BitSet services) { + List sourcePatterns = transitLayer.tripPatterns; + patterns = new ArrayList<>(sourcePatterns.size()); + for (int patternIndex = 0; patternIndex < sourcePatterns.size(); patternIndex++) { + TripPattern pattern = sourcePatterns.get(patternIndex); + RouteInfo routeInfo = transitLayer.routes.get(pattern.routeIndex); + TransitModes mode = getTransitModes(routeInfo.route_type); + if (pattern.servicesActive.intersects(services) && modes.contains(mode)) { + patterns.add(new FilteredPattern(pattern, services)); + // At least one trip on this pattern is relevant, based on the profile request's date and modes. + if (pattern.hasFrequencies) { + runningFrequencyPatterns.set(patternIndex); + } + // Schedule case is not an "else" clause because we support patterns with both frequency and schedule. + if (pattern.hasSchedules) { + runningScheduledPatterns.set(patternIndex); + } + } else { + patterns.add(null); + } + } + } + +} diff --git a/src/main/java/com/conveyal/r5/transit/TransitLayer.java b/src/main/java/com/conveyal/r5/transit/TransitLayer.java index dc53e7921..452d3040f 100644 --- a/src/main/java/com/conveyal/r5/transit/TransitLayer.java +++ b/src/main/java/com/conveyal/r5/transit/TransitLayer.java @@ -46,6 +46,7 @@ import java.util.BitSet; import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -104,6 +105,9 @@ public class TransitLayer implements Serializable, Cloneable { public List tripPatterns = new ArrayList<>(); + /** Stores the relevant patterns and trips based on the transit modes and date in an analysis request. */ + public transient FilteredPatternCache filteredPatternCache = new FilteredPatternCache(this); + // Maybe we need a StopStore that has (streetVertexForStop, transfers, flags, etc.) public TIntList streetVertexForStop = new TIntArrayList(); @@ -748,6 +752,7 @@ public TransitLayer scenarioCopy(TransportNetwork newScenarioNetwork, boolean wi // the scenario that modified it. If the scenario will not affect the contents of the layer, its // scenarioId remains unchanged as is done in StreetLayer. copy.scenarioId = newScenarioNetwork.scenarioId; + copy.filteredPatternCache = new FilteredPatternCache(copy); } return copy; } diff --git a/src/main/java/com/conveyal/r5/transit/TripPattern.java b/src/main/java/com/conveyal/r5/transit/TripPattern.java index 88324db66..7c7e08224 100644 --- a/src/main/java/com/conveyal/r5/transit/TripPattern.java +++ b/src/main/java/com/conveyal/r5/transit/TripPattern.java @@ -22,8 +22,8 @@ import java.util.stream.StreamSupport; /** + * All the Trips on the same Route that have the same sequence of stops, with the same pickup/dropoff options. * This is like a Transmodel JourneyPattern. - * All the trips on the same Route that have the same sequence of stops, with the same pickup/dropoff options. */ public class TripPattern implements Serializable, Cloneable { @@ -33,6 +33,7 @@ public class TripPattern implements Serializable, Cloneable { * This is the ID of this trip pattern _in the original transport network_. This is important because if it were the * ID in this transport network the ID would depend on the order of application of scenarios, and because this ID is * used to map results back to the original network. + * TODO This concept of an "original" transport network may be obsolete, this field doesn't seem to be used anywhere. */ public int originalId; @@ -44,8 +45,7 @@ public class TripPattern implements Serializable, Cloneable { public PickDropType[] dropoffs; public BitSet wheelchairAccessible; // One bit per stop - /** TripSchedules for all trips following this pattern, sorted in ascending order by time of departure from first - * stop */ + /** TripSchedules for all trips in this pattern, sorted in ascending order by time of departure from first stop. */ public List tripSchedules = new ArrayList<>(); /** GTFS shape for this pattern. Should be left null in non-customer-facing applications */ @@ -67,8 +67,8 @@ public class TripPattern implements Serializable, Cloneable { public BitSet servicesActive = new BitSet(); /** - * index of this route in TransitLayer data. -1 if detailed route information has not been loaded - * TODO clarify what "this route" means. The route of this tripPattern? + * The index of this TripPatterns's route in the TransitLayer, or -1 if not yet loaded. + * Do we really want/need this redundant representation of routeId? */ public int routeIndex = -1; @@ -132,6 +132,8 @@ public void setOrVerifyDirection (int directionId) { /** * Linear search. * @return null if no departure is possible. + * FIXME this is unused. And is active true by definition (this.servicesActive is a BitSet with serviceCode set for + * every one of this.tripSchedules)? */ TripSchedule findNextDeparture (int time, int stopOffset) { TripSchedule bestSchedule = null; @@ -177,9 +179,7 @@ public String toStringDetailed (TransitLayer transitLayer) { return sb.toString(); } - /** - * @return true when none of the supplied tripIds are on this pattern. - */ + /** @return true when none of the supplied tripIds are on this pattern. */ public boolean containsNoTrips(Set tripIds) { return this.tripSchedules.stream().noneMatch(ts -> tripIds.contains(ts.tripId)); } @@ -225,5 +225,4 @@ public List getHopGeometries(TransitLayer transitLayer) { } return geometries; } - } diff --git a/src/main/java/com/conveyal/r5/util/Tuple2.java b/src/main/java/com/conveyal/r5/util/Tuple2.java new file mode 100644 index 000000000..595e0f356 --- /dev/null +++ b/src/main/java/com/conveyal/r5/util/Tuple2.java @@ -0,0 +1,31 @@ +package com.conveyal.r5.util; + +import java.util.Objects; + +/** + * Generic logic for a 2-tuple of different types. + * Reduces high-maintenance boilerplate clutter when making map key types. + * TODO replace with Records in Java 16 or 17 + */ +public class Tuple2 { + public final A a; + public final B b; + + public Tuple2 (A a, B b) { + this.a = a; + this.b = b; + } + + @Override + public boolean equals (Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Tuple2 tuple2 = (Tuple2) o; + return Objects.equals(a, tuple2.a) && Objects.equals(b, tuple2.b); + } + + @Override + public int hashCode () { + return Objects.hash(a, b); + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 9a893278c..d1fb15d9d 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,4 +1,4 @@ - +