diff --git a/src/main/java/org/opentripplanner/profile/AnalystProfileRouterPrototype.java b/src/main/java/org/opentripplanner/profile/AnalystProfileRouterPrototype.java index 18743a0a1b2..cc244dd947e 100644 --- a/src/main/java/org/opentripplanner/profile/AnalystProfileRouterPrototype.java +++ b/src/main/java/org/opentripplanner/profile/AnalystProfileRouterPrototype.java @@ -181,7 +181,7 @@ public TimeSurface.RangeSet route () { // Iterate over street intersections in the vicinity of this particular transit stop. // Shift the time range at this transit stop, merging it into that for all reachable street intersections. TimeRange rangeAtTransitStop = times.get(stop); - TObjectIntMap distanceToVertex = stopTreeCache.getDistancesForStop(tstop); + TObjectIntMap distanceToVertex = null; // FIXME stopTreeCache.getDistancesForStop(tstop); for (TObjectIntIterator iter = distanceToVertex.iterator(); iter.hasNext(); ) { iter.advance(); Vertex vertex = iter.key(); diff --git a/src/main/java/org/opentripplanner/profile/Contiguous2DIntArray.java b/src/main/java/org/opentripplanner/profile/Contiguous2DIntArray.java new file mode 100644 index 00000000000..f4c0286bce4 --- /dev/null +++ b/src/main/java/org/opentripplanner/profile/Contiguous2DIntArray.java @@ -0,0 +1,55 @@ +package org.opentripplanner.profile; + + +import java.util.Arrays; + +public class Contiguous2DIntArray { + + final int dx, dy; + final int[] values; + + public Contiguous2DIntArray (int dx, int dy) { + this.dx = dx; + this.dy = dy; + values = new int[dx * dy]; + } + + public void initialize (int initialValue) { + Arrays.fill(values, initialValue); + } + + public void setX (int x, int value) { + Arrays.fill(values, x * dy, (x + 1) * dy, value); + } + + public void setY (int y, int value) { + int index = y; + while (index < values.length) { + values[index] = value; + index += dy; + } + } + + public int get (int x, int y) { + return values[x * dy + y]; + } + + public void set (int x, int y, int value) { + values[x * dy + y] = value; + } + + public boolean setIfLess (int x, int y, int value) { + int index = x * dy + y; + if (values[index] > value) { + values[index] = value; + return true; + } + return false; + } + + public void adjust (int x, int y, int amount) { + int index = x * dy + y; + values[index] += amount; + } + +} diff --git a/src/main/java/org/opentripplanner/profile/ProfileRouter.java b/src/main/java/org/opentripplanner/profile/ProfileRouter.java index f74858a6b6a..352b86d3885 100644 --- a/src/main/java/org/opentripplanner/profile/ProfileRouter.java +++ b/src/main/java/org/opentripplanner/profile/ProfileRouter.java @@ -580,7 +580,7 @@ private void makeSurfaces() { TransitStop tstop = graph.index.stopVertexForStop.get(stop); // Iterate over street intersections in the vicinity of this particular transit stop. // Shift the time range at this transit stop, merging it into that for all reachable street intersections. - TObjectIntMap distanceToVertex = stopTreeCache.getDistancesForStop(tstop); + TObjectIntMap distanceToVertex = null; //FIXME stopTreeCache.getDistancesForStop(tstop); for (TObjectIntIterator iter = distanceToVertex.iterator(); iter.hasNext(); ) { iter.advance(); Vertex vertex = iter.key(); diff --git a/src/main/java/org/opentripplanner/profile/PropagatedHistogramsStore.java b/src/main/java/org/opentripplanner/profile/PropagatedHistogramsStore.java new file mode 100644 index 00000000000..24371bf536f --- /dev/null +++ b/src/main/java/org/opentripplanner/profile/PropagatedHistogramsStore.java @@ -0,0 +1,40 @@ +package org.opentripplanner.profile; + +import gnu.trove.list.TIntList; +import gnu.trove.list.array.TIntArrayList; +import org.opentripplanner.routing.graph.Vertex; + +public class PropagatedHistogramsStore extends Contiguous2DIntArray { + + int nBins; + + public PropagatedHistogramsStore (int nBins) { + super(Vertex.getMaxIndex(), nBins); + this.nBins = nBins; + } + + /** Record a travel time observation at a vertex. Increments the histogram bin for the given travel time. */ + private void updateHistogram (int vertexIndex, int travelTimeSeconds) { + int bin = travelTimeSeconds / 60; + if (bin < nBins) { + adjust(vertexIndex, bin, 1); + } + } + + /** Return the histogram for a given vertex as an array of ints. */ + public TIntList getHistogram (Vertex vertex) { + int index = vertex.getIndex() * dy; + return TIntArrayList.wrap(values).subList(index, index + dy); + } + + /** + * Given a list of minimum travel times to all street vertices at a single departure time, + * merge them into the per-street-vertex histograms for the whole time window. + */ + public void mergeIn (int[] timesForVertices) { + for (int v = 0; v < timesForVertices.length; v++) { + updateHistogram (v, timesForVertices[v]); + } + } + +} diff --git a/src/main/java/org/opentripplanner/profile/PropagatedTimesStore.java b/src/main/java/org/opentripplanner/profile/PropagatedTimesStore.java new file mode 100644 index 00000000000..9e1934b2346 --- /dev/null +++ b/src/main/java/org/opentripplanner/profile/PropagatedTimesStore.java @@ -0,0 +1,73 @@ +package org.opentripplanner.profile; + +import org.opentripplanner.analyst.TimeSurface; +import org.opentripplanner.routing.graph.Graph; +import org.opentripplanner.routing.graph.Vertex; + +import java.util.Arrays; + +/** + * Stores times propagated to the street network in one-to-many repeated raptor profile routing. + * At each raptor call, we find minimum travel times to each transit stop. Those must be propagated out to the street + * network, giving minimum travel times to each street vertex. Those results are then merged into the summary + * statistics per street vertex over the whole time window (over many RAPTOR calls). + * This class handles the storage and merging of those summary statistics. + */ +public class PropagatedTimesStore { + + // Four parallel arrays has worse locality than one big 4|V|-length flat array, but merging per-raptor-call values + // into this summary statistics storage is not the slow part of the algorithm. Optimization should concentrate on + // the propagation of minimum travel times from transit stops to the street vertices. + + Graph graph; + int size; + int[] mins, maxs, sums, counts; + + public PropagatedTimesStore(Graph graph) { + this.graph = graph; + size = Vertex.getMaxIndex(); + mins = new int[size]; + maxs = new int[size]; + sums = new int[size]; + counts = new int[size]; + Arrays.fill(mins, Integer.MAX_VALUE); + } + + /** + * Merge in min travel times to all vertices from one raptor call, finding minimum-of-min, maximum-of-min, and + * average-of-min travel times. + */ + public void mergeIn(int[] news) { + for (int i = 0; i < size; i++) { + int newValue = news[i]; + if (newValue == 0) { + continue; + } + if (mins[i] > newValue) { + mins[i] = newValue; + } + if (maxs[i] < newValue) { + maxs[i] = newValue; + } + sums[i] += newValue; + counts[i] += 1; + } + } + + /** You need to pass in a pre-constructed rangeSet because it requires a reference to the profile router. */ + public void makeSurfaces(TimeSurface.RangeSet rangeSet) { + for (Vertex vertex : graph.index.vertexForId.values()) { + int min = mins[vertex.getIndex()]; + int max = maxs[vertex.getIndex()]; + int sum = sums[vertex.getIndex()]; + int count = counts[vertex.getIndex()]; + if (count <= 0) + continue; + // Count is positive, extrema and sum must also be present + rangeSet.min.times.put(vertex, min); + rangeSet.max.times.put(vertex, max); + rangeSet.avg.times.put(vertex, sum / count); + } + } + +} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/profile/RepeatedRaptorProfileRouter.java b/src/main/java/org/opentripplanner/profile/RepeatedRaptorProfileRouter.java index 2f6052d3976..8d8ab364348 100644 --- a/src/main/java/org/opentripplanner/profile/RepeatedRaptorProfileRouter.java +++ b/src/main/java/org/opentripplanner/profile/RepeatedRaptorProfileRouter.java @@ -5,7 +5,6 @@ import gnu.trove.map.TObjectLongMap; import gnu.trove.map.hash.TObjectIntHashMap; import gnu.trove.map.hash.TObjectLongHashMap; -import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.gtfs.model.Route; import org.opentripplanner.analyst.TimeSurface; import org.opentripplanner.api.parameter.QualifiedModeSet; @@ -49,15 +48,14 @@ public class RepeatedRaptorProfileRouter { public Graph graph; + // The spacing in minutes between RAPTOR calls within the time window + public int stepMinutes = 2; + /** Three time surfaces for min, max, and average travel time over the given time window. */ public TimeSurface.RangeSet timeSurfaceRangeSet; - /** Minimum maximum earliest-arrival travel time at each transit stop over the given time window. */ - public TObjectIntMap mins = new TObjectIntHashMap(3000, 0.75f, Integer.MAX_VALUE); - public TObjectIntMap maxs = new TObjectIntHashMap(3000, 0.75f, Integer.MIN_VALUE); - - /** If not null, completely skip this route during the calculations. Format agency:id */ - public AgencyAndId banRoute = null; + /** If not null, completely skip this agency during the calculations. */ + public String banAgency = null; /** The sum of all earliest-arrival travel times to a given transit stop. Will be divided to create an average. */ TObjectLongMap accumulator = new TObjectLongHashMap(); @@ -79,40 +77,46 @@ public void route () { LOG.info("Found {} initial transit stops", accessTimes.size()); - /** The current iteration number (start minute within the time window) for display purposes only. */ - int i = 1; - /** A compacted tabular representation of all the patterns that are running on this date in this time window. */ Map timetables = TripTimeSubset.indexGraph(graph, request.date, request.fromTime, request.toTime + MAX_DURATION); /** If a route is banned, remove all patterns belonging to that route from the timetable. */ - if (banRoute != null) { - Route route = graph.index.routeForId.get(banRoute); - LOG.info("Banning route {}", route); - int n = 0; - for (TripPattern pattern : graph.index.patternsForRoute.get(route)) { - timetables.remove(pattern); - n++; + if (banAgency != null) { + for (Route route : graph.index.routeForId.values()) { + if (route.getAgency().getId().equals(banAgency)) { + LOG.info("Banning route {}", route); + int n = 0; + for (TripPattern pattern : graph.index.patternsForRoute.get(route)) { + timetables.remove(pattern); + n++; + } + LOG.info("Removed {} patterns.", n); + } } - LOG.info("Removed {} patterns.", n); } - /** Iterate over all minutes in the time window, running a RAPTOR search at each minute. */ - for (int startTime = request.toTime - 60; startTime >= request.fromTime; startTime -= 60) { - // Create a state store which will be reused calling RAPTOR with each departure time in reverse order. // This causes portions of the solution that do not change to be reused and should provide some speedup // over naively creating a new, empty state store for each minute. - PathDiscardingRaptorStateStore rss = new PathDiscardingRaptorStateStore(MAX_TRANSFERS + 2); + PathDiscardingRaptorStateStore rss = new PathDiscardingRaptorStateStore(MAX_TRANSFERS * 2 + 1); + + // Summary stats across all minutes of the time window + PropagatedTimesStore windowSummary = new PropagatedTimesStore(graph); + // PropagatedHistogramsStore windowHistograms = new PropagatedHistogramsStore(90); + + /** Iterate over all minutes in the time window, running a RAPTOR search at each minute. */ + for (int i = 0, departureTime = request.toTime - 60 * stepMinutes; + departureTime >= request.fromTime; + departureTime -= 60 * stepMinutes) { - // Log progress every thirty minutes (iterations) - if (++i % 30 == 0) { + // Log progress every N iterations + if (++i % 5 == 0) { LOG.info("Completed {} RAPTOR searches", i); } // The departure time has changed; adjust the maximum clock time that the state store will retain - rss.maxTime = startTime + MAX_DURATION; + rss.maxTime = departureTime + MAX_DURATION; // Reset the counter. This is important if reusing the state store from one call to the next. rss.restart(); @@ -120,49 +124,38 @@ public void route () { // Find the arrival times at the initial transit stops for (TObjectIntIterator it = accessTimes.iterator(); it.hasNext();) { it.advance(); - // this is "transfer" from the origin - rss.put(it.key(), startTime + it.value(), true); + rss.put(it.key(), departureTime + it.value(), true); // store walk times for reachable transit stops } - // Call the RAPTOR algorithm for this start time - Raptor raptor = new Raptor(graph, MAX_TRANSFERS, request.walkSpeed, rss, startTime, request.date, timetables); + // Call the RAPTOR algorithm for this particular departure time + Raptor raptor = new Raptor(graph, MAX_TRANSFERS, request.walkSpeed, rss, departureTime, request.date, timetables); raptor.run(); - - // Loop over all transit stops reached by RAPTOR at this minute, - // updating min, max, and average travel time across all minutes (for the entire time window) - for (TObjectIntIterator it = raptor.iterator(); it.hasNext();) { - it.advance(); - - int et = it.value() - startTime; - - // In the dynamic programming (range-RAPTOR) method, some travel times can be left over from a previous - // RAPTOR call at a later departure time, and therefore be greater than the time limit -// if (et > MAX_DURATION) -// continue; - - TransitStop v = it.key(); - if (et < mins.get(v)) - mins.put(v, et); - - if (et > maxs.get(v)) - maxs.put(v, et); - - accumulator.putIfAbsent(v, 0); - counts.putIfAbsent(v, 0); - - accumulator.adjustValue(v, et); - counts.increment(v); + + // Propagate minimum travel times out to vertices in the street network + StopTreeCache stopTreeCache = graph.index.getStopTreeCache(); + TObjectIntIterator resultIterator = raptor.iterator(); + int[] minsPerVertex = new int[Vertex.getMaxIndex()]; + while (resultIterator.hasNext()) { + resultIterator.advance(); + TransitStop transitStop = resultIterator.key(); + int arrivalTime = resultIterator.value(); + if (arrivalTime == Integer.MAX_VALUE) continue; // stop was not reached in this round (why was it included in map?) + int elapsedTime = arrivalTime - departureTime; + stopTreeCache.propagateStop(transitStop, elapsedTime, request.walkSpeed, minsPerVertex); } + // We now have the minimum travel times to reach each street vertex for this departure minute. + // Accumulate them into the summary statistics for the entire time window. + windowSummary.mergeIn(minsPerVertex); + // The Holy Grail: histograms per street vertex. It actually seems as fast or faster than summary stats. + // windowHistograms.mergeIn(minsPerVertex); } - - // Disabled until we're sure transit routing works right - // LOG.info("Profile request complete, propagating to the street network"); - // makeSurfaces(); - + + LOG.info("Profile request complete, creating time surfaces."); + windowSummary.makeSurfaces(timeSurfaceRangeSet); LOG.info("Profile request finished in {} seconds", (System.currentTimeMillis() - computationStartTime) / 1000.0); } - + /** find the boarding stops */ private TObjectIntMap findInitialStops(boolean dest) { double lat = dest ? request.toLat : request.fromLat; @@ -183,6 +176,10 @@ private TObjectIntMap findInitialStops(boolean dest) { AStar astar = new AStar(); rr.longDistance = true; rr.setNumItineraries(1); + + // Set a path parser to avoid strange edge sequences. + // TODO choose a path parser that actually works here, or reuse nearbyStopFinder! + //rr.rctx.pathParsers = new PathParser[] { new BasicPathParser() }; ShortestPathTree spt = astar.getShortestPathTree(rr, 5); // timeout in seconds TObjectIntMap accessTimes = new TObjectIntHashMap(); @@ -193,76 +190,16 @@ private TObjectIntMap findInitialStops(boolean dest) { accessTimes.put(tstop, (int) s.getElapsedTimeSeconds()); } } - - // initialize propagation with direct modes + + // Initialize time surfaces (which will hold the final results) using the on-street search parameters timeSurfaceRangeSet = new TimeSurface.RangeSet(); timeSurfaceRangeSet.min = new TimeSurface(spt, false); timeSurfaceRangeSet.max = new TimeSurface(spt, false); timeSurfaceRangeSet.avg = new TimeSurface(spt, false); - - rr.cleanup(); - /* - TransitStop bestStop = null; - int bestTime = Integer.MAX_VALUE; - for (TransitStop stop : accessTimes.keySet()) { - int etime = accessTimes.get(stop); - if (etime < bestTime) { - bestTime = etime; - bestStop = stop; - } - } - LOG.info("{} at {} sec", bestStop, bestTime); - accessTimes.clear(); - accessTimes.put(bestStop, bestTime); - */ + rr.cleanup(); return accessTimes; } - private void makeSurfaces () { - LOG.info("Propagating from transit stops to the street network..."); - // Grab a cached map of distances to street intersections from each transit stop - StopTreeCache stopTreeCache = graph.index.getStopTreeCache(); - // Iterate over all nondominated rides at all clusters - for (TransitStop tstop : mins.keySet()) { - int lb0 = mins.get(tstop); - int ub0 = maxs.get(tstop); - int avg0 = (int) (accumulator.get(tstop) / counts.get(tstop)); - - // Iterate over street intersections in the vicinity of this particular transit stop. - // Shift the time range at this transit stop, merging it into that for all reachable street intersections. - TObjectIntMap distanceToVertex = stopTreeCache.getDistancesForStop(tstop); - for (TObjectIntIterator iter = distanceToVertex.iterator(); iter.hasNext(); ) { - iter.advance(); - Vertex vertex = iter.key(); - // distance in meters over walkspeed in meters per second --> seconds - int egressWalkTimeSeconds = (int) (iter.value() / request.walkSpeed); - if (egressWalkTimeSeconds > request.maxWalkTime * 60) { - continue; - } - int propagated_min = lb0 + egressWalkTimeSeconds; - int propagated_max = ub0 + egressWalkTimeSeconds; - // TODO: we can't take the min propagated average and call it an average - int propagated_avg = avg0 + egressWalkTimeSeconds; - int existing_min = timeSurfaceRangeSet.min.times.get(vertex); - int existing_max = timeSurfaceRangeSet.max.times.get(vertex); - int existing_avg = timeSurfaceRangeSet.avg.times.get(vertex); - // FIXME this is taking the least lower bound and the least upper bound - // which is not necessarily wrong but it's a crude way to perform the combination - if (existing_min == TimeSurface.UNREACHABLE || existing_min > propagated_min) { - timeSurfaceRangeSet.min.times.put(vertex, propagated_min); - } - if (existing_max == TimeSurface.UNREACHABLE || existing_max > propagated_max) { - timeSurfaceRangeSet.max.times.put(vertex, propagated_max); - } - if (existing_avg == TimeSurface.UNREACHABLE || existing_avg > propagated_avg) { - timeSurfaceRangeSet.avg.times.put(vertex, propagated_avg); - } - } - } - - LOG.info("Done with propagation."); - /* Store the results in a field in the router object. */ - } } diff --git a/src/main/java/org/opentripplanner/profile/StopTreeCache.java b/src/main/java/org/opentripplanner/profile/StopTreeCache.java index da788601dc9..7cbf89e369f 100644 --- a/src/main/java/org/opentripplanner/profile/StopTreeCache.java +++ b/src/main/java/org/opentripplanner/profile/StopTreeCache.java @@ -1,10 +1,6 @@ package org.opentripplanner.profile; import com.beust.jcommander.internal.Maps; - -import gnu.trove.map.TObjectIntMap; -import gnu.trove.map.hash.TObjectIntHashMap; - import org.opentripplanner.routing.algorithm.AStar; import org.opentripplanner.routing.core.RoutingRequest; import org.opentripplanner.routing.core.State; @@ -29,7 +25,8 @@ public class StopTreeCache { private static final Logger LOG = LoggerFactory.getLogger(StopTreeCache.class); final int timeCutoffMinutes; - private final Map> distancesForStop = Maps.newHashMap(); + // Flattened 2D array of (streetVertexIndex, distanceFromStop) for each TransitStop + private final Map distancesForStop = Maps.newHashMap(); public StopTreeCache (Graph graph, int timeCutoffMinutes) { this.timeCutoffMinutes = timeCutoffMinutes; @@ -46,17 +43,43 @@ public StopTreeCache (Graph graph, int timeCutoffMinutes) { rr.dominanceFunction = new DominanceFunction.EarliestArrival(); rr.setNumItineraries(1); ShortestPathTree spt = astar.getShortestPathTree(rr, 5); // timeout in seconds - TObjectIntMap distanceToVertex = new TObjectIntHashMap<>(1000, 0.5f, Integer.MAX_VALUE); - for (State state : spt.getAllStates()) { - distanceToVertex.put(state.getVertex(), (int)state.walkDistance); + // Copy vertex indices and distances into a flattened 2D array + int[] distances = new int[spt.getVertexCount() * 2]; + int i = 0; + for (Vertex vertex : spt.getVertices()) { + State state = spt.getState(vertex); + distances[i++] = vertex.getIndex(); + distances[i++] = (int) state.getWalkDistance(); } - distancesForStop.put(tstop, distanceToVertex); + distancesForStop.put(tstop, distances); rr.cleanup(); } LOG.info("Done caching distances to nearby street intersections from each transit stop."); } - public TObjectIntMap getDistancesForStop(TransitStop tstop) { - return distancesForStop.get(tstop); + /** + * Given a travel time to a transit stop, fill in the array with minimum travel times to all nearby street vertices. + * This function is meant to be called repeatedly on multiple transit stops, accumulating minima + * into the same targetArray. + */ + public void propagateStop(TransitStop transitStop, int baseTimeSeconds, double walkSpeed, int[] targetArray) { + // Iterate over street intersections in the vicinity of this particular transit stop. + // Shift the time range at this transit stop, merging it into that for all reachable street intersections. + int[] distances = distancesForStop.get(transitStop); + int v = 0; + while (v < distances.length) { + // Unravel flattened 2D array + int vertexIndex = distances[v++]; + int distance = distances[v++]; + // distance in meters over walkspeed in meters per second --> seconds + int egressWalkTimeSeconds = (int) (distance / walkSpeed); + int propagated_time = baseTimeSeconds + egressWalkTimeSeconds; + int existing_min = targetArray[vertexIndex]; + if (existing_min == 0 || existing_min > propagated_time) { + targetArray[vertexIndex] = propagated_time; + } + } + } + } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/PathDiscardingRaptorStateStore.java b/src/main/java/org/opentripplanner/routing/algorithm/PathDiscardingRaptorStateStore.java index 84b73939045..c6ff0e69c65 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/PathDiscardingRaptorStateStore.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/PathDiscardingRaptorStateStore.java @@ -16,7 +16,7 @@ public class PathDiscardingRaptorStateStore implements RaptorStateStore { private TObjectIntMap[] matrix; /** The best time to reach each stop in any round by transit only, not by transfer from another stop. */ - private TObjectIntMap bestStops; + public TObjectIntMap bestStops; /** The maximum acceptable clock time in seconds since midnight. All arrivals after this time will be ignored. */