Skip to content

Commit 3c63f05

Browse files
authored
Merge pull request #708 from conveyal/transfer-time
Trip and pattern filtering
2 parents 3a97deb + f81df25 commit 3c63f05

17 files changed

+594
-229
lines changed

.github/workflows/cypress-integration.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- uses: actions/checkout@v2
1717
with:
1818
repository: conveyal/analysis-ui
19-
ref: 62dfe5b8d8e76a095b8f923b6458b75d3e10c2c8
19+
ref: dee1f13ba891e35b859438c97e5f66a0a7347d38
2020
path: ui
2121
- uses: actions/checkout@v2
2222
with:

src/main/java/com/conveyal/r5/analyst/cluster/PathResult.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,18 @@ public static class PathIterations {
155155
this.egress = pathTemplate.stopSequence.egress == null ? null : pathTemplate.stopSequence.egress.toString();
156156
this.transitLegs = pathTemplate.transitLegs(transitLayer);
157157
this.iterations = iterations.stream().map(HumanReadableIteration::new).collect(Collectors.toList());
158+
iterations.forEach(pathTemplate.stopSequence::transferTime); // The transferTime method includes an
159+
// assertion that the transfer time is non-negative, i.e. that the access + egress + wait + ride times of
160+
// a specific iteration do not exceed the total travel time. Perform that sense check here, even though
161+
// the transfer time is not reported to the front-end for the human-readable single-point responses.
162+
// TODO add transferTime to HumanReadableIteration?
158163
}
159164
}
160165

