/* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (props, at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.opentripplanner.routing.algorithm.strategies; import gnu.trove.map.TObjectDoubleMap; import gnu.trove.map.hash.TObjectDoubleHashMap; import org.opentripplanner.common.pqueue.BinHeap; import org.opentripplanner.routing.core.RoutingRequest; import org.opentripplanner.routing.core.State; import org.opentripplanner.routing.edgetype.StreetTransitLink; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.location.StreetLocation; import org.opentripplanner.routing.spt.DominanceFunction; import org.opentripplanner.routing.spt.ShortestPathTree; import org.opentripplanner.routing.vertextype.StreetVertex; import org.opentripplanner.routing.vertextype.TransitStop; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Set; /** * This the goal direction heuristic used for transit searches. * * Euclidean heuristics are terrible for transit routing because the maximum transit speed is quite high, especially * relative to the walk speed. Transit can require going away from the destination in Euclidean space to approach it * according to the travel time metric. This heuristic is designed to be good for transit. * * After many experiments storing travel time metrics in tables or embedding them in low-dimensional Euclidean space I * eventually came to the conclusion that the most efficient structure for representing the metric was already right * in front of us: a graph. * * This heuristic searches backward from the target vertex over the street and transit network, removing any * time-dependent component of the network (e.g. by evaluating all boarding wait times as zero). This produces an * admissible heuristic (i.e. it always underestimates path weight) making it valid independent of the clock time. * This is important because you don't know precisely what time you will arrive at the destination until you get there. * * Because we often make use of the first path we find in the main search, this heuristic must be both admissible and * consistent (monotonic). If the heuristic is non-monotonic, nodes can be re-discovered and paths are not necessarily * discovered in order of increasing weight. When finding paths one by one and banning trips or routes, * suboptimal paths may be found and reported before or instead of optimal ones. * * This heuristic was previously not consistent for the reasons discussed in ticket #2153. It was possible for the * "zero zone" around the origin to overlap the egress zone around the destination, leading to decreases in the * heuristic across an edge that were greater in magnitude than the weight of that edge. This has been solved by * creating two separate distance maps, one pre-transit and one post-transit. * * Note that the backward search does not happen in a separate thread. It is interleaved with the main search in a * ratio of N:1 iterations. */ public class InterleavedBidirectionalHeuristic implements RemainingWeightHeuristic { private static final long serialVersionUID = 20160215L; private static Logger LOG = LoggerFactory.getLogger(InterleavedBidirectionalHeuristic.class); // For each step in the main search, how many steps should the reverse search proceed? private static final int HEURISTIC_STEPS_PER_MAIN_STEP = 8; // TODO determine a good value empirically /** The vertex at which the main search begins. */ Vertex origin; /** The vertex that the main search is working towards. */ Vertex target; /** All vertices within walking distance of the origin (the vertex at which the main search begins). */ Set<Vertex> preTransitVertices; /** * A lower bound on the weight of the lowest-cost path to the target (the vertex at which the main search ends) * from each vertex within walking distance of the target. As the heuristic progressively improves, this map will * include lower bounds on path weights for an increasing number of vertices on board transit. */ TObjectDoubleMap<Vertex> postBoardingWeights; Graph graph; RoutingRequest routingRequest; // The maximum weight yet seen at a closed node in the reverse search. The priority queue head has a uniformly // increasing weight, so any unreached transit node must have greater weight than this. double maxWeightSeen = 0; // The priority queue for the interleaved backward search through the transit network. BinHeap<Vertex> transitQueue; // True when the entire transit network has been explored by the reverse search. boolean finished = false; /** * Before the main search begins, the heuristic must search on the streets around the origin and destination. * This also sets up the initial states for the reverse search through the transit network, which progressively * improves lower bounds on travel time to the target to guide the main search. */ @Override public void initialize(RoutingRequest request, long abortTime) { Vertex target = request.rctx.target; if (target == this.target) { LOG.debug("Reusing existing heuristic, the target vertex has not changed."); return; } LOG.debug("Initializing heuristic computation."); this.graph = request.rctx.graph; long start = System.currentTimeMillis(); this.target = target; this.routingRequest = request; request.softWalkLimiting = false; request.softPreTransitLimiting = false; transitQueue = new BinHeap<>(); // Forward street search first, mark street vertices around the origin so H evaluates to 0 TObjectDoubleMap<Vertex> forwardStreetSearchResults = streetSearch(request, false, abortTime); if (forwardStreetSearchResults == null) { return; // Search timed out } preTransitVertices = forwardStreetSearchResults.keySet(); LOG.debug("end forward street search {} ms", System.currentTimeMillis() - start); postBoardingWeights = streetSearch(request, true, abortTime); if (postBoardingWeights == null) { return; // Search timed out } LOG.debug("end backward street search {} ms", System.currentTimeMillis() - start); // once street searches are done, raise the limits to max // because hard walk limiting is incorrect and is observed to cause problems // for trips near the cutoff request.setMaxWalkDistance(Double.POSITIVE_INFINITY); request.setMaxPreTransitTime(Integer.MAX_VALUE); LOG.debug("initialized SSSP"); request.rctx.debugOutput.finishedPrecalculating(); } /** * This function supplies the main search with an (under)estimate of the remaining path weight to the target. * No matter how much progress has been made on the reverse heuristic search, we must return an underestimate * of the cost to reach the target (i.e. the heuristic must be admissible). * All on-street vertices within walking distance of the origin or destination will have been explored by the * heuristic before the main search starts. */ @Override public double estimateRemainingWeight (State s) { final Vertex v = s.getVertex(); if (v instanceof StreetLocation) { // Temporary vertices (StreetLocations) might not be found in the street searches. // Zero is always an underestimate. return 0; } if (v instanceof StreetVertex) { // The main search is on the streets, not on transit. if (s.isEverBoarded()) { // If we have already ridden transit we must be near the destination. If not the map returns INF. return postBoardingWeights.get(v); } else { // We have not boarded transit yet. We have no idea what the weight to the target is so return zero. // We could also use a Euclidean heuristic here. if (preTransitVertices.contains(v)) { return 0; } else { return Double.POSITIVE_INFINITY; } } } else { // The main search is not currently on a street vertex, it's probably on transit. // If the current part of the transit network has been explored, then return the stored lower bound. // Otherwise return the highest lower bound yet seen -- this location must have a higher cost than that. double h = postBoardingWeights.get(v); if (h == Double.POSITIVE_INFINITY) { return maxWeightSeen; } else { return h; } } } @Override public void reset() { } /** * Move backward N steps through the transit network. * This improves the heuristic's knowledge of the transit network as seen from the target, * making its lower bounds on path weight progressively more accurate. */ @Override public void doSomeWork() { if (finished) return; for (int i = 0; i < HEURISTIC_STEPS_PER_MAIN_STEP; ++i) { if (transitQueue.empty()) { finished = true; break; } int uWeight = (int) transitQueue.peek_min_key(); Vertex u = transitQueue.extract_min(); // The weight of the queue head is uniformly increasing. // This is the highest weight ever seen for a closed vertex. maxWeightSeen = uWeight; // Now that this vertex is closed, we can store its weight for use as a lower bound / heuristic value. // We don't implement decrease-key operations though, so check whether a smaller value is already known. double uWeightOld = postBoardingWeights.get(u); if (uWeight < uWeightOld) { // Including when uWeightOld is infinite because the vertex is not yet closed. postBoardingWeights.put(u, uWeight); } else { // The vertex was already closed. This time it necessarily has a higher weight, so skip it. continue; } // This search is proceeding backward relative to the main search. // When the main search is arriveBy the heuristic search looks at OUTgoing edges. for (Edge e : routingRequest.arriveBy ? u.getOutgoing() : u.getIncoming()) { // Do not enter streets in this phase, which should only touch transit. if (e instanceof StreetTransitLink) { continue; } Vertex v = routingRequest.arriveBy ? e.getToVertex() : e.getFromVertex(); double edgeWeight = e.weightLowerBound(routingRequest); // INF heuristic value indicates unreachable (e.g. non-running transit service) // this saves time by not reverse-exploring those routes and avoids maxFound of INF. if (Double.isInfinite(edgeWeight)) { continue; } double vWeight = uWeight + edgeWeight; double vWeightOld = postBoardingWeights.get(v); if (vWeight < vWeightOld) { // Should only happen when vWeightOld is infinite because it is not yet closed. transitQueue.insert(v, vWeight); } } } } /** * Explore the streets around the origin or target, recording the minimum weight of a path to each street vertex. * When searching around the target, also retain the states that reach transit stops since we'll want to * explore the transit network backward, in order to guide the main forward search. * * The main search always proceeds from the "origin" to the "target" (names remain unchanged in arriveBy mode). * The reverse heuristic search always proceeds outward from the target (name remains unchanged in arriveBy). * * When the main search is departAfter: * it gets outgoing edges and traverses them with arriveBy=false, * the heuristic search gets incoming edges and traverses them with arriveBy=true, * the heuristic destination street search also gets incoming edges and traverses them with arriveBy=true, * the heuristic origin street search gets outgoing edges and traverses them with arriveBy=false. * * When main search is arriveBy: * it gets incoming edges and traverses them with arriveBy=true, * the heuristic search gets outgoing edges and traverses them with arriveBy=false, * the heuristic destination street search also gets outgoing edges and traverses them with arriveBy=false, * the heuristic origin street search gets incoming edges and traverses them with arriveBy=true. * The streetSearch method traverses using the real traverse method rather than the lower bound traverse method * because this allows us to keep track of the distance walked. * Perhaps rather than tracking walk distance, we should just check the straight-line radius and * only walk within that distance. This would avoid needing to call the main traversal functions. * * TODO what if the egress segment is by bicycle or car mode? This is no longer admissible. */ private TObjectDoubleMap<Vertex> streetSearch (RoutingRequest rr, boolean fromTarget, long abortTime) { LOG.debug("Heuristic street search around the {}.", fromTarget ? "target" : "origin"); rr = rr.clone(); if (fromTarget) { rr.setArriveBy(!rr.arriveBy); } // Create a map that returns Infinity when it does not contain a vertex. TObjectDoubleMap<Vertex> vertices = new TObjectDoubleHashMap<>(100, 0.5f, Double.POSITIVE_INFINITY); ShortestPathTree spt = new DominanceFunction.MinimumWeight().getNewShortestPathTree(rr); // TODO use normal OTP search for this. BinHeap<State> pq = new BinHeap<State>(); Vertex initVertex = fromTarget ? rr.rctx.target : rr.rctx.origin; State initState = new State(initVertex, rr); pq.insert(initState, 0); while ( ! pq.empty()) { if (abortTime < Long.MAX_VALUE && System.currentTimeMillis() > abortTime) { return null; } State s = pq.extract_min(); Vertex v = s.getVertex(); // At this point the vertex is closed (pulled off heap). // This is the lowest cost we will ever see for this vertex. We can record the cost to reach it. if (v instanceof TransitStop) { // We don't want to continue into the transit network yet, but when searching around the target // place vertices on the transit queue so we can explore the transit network backward later. if (fromTarget) { double weight = s.getWeight(); transitQueue.insert(v, weight); if (weight > maxWeightSeen) { maxWeightSeen = weight; } } continue; } // We don't test whether we're on an instanceof StreetVertex here because some other vertex types // (park and ride or bike rental related) that should also be explored and marked as usable. // Record the cost to reach this vertex. if (!vertices.containsKey(v)) { vertices.put(v, (int) s.getWeight()); // FIXME time or weight? is RR using right mode? } for (Edge e : rr.arriveBy ? v.getIncoming() : v.getOutgoing()) { // arriveBy has been set to match actual directional behavior in this subsearch. // Walk cutoff will happen in the street edge traversal method. State s1 = e.traverse(s); if (s1 == null) { continue; } if (spt.add(s1)) { pq.insert(s1, s1.getWeight()); } } } LOG.debug("Heuristric street search hit {} vertices.", vertices.size()); LOG.debug("Heuristric street search hit {} transit stops.", transitQueue.size()); return vertices; } }