/** * Implements a multi-objective goal-directed search algorithm like the one in Sec. 4.2 of: Perny * and Spanjaard. Near Admissible Algorithms for Multiobjective Search. * * <p>The ideas being tested here are: Pruning search based on paths already found Near-admissible * search / relaxed dominance Allow resource constraints on transfers and walking * * <p>This approach seems to need a very accurate heuristic to achieve reasonable run times, so for * now it is hard-coded to use the Bidirectional heuristic. * * <p>It will return a list of paths in order of increasing weight, starting at a weight very close * to that of the optimum path. These paths can vary quite a bit in terms of transfers, walk * distance, and trips taken. * * <p>Because the number of boardings and walk distance are considered incomparable to other weight * components, including aggregate weight itself, paths can be pruned due to excessive walking * distance or excessive number of transfers without compromising other paths. * * <p>This path service cannot be used with edges that return multiple (chained) states. * * @author andrewbyrd */ @Component public class MultiObjectivePathServiceImpl extends GenericPathService { private static final Logger LOG = LoggerFactory.getLogger(MultiObjectivePathServiceImpl.class); private static final MonitoringStore store = MonitoringStoreFactory.getStore(); private double[] _timeouts = new double[] {4, 2, 0.6, 0.4}; // seconds private double _maxPaths = 4; private TraverseVisitor traverseVisitor; /** Give up on searching for itineraries after this many seconds have elapsed. */ public void setTimeouts(List<Double> timeouts) { _timeouts = new double[timeouts.size()]; int i = 0; for (Double d : timeouts) _timeouts[i++] = d; } public void setMaxPaths(double numPaths) { _maxPaths = numPaths; } public void setTraverseVisitor(TraverseVisitor traverseVisitor) { this.traverseVisitor = traverseVisitor; } @Override public List<GraphPath> plan( NamedPlace fromPlace, NamedPlace toPlace, Date targetTime, TraverseOptions options, int nItineraries) { ArrayList<String> notFound = new ArrayList<String>(); Vertex fromVertex = getVertexForPlace(fromPlace, options); if (fromVertex == null) { notFound.add("from"); } Vertex toVertex = getVertexForPlace(toPlace, options); if (toVertex == null) { notFound.add("to"); } if (notFound.size() > 0) { throw new VertexNotFoundException(notFound); } Vertex origin = null; Vertex target = null; if (options.isArriveBy()) { origin = toVertex; target = fromVertex; } else { origin = fromVertex; target = toVertex; } State state = new State((int) (targetTime.getTime() / 1000), origin, options); return plan(state, target, nItineraries); } @Override public List<GraphPath> plan(State origin, Vertex target, int nItineraries) { TraverseOptions options = origin.getOptions(); if (_graphService.getCalendarService() != null) options.setCalendarService(_graphService.getCalendarService()); options.setTransferTable(_graphService.getGraph().getTransferTable()); options.setServiceDays(origin.getTime(), _graphService.getGraph().getAgencyIds()); if (options.getModes().getTransit() && !_graphService.getGraph().transitFeedCovers(new Date(origin.getTime() * 1000))) { // user wants a path through the transit network, // but the date provided is outside those covered by the transit feed. throw new TransitTimesException(); } // always use the bidirectional heuristic because the others are not precise enough RemainingWeightHeuristic heuristic = new BidirectionalRemainingWeightHeuristic(_graphService.getGraph()); // the states that will eventually be turned into paths and returned List<State> returnStates = new LinkedList<State>(); // Populate any extra edges final ExtraEdgesStrategy extraEdgesStrategy = options.extraEdgesStrategy; OverlayGraph extraEdges = new OverlayGraph(); extraEdgesStrategy.addEdgesFor(extraEdges, origin.getVertex()); extraEdgesStrategy.addEdgesFor(extraEdges, target); BinHeap<State> pq = new BinHeap<State>(); // List<State> boundingStates = new ArrayList<State>(); // initialize heuristic outside loop so table can be reused heuristic.computeInitialWeight(origin, target); // increase maxWalk repeatedly in case hard limiting is in use WALK: for (double maxWalk = options.getMaxWalkDistance(); maxWalk < 100000 && returnStates.isEmpty(); maxWalk *= 2) { LOG.debug("try search with max walk {}", maxWalk); // increase maxWalk if settings make trip impossible if (maxWalk < Math.min( origin.getVertex().distance(target), origin.getVertex().getDistanceToNearestTransitStop() + target.getDistanceToNearestTransitStop())) continue WALK; options.setMaxWalkDistance(maxWalk); // reinitialize states for each retry HashMap<Vertex, List<State>> states = new HashMap<Vertex, List<State>>(); pq.reset(); pq.insert(origin, 0); long startTime = System.currentTimeMillis(); long endTime = startTime + (int) (_timeouts[0] * 1000); LOG.debug("starttime {} endtime {}", startTime, endTime); QUEUE: while (!pq.empty()) { if (System.currentTimeMillis() > endTime) { LOG.debug("timeout at {} msec", System.currentTimeMillis() - startTime); if (returnStates.isEmpty()) continue WALK; else { storeMemory(); break WALK; } } State su = pq.extract_min(); // for (State bs : boundingStates) { // if (eDominates(bs, su)) { // continue QUEUE; // } // } Vertex u = su.getVertex(); if (traverseVisitor != null) { traverseVisitor.visitVertex(su); } if (u.equals(target)) { // boundingStates.add(su); returnStates.add(su); if (!options.getModes().getTransit()) break QUEUE; // options should contain max itineraries if (returnStates.size() >= _maxPaths) break QUEUE; if (returnStates.size() < _timeouts.length) { endTime = startTime + (int) (_timeouts[returnStates.size()] * 1000); LOG.debug( "{} path, set timeout to {}", returnStates.size(), _timeouts[returnStates.size()] * 1000); } continue QUEUE; } for (Edge e : u.getEdges(extraEdges, null, options.isArriveBy())) { STATE: for (State new_sv = e.traverse(su); new_sv != null; new_sv = new_sv.getNextResult()) { if (traverseVisitor != null) { traverseVisitor.visitEdge(e, new_sv); } double h = heuristic.computeForwardWeight(new_sv, target); // for (State bs : boundingStates) { // if (eDominates(bs, new_sv)) { // continue STATE; // } // } Vertex v = new_sv.getVertex(); List<State> old_states = states.get(v); if (old_states == null) { old_states = new LinkedList<State>(); states.put(v, old_states); } else { for (State old_sv : old_states) { if (eDominates(old_sv, new_sv)) { continue STATE; } } Iterator<State> iter = old_states.iterator(); while (iter.hasNext()) { State old_sv = iter.next(); if (eDominates(new_sv, old_sv)) { iter.remove(); } } } if (traverseVisitor != null) traverseVisitor.visitEnqueue(new_sv); old_states.add(new_sv); pq.insert(new_sv, new_sv.getWeight() + h); } } } } storeMemory(); // Make the states into paths and return them List<GraphPath> paths = new LinkedList<GraphPath>(); for (State s : returnStates) { LOG.debug(s.toStringVerbose()); paths.add(new GraphPath(s, true)); } // sort by arrival time, though paths are already in order of increasing difficulty // Collections.sort(paths, new PathComparator(origin.getOptions().isArriveBy())); return paths; } private void storeMemory() { if (store.isMonitoring("memoryUsed")) { System.gc(); long memoryUsed = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); store.setLongMax("memoryUsed", memoryUsed); } } // private boolean eDominates(State s0, State s1) { // final double EPSILON = 0.05; // return s0.getWeight() <= s1.getWeight() * (1 + EPSILON) && // s0.getTime() <= s1.getTime() * (1 + EPSILON) && // s0.getWalkDistance() <= s1.getWalkDistance() * (1 + EPSILON) && // s0.getNumBoardings() <= s1.getNumBoardings(); // } private boolean eDominates(State s0, State s1) { final double EPSILON = 0.05; if (s0.similarTripSeq(s1)) { return s0.getWeight() <= s1.getWeight() * (1 + EPSILON) && s0.getTime() <= s1.getTime() * (1 + EPSILON) && s0.getWalkDistance() <= s1.getWalkDistance() * (1 + EPSILON) && s0.getNumBoardings() <= s1.getNumBoardings(); } else { return false; } } // private boolean eDominates(State s0, State s1) { // final double EPSILON1 = 0.1; // if (s0.similarTripSeq(s1)) { // return s0.getWeight() <= s1.getWeight() * (1 + EPSILON1) && // s0.getElapsedTime() <= s1.getElapsedTime() * (1 + EPSILON1) && // s0.getWalkDistance() <= s1.getWalkDistance() * (1 + EPSILON1) && // s0.getNumBoardings() <= s1.getNumBoardings(); // } else if (s0.getTripId() != null && s0.getTripId() == s1.getTripId()) { // return s0.getNumBoardings() <= s1.getNumBoardings() && // s0.getWeight() <= s1.getWeight() * (1 + EPSILON2) && // s0.getElapsedTime() <= s1.getElapsedTime() * (1 + EPSILON2) && // s0.getWalkDistance() <= s1.getWalkDistance() * (1 + EPSILON2); // } else { // return false; // } // } // private boolean eDominates(State s0, State s1) { // if (s0.similarTripSeq(s1)) { // return s0.getWeight() <= s1.getWeight(); // } else if (s0.getTrip() == s1.getTrip()) { // if (s0.getNumBoardings() < s1.getNumBoardings()) // return true; // return s0.getWeight() <= s1.getWeight(); // } else { // return false; // } // } // private boolean eDominates(State s0, State s1) { // final double EPSILON = 0.1; // return s0.getWeight() <= s1.getWeight() * (1 + EPSILON) && // s0.getTime() <= s1.getTime() * (1 + EPSILON) && // s0.getNumBoardings() <= s1.getNumBoardings(); // } @Override public List<GraphPath> plan( NamedPlace fromPlace, NamedPlace toPlace, List<NamedPlace> intermediates, boolean ordered, Date targetTime, TraverseOptions options) { return null; } }
// @Component public class MultiObjectivePathServiceImpl implements PathService { @Autowired public GraphService graphService; private static final Logger LOG = LoggerFactory.getLogger(MultiObjectivePathServiceImpl.class); private static final MonitoringStore store = MonitoringStoreFactory.getStore(); private static final double MAX_WALK = 100000; private double[] _timeouts = new double[] {4, 2, 0.6, 0.4}; // seconds private double _maxPaths = 4; private TraverseVisitor traverseVisitor; private DistanceLibrary distanceLibrary = SphericalDistanceLibrary.getInstance(); /** Give up on searching for itineraries after this many seconds have elapsed. */ public void setTimeouts(List<Double> timeouts) { _timeouts = new double[timeouts.size()]; int i = 0; for (Double d : timeouts) _timeouts[i++] = d; } public void setMaxPaths(double numPaths) { _maxPaths = numPaths; } public void setTraverseVisitor(TraverseVisitor traverseVisitor) { this.traverseVisitor = traverseVisitor; } @Override public List<GraphPath> getPaths(RoutingRequest options) { if (options.rctx == null) { options.setRoutingContext(graphService.getGraph(options.getRouterId())); // move into setRoutingContext ? options.rctx.pathParsers = new PathParser[] {new BasicPathParser(), new NoThruTrafficPathParser()}; } RemainingWeightHeuristic heuristic; if (options.getModes().isTransit()) { LOG.debug("Transit itinerary requested."); // always use the bidirectional heuristic because the others are not precise enough heuristic = new BidirectionalRemainingWeightHeuristic(options.rctx.graph); } else { LOG.debug("Non-transit itinerary requested."); heuristic = new DefaultRemainingWeightHeuristic(); } // the states that will eventually be turned into paths and returned List<State> returnStates = new LinkedList<State>(); BinHeap<State> pq = new BinHeap<State>(); // List<State> boundingStates = new ArrayList<State>(); Vertex originVertex = options.rctx.origin; Vertex targetVertex = options.rctx.target; // increase maxWalk repeatedly in case hard limiting is in use WALK: for (double maxWalk = options.getMaxWalkDistance(); returnStates.isEmpty(); maxWalk *= 2) { if (maxWalk != Double.MAX_VALUE && maxWalk > MAX_WALK) { break; } LOG.debug("try search with max walk {}", maxWalk); // increase maxWalk if settings make trip impossible if (maxWalk < Math.min( distanceLibrary.distance(originVertex.getCoordinate(), targetVertex.getCoordinate()), originVertex.getDistanceToNearestTransitStop() + targetVertex.getDistanceToNearestTransitStop())) continue WALK; options.setMaxWalkDistance(maxWalk); // cap search / heuristic weight final double AVG_TRANSIT_SPEED = 25; // m/sec double cutoff = (distanceLibrary.distance(originVertex.getCoordinate(), targetVertex.getCoordinate()) * 1.5) / AVG_TRANSIT_SPEED; // wait time is irrelevant in the heuristic cutoff += options.getMaxWalkDistance() * options.walkReluctance; options.maxWeight = cutoff; State origin = new State(options); // (used to) initialize heuristic outside loop so table can be reused heuristic.computeInitialWeight(origin, targetVertex); options.maxWeight = cutoff + 30 * 60 * options.waitReluctance; // reinitialize states for each retry HashMap<Vertex, List<State>> states = new HashMap<Vertex, List<State>>(); pq.reset(); pq.insert(origin, 0); long startTime = System.currentTimeMillis(); long endTime = startTime + (int) (_timeouts[0] * 1000); LOG.debug("starttime {} endtime {}", startTime, endTime); QUEUE: while (!pq.empty()) { if (System.currentTimeMillis() > endTime) { LOG.debug("timeout at {} msec", System.currentTimeMillis() - startTime); if (returnStates.isEmpty()) break WALK; // disable walk distance increases else { storeMemory(); break WALK; } } // if (pq.peek_min_key() > options.maxWeight) { // LOG.debug("max weight {} exceeded", options.maxWeight); // break QUEUE; // } State su = pq.extract_min(); // for (State bs : boundingStates) { // if (eDominates(bs, su)) { // continue QUEUE; // } // } Vertex u = su.getVertex(); if (traverseVisitor != null) { traverseVisitor.visitVertex(su); } if (u.equals(targetVertex)) { // boundingStates.add(su); returnStates.add(su); if (!options.getModes().isTransit()) break QUEUE; // options should contain max itineraries if (returnStates.size() >= _maxPaths) break QUEUE; if (returnStates.size() < _timeouts.length) { endTime = startTime + (int) (_timeouts[returnStates.size()] * 1000); LOG.debug( "{} path, set timeout to {}", returnStates.size(), _timeouts[returnStates.size()] * 1000); } continue QUEUE; } for (Edge e : options.isArriveBy() ? u.getIncoming() : u.getOutgoing()) { STATE: for (State new_sv = e.traverse(su); new_sv != null; new_sv = new_sv.getNextResult()) { if (traverseVisitor != null) { traverseVisitor.visitEdge(e, new_sv); } double h = heuristic.computeForwardWeight(new_sv, targetVertex); if (h == Double.MAX_VALUE) continue; // for (State bs : boundingStates) { // if (eDominates(bs, new_sv)) { // continue STATE; // } // } Vertex v = new_sv.getVertex(); List<State> old_states = states.get(v); if (old_states == null) { old_states = new LinkedList<State>(); states.put(v, old_states); } else { for (State old_sv : old_states) { if (eDominates(old_sv, new_sv)) { continue STATE; } } Iterator<State> iter = old_states.iterator(); while (iter.hasNext()) { State old_sv = iter.next(); if (eDominates(new_sv, old_sv)) { iter.remove(); } } } if (traverseVisitor != null) traverseVisitor.visitEnqueue(new_sv); old_states.add(new_sv); pq.insert(new_sv, new_sv.getWeight() + h); } } } } storeMemory(); // Make the states into paths and return them List<GraphPath> paths = new LinkedList<GraphPath>(); for (State s : returnStates) { LOG.debug(s.toStringVerbose()); paths.add(new GraphPath(s, true)); } // sort by arrival time, though paths are already in order of increasing difficulty // Collections.sort(paths, new PathComparator(origin.getOptions().isArriveBy())); return paths; } private void storeMemory() { if (store.isMonitoring("memoryUsed")) { System.gc(); long memoryUsed = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); store.setLongMax("memoryUsed", memoryUsed); } } // private boolean eDominates(State s0, State s1) { // final double EPSILON = 0.05; // return s0.getWeight() <= s1.getWeight() * (1 + EPSILON) && // s0.getTime() <= s1.getTime() * (1 + EPSILON) && // s0.getWalkDistance() <= s1.getWalkDistance() * (1 + EPSILON) && // s0.getNumBoardings() <= s1.getNumBoardings(); // } // TODO: move into an epsilon-dominance shortest path tree private boolean eDominates(State s0, State s1) { final double EPSILON = 0.05; if (s0.similarRouteSequence(s1)) { return s0.getWeight() <= s1.getWeight() * (1 + EPSILON) && s0.getElapsedTime() <= s1.getElapsedTime() * (1 + EPSILON) && s0.getWalkDistance() <= s1.getWalkDistance() * (1 + EPSILON) && s0.getNumBoardings() <= s1.getNumBoardings() && (s0.getWeight() < s1.getWeight() || s0.getElapsedTime() < s1.getElapsedTime() || s0.getWalkDistance() < s1.getWalkDistance() || s0.getNumBoardings() < s1.getNumBoardings()); } else { return false; } } // private boolean eDominates(State s0, State s1) { // final double EPSILON1 = 0.1; // if (s0.similarTripSeq(s1)) { // return s0.getWeight() <= s1.getWeight() * (1 + EPSILON1) && // s0.getElapsedTime() <= s1.getElapsedTime() * (1 + EPSILON1) && // s0.getWalkDistance() <= s1.getWalkDistance() * (1 + EPSILON1) && // s0.getNumBoardings() <= s1.getNumBoardings(); // } else if (s0.getTripId() != null && s0.getTripId() == s1.getTripId()) { // return s0.getNumBoardings() <= s1.getNumBoardings() && // s0.getWeight() <= s1.getWeight() * (1 + EPSILON2) && // s0.getElapsedTime() <= s1.getElapsedTime() * (1 + EPSILON2) && // s0.getWalkDistance() <= s1.getWalkDistance() * (1 + EPSILON2); // } else { // return false; // } // } // private boolean eDominates(State s0, State s1) { // if (s0.similarTripSeq(s1)) { // return s0.getWeight() <= s1.getWeight(); // } else if (s0.getTrip() == s1.getTrip()) { // if (s0.getNumBoardings() < s1.getNumBoardings()) // return true; // return s0.getWeight() <= s1.getWeight(); // } else { // return false; // } // } // private boolean eDominates(State s0, State s1) { // final double EPSILON = 0.1; // return s0.getWeight() <= s1.getWeight() * (1 + EPSILON) && // s0.getTime() <= s1.getTime() * (1 + EPSILON) && // s0.getNumBoardings() <= s1.getNumBoardings(); // } }