161166
/**
162167
* Returns human-readable details of path iterations, for JSON representation (e.g. in the UI console).
163168
*/
164-
List<PathIterations> getPathIterationsForDestination() {
169+
public List<PathIterations> getPathIterationsForDestination() {
165170
checkState(iterationsForPathTemplates.length == 1, "Paths were stored for multiple " +
166171
"destinations, but only one is being requested");
167172
List<PathIterations> detailsForDestination = new ArrayList<>();

src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java

+137-142
Large diffs are not rendered by default.

src/main/java/com/conveyal/r5/profile/RaptorTimer.java

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ public class RaptorTimer {
77

88
public final ExecutionTimer fullSearch = new ExecutionTimer("Full range-Raptor search");
99

10+
public final ExecutionTimer patternFiltering = new ExecutionTimer(fullSearch, "Pattern filtering");
11+
1012
public final ExecutionTimer scheduledSearch = new ExecutionTimer(fullSearch, "Scheduled/bounds search");
1113

1214
public final ExecutionTimer scheduledSearchTransit = new ExecutionTimer(scheduledSearch, "Scheduled search");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.conveyal.r5.transit;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
import java.util.ArrayList;
7+
import java.util.BitSet;
8+
import java.util.List;
9+
10+
/**
11+
* FilteredPatterns correspond to a single specific TripPattern, indicating all the trips running on a particular day.
12+
* TripPatterns contain all the trips on a route that follow the same stop sequence. This often includes trips on
13+
* different days of the week or special schedules where vehicles travel faster or slower. By filtering down to only
14+
* those trips running on a particular day (a particular set of service codes), we usually get a smaller set of trips
15+
* with no overtaking, which enables certain optimizations and is more efficient for routing.
16+
*/
17+
public class FilteredPattern {
18+
19+
private static Logger LOG = LoggerFactory.getLogger(FilteredPattern.class);
20+
21+
/**
22+
* Schedule-based (i.e. not frequency-based) trips running in a particular set of GTFS services, sorted in
23+
* ascending order by time of departure from first stop
24+
*/
25+
public List<TripSchedule> runningScheduledTrips = new ArrayList<>();
26+
27+
/** Frequency-based trips active in a particular set of GTFS services */
28+
public List<TripSchedule> runningFrequencyTrips = new ArrayList<>();
29+
30+
/** If no active schedule-based trip of this filtered pattern overtakes another. */
31+
public boolean noScheduledOvertaking;
32+
33+
/**
34+
* Filter the trips in a source TripPattern, excluding trips not active in the supplied set of services, and
35+
* dividing them into separate scheduled and frequency trip lists. Check the runningScheduledTrips for overtaking.
36+
*/
37+
public FilteredPattern (TripPattern source, BitSet servicesActive) {
38+
for (TripSchedule schedule : source.tripSchedules) {
39+
if (servicesActive.get(schedule.serviceCode)) {
40+
if (schedule.headwaySeconds == null) {
41+
runningScheduledTrips.add(schedule);
42+
} else {
43+
runningFrequencyTrips.add(schedule);
44+
}
45+
}
46+
}
47+
// Check whether any running trip on this pattern overtakes another
48+
noScheduledOvertaking = true;
49+
for (int i = 0; i < runningScheduledTrips.size() - 1; i++) {
50+
if (overtakes(runningScheduledTrips.get(i), runningScheduledTrips.get(i + 1))) {
51+
noScheduledOvertaking = false;
52+
LOG.warn("Overtaking: route {} pattern {}", source.routeId, source.originalId);
53+
break;
54+
}
55+
}
56+
}
57+
58+
private static boolean overtakes (TripSchedule a, TripSchedule b) {
59+
for (int s = 0; s < a.departures.length; s++) {
60+
if (a.departures[s] > b.departures[s]) return true;
61+
}
62+
return false;
63+
}
64+
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.conveyal.r5.transit;
2+
3+
import com.conveyal.r5.api.util.TransitModes;
4+
import com.conveyal.r5.util.Tuple2;
5+
import com.github.benmanes.caffeine.cache.Caffeine;
6+
import com.github.benmanes.caffeine.cache.LoadingCache;
7+
8+
import java.util.BitSet;
9+
import java.util.EnumSet;
10+
11+
/**
12+
* Stores the patterns and trips relevant for routing based on the transit modes and date in an analysis request.
13+
* We can't just cache the single most recently used filtered patterns, because a worker might need to simultaneously
14+
* handle two requests for the same scenario on different dates or with different modes.
15+
*
16+
* There are good reasons why this cache is specific to a single TransitLayer (representing one specific scenario).
17+
* To create FilteredPatterns we need the source TransitLayer object. LoadingCaches must compute values based only on
18+
* their keys. So a system-wide FilteredPatternCache would either need to recursively look up TransportNetworks in
19+
* the TransportNetworkCache, or would need to have TransportNetwork or TransitLayer references in its keys. Neither
20+
* of these seems desirable - the latter would impede garbage collection of evicted TransportNetworks.
21+
*/
22+
public class FilteredPatternCache {
23+
24+
/**
25+
* All FilteredPatterns stored in this cache will be derived from this single TransitLayer representing a single
26+
* scenario, but for different unique combinations of (transitModes, services).
27+
*/
28+
private final TransitLayer transitLayer;
29+
30+
private final LoadingCache<Key, FilteredPatterns> cache;
31+
32+
public FilteredPatternCache (TransitLayer transitLayer) {
33+
this.transitLayer = transitLayer;
34+
this.cache = Caffeine.newBuilder().maximumSize(2).build(key -> {
35+
return new FilteredPatterns(transitLayer, key.a, key.b);
36+
});
37+
}
38+
39+
// TODO replace all keys and tuples with Java 16/17 Records
40+
private static class Key extends Tuple2<EnumSet<TransitModes>, BitSet> {
41+
public Key (EnumSet<TransitModes> transitModes, BitSet servicesActive) {
42+
super(transitModes, servicesActive);
43+
}
44+
}
45+
46+
public FilteredPatterns get (EnumSet<TransitModes> transitModes, BitSet servicesActive) {
47+
return cache.get(new Key(transitModes, servicesActive));
48+
}
49+
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.conveyal.r5.transit;
2+
3+
import com.conveyal.r5.api.util.TransitModes;
4+
import com.conveyal.r5.util.Tuple2;
5+
6+
import java.util.ArrayList;
7+
import java.util.BitSet;
8+
import java.util.EnumSet;
9+
import java.util.List;
10+
11+
import static com.conveyal.r5.transit.TransitLayer.getTransitModes;
12+
13+
/**
14+
* Holds all the FilteredPatterns instances for a particular TransitLayer (scenario) given a particular set of
15+
* filtering criteria (transit modes and active services). There is one FilteredPattern instance for each TripPattern
16+
* that is present in the filtered TransitLayer. Many TripPatterns contain a mixture of trips from different days,
17+
* and those trips appear to overtake one another if we do not filter them down. Filtering allows us to flag more
18+
* effectively which patterns have no overtaking, which is useful because departure time searches can be then optimized
19+
* for patterns with no overtaking. All trips in a TripPattern are defined to be on same route, and GTFS allows only one
20+
* mode per route.
21+
*/
22+
public class FilteredPatterns {
23+
24+
/**
25+
* List with the same length and indexes as the unfiltered TripPatterns in the input TransitLayer.
26+
* Patterns that do not meet the mode/services filtering criteria are recorded as null.
27+
*/
28+
public final List<FilteredPattern> patterns;
29+
30+
/** The indexes of the trip patterns running on a given day with frequency-based trips of selected modes. */
31+
public BitSet runningFrequencyPatterns = new BitSet();
32+
33+
/** The indexes of the trip patterns running on a given day with scheduled trips of selected modes. */
34+
public BitSet runningScheduledPatterns = new BitSet();
35+
36+
/**
37+
* Construct FilteredPatterns from the given TransitLayer, filtering for the specified modes and active services.
38+
* It's tempting to use List.of() or Collectors.toUnmodifiableList() but these cause an additional array copy.
39+
*/
40+
public FilteredPatterns (TransitLayer transitLayer, EnumSet<TransitModes> modes, BitSet services) {
41+
List<TripPattern> sourcePatterns = transitLayer.tripPatterns;
42+
patterns = new ArrayList<>(sourcePatterns.size());
43+
for (int patternIndex = 0; patternIndex < sourcePatterns.size(); patternIndex++) {
44+
TripPattern pattern = sourcePatterns.get(patternIndex);
45+
RouteInfo routeInfo = transitLayer.routes.get(pattern.routeIndex);
46+
TransitModes mode = getTransitModes(routeInfo.route_type);
47+
if (pattern.servicesActive.intersects(services) && modes.contains(mode)) {
48+
patterns.add(new FilteredPattern(pattern, services));
49+
// At least one trip on this pattern is relevant, based on the profile request's date and modes.
50+
if (pattern.hasFrequencies) {
51+
runningFrequencyPatterns.set(patternIndex);
52+
}
53+
// Schedule case is not an "else" clause because we support patterns with both frequency and schedule.
54+
if (pattern.hasSchedules) {
55+
runningScheduledPatterns.set(patternIndex);
56+
}
57+
} else {
58+
patterns.add(null);
59+
}
60+
}
61+
}
62+
63+
}

src/main/java/com/conveyal/r5/transit/TransitLayer.java

+5
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import java.util.BitSet;
4747
import java.util.Collection;
4848
import java.util.Collections;
49+
import java.util.EnumSet;
4950
import java.util.HashMap;
5051
import java.util.List;
5152
import java.util.Map;
@@ -104,6 +105,9 @@ public class TransitLayer implements Serializable, Cloneable {
104105

105106
public List<TripPattern> tripPatterns = new ArrayList<>();
106107

108+
/** Stores the relevant patterns and trips based on the transit modes and date in an analysis request. */
109+
public transient FilteredPatternCache filteredPatternCache = new FilteredPatternCache(this);
110+
107111
// Maybe we need a StopStore that has (streetVertexForStop, transfers, flags, etc.)
108112
public TIntList streetVertexForStop = new TIntArrayList();
109113

@@ -748,6 +752,7 @@ public TransitLayer scenarioCopy(TransportNetwork newScenarioNetwork, boolean wi
748752
// the scenario that modified it. If the scenario will not affect the contents of the layer, its
749753
// scenarioId remains unchanged as is done in StreetLayer.
750754
copy.scenarioId = newScenarioNetwork.scenarioId;
755+
copy.filteredPatternCache = new FilteredPatternCache(copy);
751756
}
752757
return copy;
753758
}

src/main/java/com/conveyal/r5/transit/TripPattern.java

+8-9
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
import java.util.stream.StreamSupport;
2323

2424
/**
25+
* All the Trips on the same Route that have the same sequence of stops, with the same pickup/dropoff options.
2526
* This is like a Transmodel JourneyPattern.
26-
* All the trips on the same Route that have the same sequence of stops, with the same pickup/dropoff options.
2727
*/
2828
public class TripPattern implements Serializable, Cloneable {
2929

@@ -33,6 +33,7 @@ public class TripPattern implements Serializable, Cloneable {
3333
* This is the ID of this trip pattern _in the original transport network_. This is important because if it were the
3434
* ID in this transport network the ID would depend on the order of application of scenarios, and because this ID is
3535
* used to map results back to the original network.
36+
* TODO This concept of an "original" transport network may be obsolete, this field doesn't seem to be used anywhere.
3637
*/
3738
public int originalId;
3839

@@ -44,8 +45,7 @@ public class TripPattern implements Serializable, Cloneable {
4445
public PickDropType[] dropoffs;
4546
public BitSet wheelchairAccessible; // One bit per stop
4647

47-
/** TripSchedules for all trips following this pattern, sorted in ascending order by time of departure from first
48-
* stop */
48+
/** TripSchedules for all trips in this pattern, sorted in ascending order by time of departure from first stop. */
4949
public List<TripSchedule> tripSchedules = new ArrayList<>();
5050

5151
/** GTFS shape for this pattern. Should be left null in non-customer-facing applications */
@@ -67,8 +67,8 @@ public class TripPattern implements Serializable, Cloneable {
6767
public BitSet servicesActive = new BitSet();
6868

6969
/**
70-
* index of this route in TransitLayer data. -1 if detailed route information has not been loaded
71-
* TODO clarify what "this route" means. The route of this tripPattern?
70+
* The index of this TripPatterns's route in the TransitLayer, or -1 if not yet loaded.
71+
* Do we really want/need this redundant representation of routeId?
7272
*/
7373
public int routeIndex = -1;
7474

@@ -132,6 +132,8 @@ public void setOrVerifyDirection (int directionId) {
132132
/**
133133
* Linear search.
134134
* @return null if no departure is possible.
135+
* FIXME this is unused. And is active true by definition (this.servicesActive is a BitSet with serviceCode set for
136+
* every one of this.tripSchedules)?
135137
*/
136138
TripSchedule findNextDeparture (int time, int stopOffset) {
137139
TripSchedule bestSchedule = null;
@@ -177,9 +179,7 @@ public String toStringDetailed (TransitLayer transitLayer) {
177179
return sb.toString();
178180
}
179181

180-
/**
181-
* @return true when none of the supplied tripIds are on this pattern.
182-
*/
182+
/** @return true when none of the supplied tripIds are on this pattern. */
183183
public boolean containsNoTrips(Set<String> tripIds) {
184184
return this.tripSchedules.stream().noneMatch(ts -> tripIds.contains(ts.tripId));
185185
}
@@ -225,5 +225,4 @@ public List<LineString> getHopGeometries(TransitLayer transitLayer) {
225225
}
226226
return geometries;
227227
}
228-
229228
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.conveyal.r5.util;
2+
3+
import java.util.Objects;
4+
5+
/**
6+
* Generic logic for a 2-tuple of different types.
7+
* Reduces high-maintenance boilerplate clutter when making map key types.
8+
* TODO replace with Records in Java 16 or 17
9+
*/
10+
public class Tuple2<A, B> {
11+
public final A a;
12+
public final B b;
13+
14+
public Tuple2 (A a, B b) {
15+
this.a = a;
16+
this.b = b;
17+
}
18+
19+
@Override
20+
public boolean equals (Object o) {
21+
if (this == o) return true;
22+
if (o == null || getClass() != o.getClass()) return false;
23+
Tuple2<?, ?> tuple2 = (Tuple2<?, ?>) o;
24+
return Objects.equals(a, tuple2.a) && Objects.equals(b, tuple2.b);
25+
}
26+
27+
@Override
28+
public int hashCode () {
29+
return Objects.hash(a, b);
30+
}
31+
}

src/main/resources/logback.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<configuration debug="true">
1+
<configuration>
22

33
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
44
<!-- encoders are by default assigned the type
@@ -15,5 +15,6 @@
1515
<logger name="com.conveyal.osmlib" level="INFO" />
1616
<logger name="com.conveyal.gtfs" level="INFO" />
1717
<logger name="com.conveyal.r5.profile.ExecutionTimer" level="INFO"/>
18+
<logger name="com.conveyal.r5.profile.FastRaptorWorker" level="INFO" />
1819

1920
</configuration>

src/test/java/com/conveyal/r5/analyst/network/DistributionTester.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ public static void assertExpectedDistribution (Distribution expectedDistribution
3232
}
3333
}
3434

35-
}
35+
}

0 commit comments

Comments
 (0)