diff --git a/src/main/java/org/opentripplanner/framework/lang/ArrayUtils.java b/src/main/java/org/opentripplanner/framework/lang/ArrayUtils.java new file mode 100644 index 00000000000..84dfd3e224b --- /dev/null +++ b/src/main/java/org/opentripplanner/framework/lang/ArrayUtils.java @@ -0,0 +1,34 @@ +package org.opentripplanner.framework.lang; + +import java.util.function.Function; + +public class ArrayUtils { + + private ArrayUtils() {} + + /** + * Check if all values, mapped using {@param mapper}, of an array are equal. + */ + public static boolean allValuesEquals(T[] arr, Function mapper) { + if (arr.length < 2) { + return true; + } + + Object reference = mapper.apply(arr[0]); + + for (int i = 1; i < arr.length; i++) { + if (!reference.equals(mapper.apply(arr[i]))) { + return false; + } + } + + return true; + } + + /** + * Check if all values of an array are equal. + */ + public static boolean allValuesEquals(Object[] arr) { + return allValuesEquals(arr, Function.identity()); + } +} diff --git a/src/main/java/org/opentripplanner/raptor/api/request/RaptorProfile.java b/src/main/java/org/opentripplanner/raptor/api/request/RaptorProfile.java index 41a17d2fc31..b9d5b56d802 100644 --- a/src/main/java/org/opentripplanner/raptor/api/request/RaptorProfile.java +++ b/src/main/java/org/opentripplanner/raptor/api/request/RaptorProfile.java @@ -35,7 +35,12 @@ public enum RaptorProfile { /** * Same as {@link #MIN_TRAVEL_DURATION}, but no paths are computed/returned. */ - MIN_TRAVEL_DURATION_BEST_TIME("MinTravelDurationBT"); + MIN_TRAVEL_DURATION_BEST_TIME("MinTravelDurationBT"), + + /** + * Same as {@link #MIN_TRAVEL_DURATION_BEST_TIME}, but also min cost is calculated. + */ + MIN_TRAVEL_DURATION_AND_COST_BEST_TIME("MinTravelDurationAndCostBT"); private final String abbreviation; diff --git a/src/main/java/org/opentripplanner/raptor/heuristic/HeuristicRoutingStrategy.java b/src/main/java/org/opentripplanner/raptor/heuristic/HeuristicRoutingStrategy.java new file mode 100644 index 00000000000..a6d1d418d7e --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/heuristic/HeuristicRoutingStrategy.java @@ -0,0 +1,122 @@ +package org.opentripplanner.raptor.heuristic; + +import org.opentripplanner.raptor.rangeraptor.internalapi.RoundProvider; +import org.opentripplanner.raptor.rangeraptor.internalapi.RoutingStrategy; +import org.opentripplanner.raptor.rangeraptor.standard.StdWorkerState; +import org.opentripplanner.raptor.rangeraptor.transit.TransitCalculator; +import org.opentripplanner.raptor.spi.CostCalculator; +import org.opentripplanner.raptor.spi.RaptorAccessEgress; +import org.opentripplanner.raptor.spi.RaptorConstrainedBoardingSearch; +import org.opentripplanner.raptor.spi.RaptorTimeTable; +import org.opentripplanner.raptor.spi.RaptorTransferConstraint; +import org.opentripplanner.raptor.spi.RaptorTripSchedule; + +/** + * Routing strategy for heuristic searches. Does not operate on actual times, but only durations. + * Does not do any trip searches, but operates on a single heuristic trip. + */ +public class HeuristicRoutingStrategy implements RoutingStrategy { + + public static final int UNREACHED = 999_999_999; + + private final TimeAndCostHeuristicState state; + private final CostCalculator costCalculator; + private final TransitCalculator calculator; + private final RoundProvider roundProvider; + private T currentTrip; + private int currentBoardingTime; + private int currentBoardCost; + private int currentBoardingTotalDuration; + + public HeuristicRoutingStrategy( + StdWorkerState state, + RoundProvider roundProvider, + TransitCalculator calculator, + CostCalculator costCalculator + ) { + this.state = (TimeAndCostHeuristicState) state; + this.costCalculator = costCalculator; + this.calculator = calculator; + this.roundProvider = roundProvider; + } + + @Override + public void setAccessToStop(RaptorAccessEgress accessPath, int iterationDepartureTime) { + state.setAccessToStop(accessPath, iterationDepartureTime); + } + + @Override + public void prepareForTransitWith(RaptorTimeTable timetable) { + this.currentTrip = (T) timetable.getHeuristicTrip(); + this.currentBoardingTime = UNREACHED; + this.currentBoardingTotalDuration = UNREACHED; + this.currentBoardCost = UNREACHED; + } + + @Override + public void boardWithRegularTransfer(int stopIndex, int stopPos, int boardSlack) { + int arrivalDuration = state.bestTimePreviousRound(stopIndex); + int boardingDuration = arrivalDuration + boardSlack; + int boardingCost = costCalculator.boardingCost( + roundProvider.round() < 2, + arrivalDuration, + stopIndex, + boardingDuration, + currentTrip, + RaptorTransferConstraint.REGULAR_TRANSFER + ); + + board(stopIndex, stopPos, boardingDuration, boardingCost); + } + + @Override + public void boardWithConstrainedTransfer( + int stopIndex, + int stopPos, + int boardSlack, + RaptorConstrainedBoardingSearch txSearch + ) { + // Assume 0 boarding slack and cost as it is the lower bound for constrained transfers + board(stopIndex, stopPos, state.bestTimePreviousRound(stopIndex), 0); + } + + private void board(int stopIndex, int stopPos, int boardingDuration, int boardCost) { + int boardingTime = calculator.stopDepartureTime(currentTrip, stopPos); + + if ( + currentBoardingTotalDuration == UNREACHED || + (currentBoardingTotalDuration - boardingDuration) > + calculator.duration(boardingTime, currentBoardingTime) + ) { + currentBoardingTime = boardingTime; + currentBoardingTotalDuration = boardingDuration; + + // TODO, we should add cost + time to an array, so we can check both what is the minimum time + // and cost separately at arrival, now we optimize for minimum time + currentBoardCost = state.bestCostPreviousRound(stopIndex) + boardCost; + } + } + + @Override + public void alight(int stopIndex, int stopPos, int alightSlack) { + if (currentBoardingTime != UNREACHED) { + int transitDuration = calculator.duration( + currentBoardingTime, + calculator.stopArrivalTime(currentTrip, stopPos) + ); + int transitCost = costCalculator.transitArrivalCost( + currentBoardCost, + alightSlack, + transitDuration, + currentTrip, + stopIndex + ); + state.updateNewBestTimeCostAndRound( + stopIndex, + currentBoardingTotalDuration + alightSlack + transitDuration, + currentBoardCost + transitCost, + true + ); + } + } +} diff --git a/src/main/java/org/opentripplanner/raptor/heuristic/TimeAndCostHeuristicState.java b/src/main/java/org/opentripplanner/raptor/heuristic/TimeAndCostHeuristicState.java new file mode 100644 index 00000000000..65b93e7a5dd --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/heuristic/TimeAndCostHeuristicState.java @@ -0,0 +1,243 @@ +package org.opentripplanner.raptor.heuristic; + +import static org.opentripplanner.framework.lang.IntUtils.intArray; + +import java.util.BitSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import org.opentripplanner.raptor.api.path.Path; +import org.opentripplanner.raptor.api.response.StopArrivals; +import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; +import org.opentripplanner.raptor.rangeraptor.internalapi.RoundProvider; +import org.opentripplanner.raptor.rangeraptor.internalapi.WorkerLifeCycle; +import org.opentripplanner.raptor.rangeraptor.standard.StdWorkerState; +import org.opentripplanner.raptor.spi.IntIterator; +import org.opentripplanner.raptor.spi.RaptorAccessEgress; +import org.opentripplanner.raptor.spi.RaptorTransfer; +import org.opentripplanner.raptor.spi.RaptorTripSchedule; +import org.opentripplanner.raptor.spi.TransitArrival; +import org.opentripplanner.raptor.util.BitSetIterator; + +/** + * The state for a {@link HeuristicRoutingStrategy}, which considers both time and cost criteria. + */ +public class TimeAndCostHeuristicState implements StdWorkerState { + + public static final int UNREACHED = 999_999_999; + private final int[] egressStops; + + private final int[] bestNumOfTransfers; + + /** The best times to reach a stop, across rounds and iterations. */ + private final int[] times; + + /** + * The best "on-board" arrival times to reach a stop, across rounds and iterations. It includes + * both transit arrivals and access-on-board arrivals. + */ + private final int[] transitArrivalTimes; + + private final int[] costs; + + private final int[] transitArrivalCosts; + + private final BitSet reachedByTransitCurrentRound; + + private final RoundProvider roundProvider; + + /** Stops touched in the CURRENT round. */ + private BitSet reachedCurrentRound; + /** Stops touched by in LAST round. */ + private BitSet reachedLastRound; + + public TimeAndCostHeuristicState( + int[] egressStops, + int nStops, + RoundProvider roundProvider, + WorkerLifeCycle lifeCycle + ) { + this.egressStops = egressStops; + this.bestNumOfTransfers = intArray(nStops, UNREACHED); + this.times = intArray(nStops, UNREACHED); + this.transitArrivalTimes = intArray(nStops, UNREACHED); + this.costs = intArray(nStops, UNREACHED); + this.transitArrivalCosts = intArray(nStops, UNREACHED); + this.reachedByTransitCurrentRound = new BitSet(nStops); + this.reachedCurrentRound = new BitSet(nStops); + this.reachedLastRound = new BitSet(nStops); + this.roundProvider = roundProvider; + + // Attach to Worker life cycle + lifeCycle.onSetupIteration(ignore -> setupIteration()); + lifeCycle.onPrepareForNextRound(round -> prepareForNextRound()); + } + + @Override + public boolean isNewRoundAvailable() { + return !reachedCurrentRound.isEmpty(); + } + + @Override + public boolean isDestinationReachedInCurrentRound() { + // This is fast enough, we could use a BitSet for egressStops, but it takes up more + // memory and the performance is the same. + for (final int egressStop : egressStops) { + if (reachedByTransitCurrentRound.get(egressStop)) { + return true; + } + } + return false; + } + + @Override + public IntIterator stopsTouchedPreviousRound() { + return new BitSetIterator(reachedLastRound); + } + + @Override + public IntIterator stopsTouchedByTransitCurrentRound() { + return new BitSetIterator(reachedByTransitCurrentRound); + } + + @Override + public boolean isStopReachedInPreviousRound(int stopIndex) { + return reachedLastRound.get(stopIndex); + } + + public int bestTimePreviousRound(int stopIndex) { + return times[stopIndex]; + } + + @Override + public void transitToStop(int alightStop, int alightTime, int boardStop, int boardTime, T trip) { + throw new IllegalStateException("Not implemented"); + } + + @Override + public TransitArrival previousTransit(int boardStopIndex) { + throw new IllegalStateException("Not implemented"); + } + + public int bestCostPreviousRound(int stopIndex) { + return costs[stopIndex]; + } + + @Override + public void setAccessToStop(RaptorAccessEgress accessPath, int iterationDepartureTime) { + updateNewBestTimeCostAndRound( + accessPath.stop(), + accessPath.durationInSeconds(), + accessPath.generalizedCost(), + accessPath.stopReachedOnBoard() + ); + } + + @Override + public void transferToStops(int fromStop, Iterator transfers) { + final int prevDuration = bestTimePreviousRound(fromStop); + final int prevCost = bestCostPreviousRound(fromStop); + + while (transfers.hasNext()) { + RaptorTransfer it = transfers.next(); + updateNewBestTimeCostAndRound( + it.stop(), + it.durationInSeconds() + prevDuration, + it.generalizedCost() + prevCost, + false + ); + } + } + + void updateNewBestTimeCostAndRound(int stop, int time, int cost, boolean isTransit) { + if (!isTransit || updateBestTransitArrivalTime(stop, time)) { + updateBestTime(stop, time); + } + if (!isTransit || updateBestTransitArrivalCost(stop, cost)) { + updateBestCost(stop, cost); + } + updateBestRound(stop); + } + + @Override + public Collection> extractPaths() { + return List.of(); + } + + @Override + public StopArrivals extractStopArrivals() { + return null; + } + + public Heuristics heuristics() { + return new TimeAndCostHeuristicsAdapter(bestNumOfTransfers, times, costs, egressStops); + } + + /** + * Clear all reached flags before we start a new iteration. This is important so stops visited in + * the previous iteration in the last round does not "overflow" into the next iteration. + */ + private void setupIteration() { + // clear all touched stops to avoid constant reƫxploration + reachedCurrentRound.clear(); + reachedByTransitCurrentRound.clear(); + } + + private void swapReachedCurrentAndLastRound() { + BitSet tmp = reachedLastRound; + reachedLastRound = reachedCurrentRound; + reachedCurrentRound = tmp; + } + + private boolean updateBestTime(int stop, int time) { + if (time < times[stop]) { + times[stop] = time; + reachedCurrentRound.set(stop); + return true; + } + return false; + } + + private boolean updateBestTransitArrivalTime(int stop, int time) { + if (time < transitArrivalTimes[stop]) { + transitArrivalTimes[stop] = time; + reachedByTransitCurrentRound.set(stop); + return true; + } + return false; + } + + private boolean updateBestCost(int stop, int cost) { + if (cost < costs[stop]) { + costs[stop] = cost; + reachedCurrentRound.set(stop); + return true; + } + return false; + } + + private boolean updateBestTransitArrivalCost(int stop, int cost) { + if (cost < transitArrivalCosts[stop]) { + transitArrivalCosts[stop] = cost; + reachedByTransitCurrentRound.set(stop); + return true; + } + return false; + } + + void updateBestRound(int stop) { + final int numOfTransfers = roundProvider.round() - 1; + if (numOfTransfers < bestNumOfTransfers[stop]) { + bestNumOfTransfers[stop] = numOfTransfers; + } + } + + /** + * Prepare this class for the next round updating reached flags. + */ + private void prepareForNextRound() { + swapReachedCurrentAndLastRound(); + reachedCurrentRound.clear(); + reachedByTransitCurrentRound.clear(); + } +} diff --git a/src/main/java/org/opentripplanner/raptor/heuristic/TimeAndCostHeuristicsAdapter.java b/src/main/java/org/opentripplanner/raptor/heuristic/TimeAndCostHeuristicsAdapter.java new file mode 100644 index 00000000000..c6b987a6725 --- /dev/null +++ b/src/main/java/org/opentripplanner/raptor/heuristic/TimeAndCostHeuristicsAdapter.java @@ -0,0 +1,122 @@ +package org.opentripplanner.raptor.heuristic; + +import java.util.function.IntUnaryOperator; +import org.opentripplanner.framework.lang.IntUtils; +import org.opentripplanner.raptor.rangeraptor.internalapi.HeuristicAtStop; +import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; + +/** + * Heuristics adapter for a {@link TimeAndCostHeuristicState} + */ +public class TimeAndCostHeuristicsAdapter implements Heuristics { + + private final int[] bestNumOfTransfers; + private final int[] times; + private final int[] costs; + private final int[] egressStops; + + public TimeAndCostHeuristicsAdapter( + int[] bestNumOfTransfers, + int[] times, + int[] costs, + int[] egressStops + ) { + this.bestNumOfTransfers = bestNumOfTransfers; + this.times = times; + this.costs = costs; + this.egressStops = egressStops; + } + + @Override + public int[] bestTravelDurationToIntArray(int unreached) { + return toIntArray(size(), unreached, this::bestTravelDuration); + } + + @Override + public int[] bestNumOfTransfersToIntArray(int unreached) { + return toIntArray(size(), unreached, this::bestNumOfTransfers); + } + + @Override + public int[] bestGeneralizedCostToIntArray(int unreached) { + return toIntArray(size(), unreached, this::bestGeneralizedCost); + } + + @Override + public HeuristicAtStop createHeuristicAtStop(int stop) { + return reached(stop) + ? new HeuristicAtStop(times[stop], bestNumOfTransfers[stop], costs[stop]) + : HeuristicAtStop.UNREACHED; + } + + @Override + public int size() { + return bestNumOfTransfers.length; + } + + @Override + public int bestOverallJourneyTravelDuration() { + int best = HeuristicRoutingStrategy.UNREACHED; + for (int stop : egressStops) { + if (times[stop] < best) { + best = times[stop]; + } + } + return best; + } + + @Override + public int bestOverallJourneyNumOfTransfers() { + int best = HeuristicRoutingStrategy.UNREACHED; + for (int stop : egressStops) { + if (bestNumOfTransfers[stop] < best) { + best = bestNumOfTransfers[stop]; + } + } + return best; + } + + @Override + public int minWaitTimeForJourneysReachingDestination() { + return 0; + } + + @Override + public boolean destinationReached() { + for (int stop : egressStops) { + if (reached(stop)) { + return true; + } + } + return false; + } + + private boolean reached(int stop) { + return bestNumOfTransfers[stop] < HeuristicRoutingStrategy.UNREACHED; + } + + private int bestTravelDuration(int stop) { + return times[stop]; + } + + private int bestNumOfTransfers(int stop) { + return bestNumOfTransfers[stop]; + } + + private int bestGeneralizedCost(int stop) { + return costs[stop]; + } + + /** + * Convert one of heuristics to an int array. + */ + private int[] toIntArray(int size, int unreached, IntUnaryOperator supplier) { + int[] a = IntUtils.intArray(size, unreached); + for (int i = 0; i < a.length; i++) { + if (reached(i)) { + a[i] = supplier.applyAsInt(i); + } + } + return a; + } +} diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/configure/StdRangeRaptorConfig.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/configure/StdRangeRaptorConfig.java index 222601271bc..05e8da7b595 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/configure/StdRangeRaptorConfig.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/configure/StdRangeRaptorConfig.java @@ -1,6 +1,8 @@ package org.opentripplanner.raptor.rangeraptor.standard.configure; import java.util.function.BiFunction; +import org.opentripplanner.raptor.heuristic.HeuristicRoutingStrategy; +import org.opentripplanner.raptor.heuristic.TimeAndCostHeuristicState; import org.opentripplanner.raptor.rangeraptor.context.SearchContext; import org.opentripplanner.raptor.rangeraptor.internalapi.HeuristicSearch; import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; @@ -60,10 +62,15 @@ public HeuristicSearch createHeuristicSearch( BiFunction, RoutingStrategy, Worker> createWorker, CostCalculator costCalculator ) { - StdRangeRaptorWorkerState state = createState(); - Heuristics heuristics = createHeuristicsAdapter(costCalculator); + StdWorkerState state = createState(); + Heuristics heuristics; + if (state instanceof TimeAndCostHeuristicState heuristicState) { + heuristics = heuristicState.heuristics(); + } else { + heuristics = createHeuristicsAdapter(costCalculator); + } return new HeuristicSearch<>( - createWorker.apply(state, createWorkerStrategy(state)), + createWorker.apply(state, createWorkerStrategy(state, costCalculator)), heuristics ); } @@ -71,26 +78,26 @@ public HeuristicSearch createHeuristicSearch( public Worker createSearch( BiFunction, RoutingStrategy, Worker> createWorker ) { - StdRangeRaptorWorkerState state = createState(); - return createWorker.apply(state, createWorkerStrategy(state)); + StdWorkerState state = createState(); + return createWorker.apply(state, createWorkerStrategy(state, ctx.costCalculator())); } /* private factory methods */ - private StdRangeRaptorWorkerState createState() { + private StdWorkerState createState() { new VerifyRequestIsValid(ctx).verify(); - switch (ctx.profile()) { - case STANDARD: - case MIN_TRAVEL_DURATION: - return workerState(stdStopArrivalsState()); - case BEST_TIME: - case MIN_TRAVEL_DURATION_BEST_TIME: - return workerState(bestTimeStopArrivalsState()); - } - throw new IllegalArgumentException(ctx.profile().toString()); + return switch (ctx.profile()) { + case STANDARD, MIN_TRAVEL_DURATION -> workerState(stdStopArrivalsState()); + case BEST_TIME, MIN_TRAVEL_DURATION_BEST_TIME -> workerState(bestTimeStopArrivalsState()); + case MIN_TRAVEL_DURATION_AND_COST_BEST_TIME -> heuristicWorkerState(); + case MULTI_CRITERIA -> throw new IllegalArgumentException(ctx.profile().toString()); + }; } - private RoutingStrategy createWorkerStrategy(StdWorkerState state) { + private RoutingStrategy createWorkerStrategy( + StdWorkerState state, + CostCalculator mcCostCalculator + ) { switch (ctx.profile()) { case STANDARD: case BEST_TIME: @@ -106,6 +113,13 @@ private RoutingStrategy createWorkerStrategy(StdWorkerState state) { ctx.createTimeBasedBoardingSupport(), ctx.calculator() ); + case MIN_TRAVEL_DURATION_AND_COST_BEST_TIME: + return new HeuristicRoutingStrategy<>( + state, + ctx.roundProvider(), + ctx.calculator(), + mcCostCalculator + ); } throw new IllegalArgumentException(ctx.profile().toString()); } @@ -131,6 +145,15 @@ private StdRangeRaptorWorkerState workerState(StopArrivalsState stopArriva ); } + private TimeAndCostHeuristicState heuristicWorkerState() { + return new TimeAndCostHeuristicState<>( + ctx.egressStops(), + ctx.nStops(), + ctx.roundProvider(), + ctx.lifeCycle() + ); + } + private BestTimesOnlyStopArrivalsState bestTimeStopArrivalsState() { return new BestTimesOnlyStopArrivalsState<>(bestTimes(), simpleBestNumberOfTransfers()); } diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/configure/VerifyRequestIsValid.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/configure/VerifyRequestIsValid.java index f5197f02b3b..7af442862bc 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/configure/VerifyRequestIsValid.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/standard/configure/VerifyRequestIsValid.java @@ -40,7 +40,11 @@ private RaptorProfile profile() { private boolean minTravelDurationStrategy() { return profile() - .isOneOf(RaptorProfile.MIN_TRAVEL_DURATION, RaptorProfile.MIN_TRAVEL_DURATION_BEST_TIME); + .isOneOf( + RaptorProfile.MIN_TRAVEL_DURATION, + RaptorProfile.MIN_TRAVEL_DURATION_BEST_TIME, + RaptorProfile.MIN_TRAVEL_DURATION_AND_COST_BEST_TIME + ); } private boolean oneIteration() { diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/ForwardTransitCalculator.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/ForwardTransitCalculator.java index 5b757fcd7fb..ef8d7a5c695 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/ForwardTransitCalculator.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/ForwardTransitCalculator.java @@ -55,6 +55,16 @@ public int stopArrivalTime(T onTrip, int stopPositionInPattern, int alightSlack) return onTrip.arrival(stopPositionInPattern) + alightSlack; } + @Override + public int stopArrivalTime(T trip, int stopPositionInPattern) { + return trip.arrival(stopPositionInPattern); + } + + @Override + public int stopDepartureTime(T trip, int stopPositionInPattern) { + return trip.departure(stopPositionInPattern); + } + @Override public boolean exceedsTimeLimit(int time) { return isBefore(latestAcceptableArrivalTime, time); diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/ReverseTransitCalculator.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/ReverseTransitCalculator.java index 0a5fcbd33ec..88c331907b4 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/ReverseTransitCalculator.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/ReverseTransitCalculator.java @@ -60,6 +60,16 @@ public int stopArrivalTime(T onTrip, int stopPositionInPattern, int alightSlack) return plusDuration(onTrip.departure(stopPositionInPattern), alightSlack); } + @Override + public int stopArrivalTime(T trip, int stopPositionInPattern) { + return trip.departure(stopPositionInPattern); + } + + @Override + public int stopDepartureTime(T trip, int stopPositionInPattern) { + return trip.arrival(stopPositionInPattern); + } + @Override public boolean exceedsTimeLimit(int time) { return isBefore(earliestAcceptableDepartureTime, time); diff --git a/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/TransitCalculator.java b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/TransitCalculator.java index c3581fd0a10..1521f2c9279 100644 --- a/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/TransitCalculator.java +++ b/src/main/java/org/opentripplanner/raptor/rangeraptor/transit/TransitCalculator.java @@ -68,6 +68,26 @@ static TransitCalculator testDummyCalculator(b */ int stopArrivalTime(T trip, int stopPositionInPattern, int slack); + /** + * For a forward search return the trip arrival time at stop position. For a reverse search return + * the trip's departure time at stop position. + * added. + * + * @param trip the current boarded trip + * @param stopPositionInPattern the stop position/index + */ + int stopArrivalTime(T trip, int stopPositionInPattern); + + /** + * For a forward search return the trip departure time at stop position. For a reverse search + * return the trip's arrival time at stop position. + * added. + * + * @param trip the current boarded trip + * @param stopPositionInPattern the stop position/index + */ + int stopDepartureTime(T trip, int stopPositionInPattern); + /** * Stop the search when the time exceeds the latest-acceptable-arrival-time. In a reverse search * this is the earliest acceptable departure time. diff --git a/src/main/java/org/opentripplanner/raptor/service/HeuristicSearchTask.java b/src/main/java/org/opentripplanner/raptor/service/HeuristicSearchTask.java index 509b67ac215..99283902605 100644 --- a/src/main/java/org/opentripplanner/raptor/service/HeuristicSearchTask.java +++ b/src/main/java/org/opentripplanner/raptor/service/HeuristicSearchTask.java @@ -1,7 +1,6 @@ package org.opentripplanner.raptor.service; -import static org.opentripplanner.raptor.api.request.RaptorProfile.MIN_TRAVEL_DURATION; -import static org.opentripplanner.raptor.api.request.RaptorProfile.MIN_TRAVEL_DURATION_BEST_TIME; +import static org.opentripplanner.raptor.api.request.RaptorProfile.MIN_TRAVEL_DURATION_AND_COST_BEST_TIME; import javax.annotation.Nullable; import org.opentripplanner.framework.time.DurationUtils; @@ -123,19 +122,11 @@ void run() { private void createHeuristicSearchIfNotExist(RaptorRequest request) { if (search == null) { - var profile = MIN_TRAVEL_DURATION_BEST_TIME; - - if (request.searchParams().constrainedTransfersEnabled()) { - // We need to look up the previous transit arrival, this is not possible with the - // BEST_TIMES only states. - profile = MIN_TRAVEL_DURATION; - } - var builder = request .mutate() // Disable any optimization that is not valid for a heuristic search .clearOptimizations() - .profile(profile) + .profile(MIN_TRAVEL_DURATION_AND_COST_BEST_TIME) .searchDirection(direction); builder.searchParams().searchOneIterationOnly(); diff --git a/src/main/java/org/opentripplanner/raptor/spi/RaptorTimeTable.java b/src/main/java/org/opentripplanner/raptor/spi/RaptorTimeTable.java index 42379cc1e5c..9400ca46a10 100644 --- a/src/main/java/org/opentripplanner/raptor/spi/RaptorTimeTable.java +++ b/src/main/java/org/opentripplanner/raptor/spi/RaptorTimeTable.java @@ -28,4 +28,10 @@ public interface RaptorTimeTable { * Factory method to create the trip search */ RaptorTripScheduleSearch tripSearch(SearchDirection direction); + + /** + * Get a heuristic trip for this TimeTable, which represents the lower bound of travel and dwell + * times available in the pattern, but is unrelated to the actual schedule. + */ + RaptorTripSchedule getHeuristicTrip(); } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/api/DefaultTripPattern.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/api/DefaultTripPattern.java index fd73e7db162..0ab9a775e5d 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/api/DefaultTripPattern.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/api/DefaultTripPattern.java @@ -10,4 +10,6 @@ public interface DefaultTripPattern extends RaptorTripPattern { * give unpreferred routes or agencies a generalized-cost penalty. */ Route route(); + + int transitReluctanceFactorIndex(); } diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/HeuristicTrip.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/HeuristicTrip.java new file mode 100644 index 00000000000..65c1cc5cb2b --- /dev/null +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/HeuristicTrip.java @@ -0,0 +1,180 @@ +package org.opentripplanner.routing.algorithm.raptoradapter.transit; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.IntUnaryOperator; +import org.opentripplanner.framework.lang.IntUtils; +import org.opentripplanner.raptor.spi.RaptorTripPattern; +import org.opentripplanner.raptor.spi.RaptorTripSchedule; +import org.opentripplanner.routing.algorithm.raptoradapter.api.DefaultTripPattern; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.cost.DefaultTripSchedule; +import org.opentripplanner.transit.model.basic.Accessibility; +import org.opentripplanner.transit.model.framework.Deduplicator; +import org.opentripplanner.transit.model.timetable.FrequencyEntry; +import org.opentripplanner.transit.model.timetable.TripTimes; + +/** + * A heuristic trip contains the lowest bound of hop and dwell times for a set of timetables on a + * pattern. It is useful for computing the heuristic for the path pruning, as no schedule searches + * need to be done. + */ +public final class HeuristicTrip implements DefaultTripSchedule { + + static final Deduplicator DEDUPLICATOR = new Deduplicator(); + + private final int[] arrivalTimes; + private final int[] departureTimes; + private final DefaultTripPattern pattern; + + public static HeuristicTrip of(TripPatternForDate tripPatternForDate) { + HeuristicTrip trip = new HeuristicTrip( + tripPatternForDate.getTripPattern(), + updater -> { + for (TripTimes times : tripPatternForDate.tripTimes()) { + updater.accept(times::getArrivalTime, times::getDepartureTime); + } + for (FrequencyEntry frequencyEntry : tripPatternForDate.getFrequencies()) { + TripTimes times = frequencyEntry.tripTimes; + updater.accept(times::getArrivalTime, times::getDepartureTime); + } + } + ); + + return DEDUPLICATOR.deduplicateObject(HeuristicTrip.class, trip); + } + + public static HeuristicTrip of(TripPatternForDate[] tripPatternForDates) { + HeuristicTrip trip = new HeuristicTrip( + tripPatternForDates[0].getTripPattern(), + updater -> { + for (var tripPatternForDate : tripPatternForDates) { + HeuristicTrip heuristicTrip = tripPatternForDate.heuristicTrip(); + updater.accept(heuristicTrip::arrival, heuristicTrip::departure); + } + } + ); + + return DEDUPLICATOR.deduplicateObject(HeuristicTrip.class, trip); + } + + public static RaptorTripSchedule of(List trips) { + HeuristicTrip heuristicTrip = new HeuristicTrip( + (DefaultTripPattern) trips.get(0).pattern(), + updater -> { + for (var trip : trips) { + updater.accept(trip::arrival, trip::departure); + } + } + ); + + return DEDUPLICATOR.deduplicateObject(HeuristicTrip.class, heuristicTrip); + } + + private HeuristicTrip( + DefaultTripPattern pattern, + Consumer> consumer + ) { + this.pattern = pattern; + + var arrivalTimes = IntUtils.intArray(pattern.numberOfStopsInPattern() - 1, Integer.MAX_VALUE); + var departureTimes = IntUtils.intArray(pattern.numberOfStopsInPattern() - 1, Integer.MAX_VALUE); + + consumer.accept((IntUnaryOperator getArrivalTime, IntUnaryOperator getDepartureTime) -> + updateTimes(arrivalTimes, departureTimes, getArrivalTime, getDepartureTime) + ); + + this.arrivalTimes = DEDUPLICATOR.deduplicateIntArray(arrivalTimes); + this.departureTimes = DEDUPLICATOR.deduplicateIntArray(departureTimes); + } + + @Override + public int departure(int stopPosInPattern) { + if (stopPosInPattern == 0) { + return 0; + } + + return departureTimes[stopPosInPattern - 1]; + } + + @Override + public int arrival(int stopPosInPattern) { + if (stopPosInPattern == 0) { + return 0; + } + + return arrivalTimes[stopPosInPattern - 1]; + } + + @Override + public RaptorTripPattern pattern() { + return pattern; + } + + @Override + public int tripSortIndex() { + return 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HeuristicTrip that)) { + return false; + } + + return ( + pattern.equals(that.pattern) && + Arrays.equals(arrivalTimes, that.arrivalTimes) && + Arrays.equals(departureTimes, that.departureTimes) + ); + } + + @Override + public int hashCode() { + return Objects.hash(pattern, Arrays.hashCode(arrivalTimes), Arrays.hashCode(departureTimes)); + } + + @Override + public int transitReluctanceFactorIndex() { + return pattern.slackIndex(); + } + + @Override + public Accessibility wheelchairBoarding() { + return Accessibility.POSSIBLE; + } + + private static void updateTimes( + int[] arrivalTimes, + int[] departureTimes, + IntUnaryOperator getArrivalTime, + IntUnaryOperator getDepartureTime + ) { + int departureTime = getDepartureTime.applyAsInt(0); + int previous = 0; + for (int i = 1; i <= arrivalTimes.length; i++) { + int arrayIndex = i - 1; + + // Get arrival time and calculate hop time + int arrivalTime = getArrivalTime.applyAsInt(i); + int hopTime = arrivalTime - departureTime; + + // Update arrival time + arrivalTimes[arrayIndex] = Math.min(arrivalTimes[arrayIndex], previous + hopTime); + previous = arrivalTimes[arrayIndex]; + + // Get departure time and calculate dwell time + departureTime = getDepartureTime.applyAsInt(i); + int dwellTime = departureTime - arrivalTime; + + // Update departure time + departureTimes[arrayIndex] = Math.min(departureTimes[arrayIndex], previous + dwellTime); + previous = departureTimes[arrayIndex]; + } + } +} diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TripPatternForDate.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TripPatternForDate.java index 55278502b75..b4a62df54bf 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TripPatternForDate.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/TripPatternForDate.java @@ -51,6 +51,7 @@ public class TripPatternForDate implements Comparable { * The date on which the last trip arrives. */ private final LocalDate endOfRunningPeriod; + private final HeuristicTrip heuristicTrip; public TripPatternForDate( RoutingTripPattern tripPattern, @@ -62,6 +63,7 @@ public TripPatternForDate( this.tripTimes = tripTimes.toArray(new TripTimes[0]); this.frequencies = frequencies.toArray(new FrequencyEntry[0]); this.localDate = localDate; + this.heuristicTrip = HeuristicTrip.of(this); // TODO: We expect a pattern only containing trips or frequencies, fix ability to merge if (hasFrequencies()) { @@ -143,6 +145,10 @@ public boolean hasFrequencies() { return frequencies.length != 0; } + public HeuristicTrip heuristicTrip() { + return heuristicTrip; + } + @Override public int compareTo(TripPatternForDate other) { return localDate.compareTo(other.localDate); diff --git a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TripPatternForDates.java b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TripPatternForDates.java index 16d50f85342..f00da3517e1 100644 --- a/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TripPatternForDates.java +++ b/src/main/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/request/TripPatternForDates.java @@ -2,15 +2,18 @@ import java.util.BitSet; import java.util.function.IntUnaryOperator; +import org.opentripplanner.framework.lang.ArrayUtils; import org.opentripplanner.framework.tostring.ToStringBuilder; import org.opentripplanner.raptor.spi.IntIterator; import org.opentripplanner.raptor.spi.RaptorRoute; import org.opentripplanner.raptor.spi.RaptorTimeTable; import org.opentripplanner.raptor.spi.RaptorTripPattern; +import org.opentripplanner.raptor.spi.RaptorTripSchedule; import org.opentripplanner.raptor.spi.RaptorTripScheduleSearch; import org.opentripplanner.raptor.spi.SearchDirection; import org.opentripplanner.raptor.util.IntIterators; import org.opentripplanner.routing.algorithm.raptoradapter.api.DefaultTripPattern; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.HeuristicTrip; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripPatternForDate; import org.opentripplanner.routing.algorithm.raptoradapter.transit.TripSchedule; import org.opentripplanner.routing.algorithm.raptoradapter.transit.frequency.TripFrequencyAlightSearch; @@ -59,6 +62,7 @@ public class TripPatternForDates // potentially filtered by wheelchair accessibility private final BitSet boardingPossible; private final BitSet alightingPossible; + private final HeuristicTrip heuristicTrip; TripPatternForDates( RoutingTripPattern tripPattern, @@ -72,6 +76,10 @@ public class TripPatternForDates this.offsets = offsets; this.boardingPossible = boardingPossible; this.alightingPossible = alightningPossible; + this.heuristicTrip = + ArrayUtils.allValuesEquals(tripPatternForDates, TripPatternForDate::heuristicTrip) + ? tripPatternForDates[0].heuristicTrip() + : HeuristicTrip.of(tripPatternForDates); int numberOfTripSchedules = 0; boolean hasFrequencies = false; @@ -171,6 +179,7 @@ public int slackIndex() { return tripPattern.slackIndex(); } + @Override public int transitReluctanceFactorIndex() { return tripPattern.transitReluctanceFactorIndex(); } @@ -222,6 +231,11 @@ public int numberOfTripSchedules() { return numberOfTripSchedules; } + @Override + public RaptorTripSchedule getHeuristicTrip() { + return heuristicTrip; + } + @Override public Route route() { return tripPattern.route(); diff --git a/src/main/java/org/opentripplanner/transit/model/framework/Deduplicator.java b/src/main/java/org/opentripplanner/transit/model/framework/Deduplicator.java index aa109f8a556..7f4de748055 100644 --- a/src/main/java/org/opentripplanner/transit/model/framework/Deduplicator.java +++ b/src/main/java/org/opentripplanner/transit/model/framework/Deduplicator.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -25,15 +26,15 @@ public class Deduplicator implements Serializable { private static final String ZERO_COUNT = sizeAndCount(0, 0); private final Map canonicalBitSets = new HashMap<>(); - private final Map canonicalIntArrays = new HashMap<>(); + private final Map canonicalIntArrays = new ConcurrentHashMap<>(); private final Map canonicalStrings = new HashMap<>(); private final Map canonicalStringArrays = new HashMap<>(); private final Map canonicalString2DArrays = new HashMap<>(); - private final Map, Map> canonicalObjects = new HashMap<>(); + private final Map, Map> canonicalObjects = new ConcurrentHashMap<>(); private final Map, Map> canonicalObjArrays = new HashMap<>(); private final Map, Map, List>> canonicalLists = new HashMap<>(); - private final Map effectCounter = new HashMap<>(); + private final Map effectCounter = new ConcurrentHashMap<>(); @Inject public Deduplicator() {} @@ -71,10 +72,9 @@ public int[] deduplicateIntArray(int[] original) { return null; } IntArray intArray = new IntArray(original); - IntArray canonical = canonicalIntArrays.get(intArray); + IntArray canonical = canonicalIntArrays.putIfAbsent(intArray, intArray); if (canonical == null) { canonical = intArray; - canonicalIntArrays.put(canonical, canonical); } incrementEffectCounter(IntArray.class); return canonical.array; diff --git a/src/main/java/org/opentripplanner/transit/model/network/RoutingTripPattern.java b/src/main/java/org/opentripplanner/transit/model/network/RoutingTripPattern.java index 7908ccbd7f7..670dbfd6275 100644 --- a/src/main/java/org/opentripplanner/transit/model/network/RoutingTripPattern.java +++ b/src/main/java/org/opentripplanner/transit/model/network/RoutingTripPattern.java @@ -100,6 +100,7 @@ public int slackIndex() { return slackIndex; } + @Override public int transitReluctanceFactorIndex() { return transitReluctanceFactorIndex; } diff --git a/src/test/java/org/opentripplanner/raptor/_data/transit/TestRoute.java b/src/test/java/org/opentripplanner/raptor/_data/transit/TestRoute.java index 67fa485507f..d78c2fb575d 100644 --- a/src/test/java/org/opentripplanner/raptor/_data/transit/TestRoute.java +++ b/src/test/java/org/opentripplanner/raptor/_data/transit/TestRoute.java @@ -8,9 +8,11 @@ import org.opentripplanner.raptor.spi.RaptorConstrainedBoardingSearch; import org.opentripplanner.raptor.spi.RaptorRoute; import org.opentripplanner.raptor.spi.RaptorTimeTable; +import org.opentripplanner.raptor.spi.RaptorTripSchedule; import org.opentripplanner.raptor.spi.RaptorTripScheduleSearch; import org.opentripplanner.raptor.spi.SearchDirection; import org.opentripplanner.routing.algorithm.raptoradapter.api.DefaultTripPattern; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.HeuristicTrip; import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripScheduleSearchFactory; public class TestRoute implements RaptorRoute, RaptorTimeTable { @@ -73,6 +75,11 @@ public RaptorTripScheduleSearch tripSearch(SearchDirection dir return TripScheduleSearchFactory.create(direction, new TestTripSearchTimetable(this)); } + @Override + public RaptorTripSchedule getHeuristicTrip() { + return HeuristicTrip.of(schedules); + } + public List listTransferConstraintsForwardSearch() { return transferConstraintsForwardSearch.constrainedBoardings(); } diff --git a/src/test/java/org/opentripplanner/raptor/_data/transit/TestTripPattern.java b/src/test/java/org/opentripplanner/raptor/_data/transit/TestTripPattern.java index 7985f331f68..c04d3e1bd19 100644 --- a/src/test/java/org/opentripplanner/raptor/_data/transit/TestTripPattern.java +++ b/src/test/java/org/opentripplanner/raptor/_data/transit/TestTripPattern.java @@ -17,7 +17,7 @@ public class TestTripPattern implements DefaultTripPattern { * improves the performance. */ private int slackIndex = 0; - + private int transitReluctanceFactorIndex = 0; private int patternIndex = 0; /** @@ -57,6 +57,11 @@ TestTripPattern withPatternIndex(int index) { return this; } + public TestTripPattern withTransitReluctanceFactorIndex(int index) { + this.transitReluctanceFactorIndex = index; + return this; + } + public TestTripPattern withRoute(Route route) { this.route = route; return this; @@ -117,6 +122,11 @@ public int slackIndex() { return slackIndex; } + @Override + public int transitReluctanceFactorIndex() { + return transitReluctanceFactorIndex; + } + @Override public int patternIndex() { return patternIndex; diff --git a/src/test/java/org/opentripplanner/raptor/_data/transit/TestTripSearchTimetable.java b/src/test/java/org/opentripplanner/raptor/_data/transit/TestTripSearchTimetable.java index b4a1af68200..0412fb73757 100644 --- a/src/test/java/org/opentripplanner/raptor/_data/transit/TestTripSearchTimetable.java +++ b/src/test/java/org/opentripplanner/raptor/_data/transit/TestTripSearchTimetable.java @@ -1,8 +1,11 @@ package org.opentripplanner.raptor._data.transit; +import java.util.Arrays; import java.util.function.IntUnaryOperator; +import org.opentripplanner.raptor.spi.RaptorTripSchedule; import org.opentripplanner.raptor.spi.RaptorTripScheduleSearch; import org.opentripplanner.raptor.spi.SearchDirection; +import org.opentripplanner.routing.algorithm.raptoradapter.transit.HeuristicTrip; import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripScheduleSearchFactory; import org.opentripplanner.routing.algorithm.raptoradapter.transit.request.TripSearchTimetable; @@ -43,4 +46,9 @@ public IntUnaryOperator getDepartureTimes(int stopPositionInPattern) { public RaptorTripScheduleSearch tripSearch(SearchDirection direction) { return TripScheduleSearchFactory.create(direction, this); } + + @Override + public RaptorTripSchedule getHeuristicTrip() { + return HeuristicTrip.of(Arrays.asList(trips)); + } } diff --git a/src/test/java/org/opentripplanner/raptor/moduletests/G02_HeuristicReboardingTest.java b/src/test/java/org/opentripplanner/raptor/moduletests/G02_HeuristicReboardingTest.java new file mode 100644 index 00000000000..5726540b834 --- /dev/null +++ b/src/test/java/org/opentripplanner/raptor/moduletests/G02_HeuristicReboardingTest.java @@ -0,0 +1,222 @@ +package org.opentripplanner.raptor.moduletests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.framework.time.TimeUtils.hm2time; +import static org.opentripplanner.raptor._data.transit.TestRoute.route; +import static org.opentripplanner.raptor._data.transit.TestTripPattern.pattern; +import static org.opentripplanner.raptor._data.transit.TestTripSchedule.schedule; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.opentripplanner.raptor._data.RaptorTestConstants; +import org.opentripplanner.raptor._data.transit.TestAccessEgress; +import org.opentripplanner.raptor._data.transit.TestTransitData; +import org.opentripplanner.raptor._data.transit.TestTripSchedule; +import org.opentripplanner.raptor.api.request.RaptorProfile; +import org.opentripplanner.raptor.api.request.RaptorRequestBuilder; +import org.opentripplanner.raptor.configure.RaptorConfig; +import org.opentripplanner.raptor.rangeraptor.internalapi.Heuristics; +import org.opentripplanner.raptor.spi.RaptorSlackProvider; +import org.opentripplanner.raptor.spi.SearchDirection; +import org.opentripplanner.test.support.VariableSource; + +public class G02_HeuristicReboardingTest implements RaptorTestConstants { + + // Any big negative number will do, but -1 is a legal value + private static final int UNREACHED = -9999; + private static final int[] BEST_TRANSFERS_REVERSE = { UNREACHED, 0, 0, -1, -1 }; + private static final int[] BEST_TRANSFERS_FORWARD = { UNREACHED, -1, -1, 0, 0 }; + + private final TestTransitData data = new TestTransitData(); + private final RaptorRequestBuilder requestBuilder = new RaptorRequestBuilder<>(); + private final RaptorConfig config = RaptorConfig.defaultConfigForTest(); + + @BeforeEach + public void setup() { + data.withRoute( + route(pattern("R1", STOP_A, STOP_B, STOP_C, STOP_D)) + .withTimetable(schedule("00:01, 00:03, 00:05, 00:07")) + ); + + requestBuilder.slackProvider(RaptorSlackProvider.defaultSlackProvider(0, 0, 0)); + + requestBuilder + .searchParams() + .earliestDepartureTime(T00_00) + .latestArrivalTime(hm2time(0, 8)) + .searchOneIterationOnly(); + } + + private static final List profiles = List.of( + Arguments.of(RaptorProfile.MIN_TRAVEL_DURATION, SearchDirection.FORWARD), + Arguments.of(RaptorProfile.MIN_TRAVEL_DURATION_BEST_TIME, SearchDirection.FORWARD), + Arguments.of(RaptorProfile.MIN_TRAVEL_DURATION_AND_COST_BEST_TIME, SearchDirection.FORWARD), + Arguments.of(RaptorProfile.MIN_TRAVEL_DURATION, SearchDirection.REVERSE), + Arguments.of(RaptorProfile.MIN_TRAVEL_DURATION_BEST_TIME, SearchDirection.REVERSE), + Arguments.of(RaptorProfile.MIN_TRAVEL_DURATION_AND_COST_BEST_TIME, SearchDirection.REVERSE) + ); + + /** + *
+   * Stops: 0..4
+   *
+   * Stop on route (stop indexes):
+   *   R1:  1 - 2 - 3 - 4
+   *
+   * Schedule:
+   *   R1: 00:01 - 00:03 - 00:05 - 00:07
+   *
+   * Access (toStop & duration):
+   *   1  1min
+   *   2  2min
+   *
+   * Egress (fromStop & duration):
+   *   3  2min
+   *   4  1min
+   *
+   * 
+ */ + @ParameterizedTest(name = "profile {0}, direction {1}") + @VariableSource("profiles") + public void testRequireReboarding(RaptorProfile profile, SearchDirection direction) { + requestBuilder + .searchParams() + .addAccessPaths(TestAccessEgress.walk(STOP_A, D1m)) + .addAccessPaths(TestAccessEgress.walk(STOP_B, D2m)) + .addEgressPaths(TestAccessEgress.walk(STOP_C, D2m)) + .addEgressPaths(TestAccessEgress.walk(STOP_D, D1m)); + + var request = requestBuilder.profile(profile).searchDirection(direction).build(); + + var search = config.createHeuristicSearch(data, data.multiCriteriaCostCalculator(), request); + + search.route(); + var destinationHeuristics = search.heuristics(); + + var bestTransfers = direction == SearchDirection.FORWARD + ? BEST_TRANSFERS_FORWARD + : BEST_TRANSFERS_REVERSE; + + var bestTimes = direction == SearchDirection.FORWARD + ? new int[] { + UNREACHED, + // Access + R1 + 60, + 2 * 60, + 2 * 60 + 2 * 60, + 2 * 60 + 4 * 60, + } + : new int[] { + UNREACHED, + // Egress + R1 + 2 * 60 + 4 * 60, + 2 * 60 + 2 * 60, + 2 * 60, + 60, + }; + + assertHeuristics(destinationHeuristics, bestTransfers, bestTimes); + } + + /** + *
+   * Stops: 0..4
+   *
+   * Stop on route (stop indexes):
+   *   R1:  1 - 2 - 3 - 4
+   *
+   * Schedule:
+   *   R1: 00:01 - 00:03 - 00:05 - 00:07
+   *
+   * Access (toStop & duration):
+   *   1  1min
+   *   2  5min
+   *
+   * Egress (fromStop & duration):
+   *   3  5min
+   *   4  1min
+   *
+   * 
+ */ + @ParameterizedTest(name = "profile {0}, direction {1}") + @VariableSource("profiles") + public void testSlowerByReboarding(RaptorProfile profile, SearchDirection direction) { + requestBuilder + .searchParams() + .addAccessPaths(TestAccessEgress.walk(STOP_A, D1m)) + .addAccessPaths(TestAccessEgress.walk(STOP_B, D5m)) + .addEgressPaths(TestAccessEgress.walk(STOP_C, D5m)) + .addEgressPaths(TestAccessEgress.walk(STOP_D, D1m)); + + var request = requestBuilder.profile(profile).searchDirection(direction).build(); + + var search = config.createHeuristicSearch(data, data.multiCriteriaCostCalculator(), request); + + search.route(); + var destinationHeuristics = search.heuristics(); + + var bestTransfers = direction == SearchDirection.FORWARD + ? BEST_TRANSFERS_FORWARD + : BEST_TRANSFERS_REVERSE; + + var bestTimes = direction == SearchDirection.FORWARD + ? new int[] { + UNREACHED, + // Access + R1 + 60, + 60 + 2 * 60, + 60 + 4 * 60, + 60 + 6 * 60, + } + : new int[] { + UNREACHED, + // Egress + R1 + 60 + 6 * 60, + 60 + 4 * 60, + 60 + 2 * 60, + 60, + }; + + assertHeuristics(destinationHeuristics, bestTransfers, bestTimes); + } + + private static void assertHeuristics( + Heuristics destinationHeuristics, + int[] bestTransfers, + int[] bestTimes + ) { + assertNotNull(destinationHeuristics); + + assertArrayLessOrEqual( + bestTransfers, + destinationHeuristics.bestNumOfTransfersToIntArray(UNREACHED), + "best number of transfers" + ); + assertArrayLessOrEqual( + bestTimes, + destinationHeuristics.bestTravelDurationToIntArray(UNREACHED), + "best times" + ); + } + + private static void assertArrayLessOrEqual(int[] expected, int[] actual, String arrayName) { + assertNotNull(actual); + assertEquals(expected.length, actual.length); + for (int i = 0; i < expected.length; i++) { + assertTrue( + expected[i] >= actual[i], + String.format( + "Value %d is greater than %d for index %d in %s", + actual[i], + expected[i], + i, + arrayName + ) + ); + } + } +} diff --git a/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/HeuristicTripTest.java b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/HeuristicTripTest.java new file mode 100644 index 00000000000..9618ec75e44 --- /dev/null +++ b/src/test/java/org/opentripplanner/routing/algorithm/raptoradapter/transit/HeuristicTripTest.java @@ -0,0 +1,122 @@ +package org.opentripplanner.routing.algorithm.raptoradapter.transit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.raptor._data.RaptorTestConstants.STOP_A; +import static org.opentripplanner.raptor._data.RaptorTestConstants.STOP_B; +import static org.opentripplanner.raptor._data.RaptorTestConstants.STOP_C; +import static org.opentripplanner.raptor._data.RaptorTestConstants.STOP_D; +import static org.opentripplanner.raptor._data.RaptorTestConstants.STOP_E; +import static org.opentripplanner.raptor._data.transit.TestTripSchedule.schedule; + +import org.junit.jupiter.api.Test; +import org.opentripplanner.raptor._data.transit.TestRoute; +import org.opentripplanner.raptor.spi.RaptorTripSchedule; + +class HeuristicTripTest { + + /** + * Test with five minutes of running time and no dwell time + */ + @Test + public void testSameArrivalDepartureTimes() { + int[] times = { 0, 5, 10, 15, 20 }; + + TestRoute route = TestRoute + .route("R1", STOP_A, STOP_B, STOP_C, STOP_D, STOP_E) + .withTimetable( + schedule("00:10 00:15 00:20 00:25 00:30"), + schedule("00:15 00:20 00:25 00:30 00:35"), + schedule("00:20 00:25 00:30 00:35 00:40"), + schedule("00:25 00:30 00:35 00:40 00:45") + ); + + RaptorTripSchedule subject = route.getHeuristicTrip(); + + for (int i = 0; i < subject.pattern().numberOfStopsInPattern(); i++) { + assertEquals(times[i], subject.departure(i) / 60); + assertEquals(subject.arrival(i), subject.departure(i)); + } + } + + /** + * Test with four minutes of running time and one minute of dwell + */ + @Test + public void testDwellTimes() { + int[] times = { 0, 4, 9, 14, 19 }; + int departureOffset = 60; + + TestRoute route = TestRoute + .route("R1", STOP_A, STOP_B, STOP_C, STOP_D, STOP_E) + .withTimetable( + schedule().arrivals("00:10 00:15 00:20 00:25 00:30").arrDepOffset(departureOffset), + schedule().arrivals("00:15 00:20 00:25 00:30 00:35").arrDepOffset(departureOffset), + schedule().arrivals("00:20 00:25 00:30 00:35 00:40").arrDepOffset(departureOffset), + schedule().arrivals("00:25 00:30 00:35 00:40 00:45").arrDepOffset(departureOffset) + ); + + RaptorTripSchedule subject = route.getHeuristicTrip(); + + assertEquals(0, subject.arrival(0)); + assertEquals(0, subject.departure(0)); + + for (int i = 1; i < subject.pattern().numberOfStopsInPattern(); i++) { + assertEquals(times[i], subject.arrival(i) / 60); + assertEquals(subject.arrival(i) + departureOffset, subject.departure(i)); + } + } + + /** + * Test with two trips, one with five minutes of running time and no dwell time and the other with + * four minutes of running time and one minute of dwell + */ + @Test + public void testStaggeredTimes() { + int[] times = { 0, 4, 9, 14, 19 }; + int departureOffset = 60; + + TestRoute route = TestRoute + .route("R1", STOP_A, STOP_B, STOP_C, STOP_D, STOP_E) + .withTimetable( + schedule().arrivals("00:10 00:15 00:20 00:25 00:30").arrDepOffset(0), + schedule().arrivals("10:10 10:15 10:20 10:25 10:30").arrDepOffset(departureOffset) + ); + + RaptorTripSchedule subject = route.getHeuristicTrip(); + + assertEquals(0, subject.arrival(0)); + assertEquals(0, subject.departure(0)); + + for (int i = 1; i < subject.pattern().numberOfStopsInPattern(); i++) { + assertEquals(times[i], subject.arrival(i) / 60); + assertEquals(subject.arrival(i) + departureOffset, subject.departure(i)); + } + } + + /** + * Test with two trips, one with five minutes of running time on the two first hosp and 10 minutes + * on the last hops and a second trip with the hop durations reversed. Note that the last times + * are less than either of the running times. + */ + @Test + public void testDifferentTimes() { + int[] times = { 0, 5, 10, 15, 20 }; + + TestRoute route = TestRoute + .route("R1", STOP_A, STOP_B, STOP_C, STOP_D, STOP_E) + .withTimetable( + schedule().times("00:10 00:15 00:20 00:30 00:40"), + schedule().times("10:10 10:20 10:30 10:35 10:40") + ); + + RaptorTripSchedule subject = route.getHeuristicTrip(); + + assertEquals(0, subject.arrival(0)); + assertEquals(0, subject.departure(0)); + + for (int i = 1; i < subject.pattern().numberOfStopsInPattern(); i++) { + assertEquals(times[i], subject.arrival(i) / 60); + assertEquals(subject.arrival(i), subject.departure(i)); + } + } +} diff --git a/src/test/java/org/opentripplanner/test/support/VariableArgumentsProvider.java b/src/test/java/org/opentripplanner/test/support/VariableArgumentsProvider.java index 1ae9fb2af84..afaf5861a4e 100644 --- a/src/test/java/org/opentripplanner/test/support/VariableArgumentsProvider.java +++ b/src/test/java/org/opentripplanner/test/support/VariableArgumentsProvider.java @@ -1,6 +1,7 @@ package org.opentripplanner.test.support; import java.lang.reflect.Field; +import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.provider.Arguments; @@ -50,6 +51,12 @@ private Stream getValue(Field field) { field.setAccessible(accessible); - return value == null ? null : (Stream) value; + if (value instanceof Stream stream) { + return (Stream) stream; + } else if (value instanceof List list) { + return list.stream(); + } else { + return null; + } } } diff --git a/src/test/java/org/opentripplanner/transit/speed_test/SpeedTestRequest.java b/src/test/java/org/opentripplanner/transit/speed_test/SpeedTestRequest.java index e0f2c161417..5da1cd268f4 100644 --- a/src/test/java/org/opentripplanner/transit/speed_test/SpeedTestRequest.java +++ b/src/test/java/org/opentripplanner/transit/speed_test/SpeedTestRequest.java @@ -1,6 +1,7 @@ package org.opentripplanner.transit.speed_test; import static org.opentripplanner.raptor.api.request.RaptorProfile.MIN_TRAVEL_DURATION; +import static org.opentripplanner.raptor.api.request.RaptorProfile.MIN_TRAVEL_DURATION_AND_COST_BEST_TIME; import static org.opentripplanner.raptor.api.request.RaptorProfile.MIN_TRAVEL_DURATION_BEST_TIME; import java.time.Duration; @@ -62,7 +63,15 @@ RouteRequest toRouteRequest() { request.setNumItineraries(opts.numOfItineraries()); request.journey().setModes(input.modes()); - if (profile.raptorProfile().isOneOf(MIN_TRAVEL_DURATION, MIN_TRAVEL_DURATION_BEST_TIME)) { + if ( + profile + .raptorProfile() + .isOneOf( + MIN_TRAVEL_DURATION, + MIN_TRAVEL_DURATION_BEST_TIME, + MIN_TRAVEL_DURATION_AND_COST_BEST_TIME + ) + ) { request.setSearchWindow(Duration.ZERO); } diff --git a/src/test/java/org/opentripplanner/transit/speed_test/model/SpeedTestProfile.java b/src/test/java/org/opentripplanner/transit/speed_test/model/SpeedTestProfile.java index d5f7c14048d..a293097f04e 100644 --- a/src/test/java/org/opentripplanner/transit/speed_test/model/SpeedTestProfile.java +++ b/src/test/java/org/opentripplanner/transit/speed_test/model/SpeedTestProfile.java @@ -56,6 +56,18 @@ public enum SpeedTestProfile { RaptorProfile.MIN_TRAVEL_DURATION_BEST_TIME, SearchDirection.REVERSE ), + min_travel_duration_best_time_and_cost( + "tbc", + "Best Time and Cost Range Raptor without waiting time.", + RaptorProfile.MIN_TRAVEL_DURATION_AND_COST_BEST_TIME, + SearchDirection.FORWARD + ), + min_travel_duration_best_time_and_cost_reverse( + "tbcr", + "Reverse Best Time and Cost Range Raptor without waiting time.", + RaptorProfile.MIN_TRAVEL_DURATION_AND_COST_BEST_TIME, + SearchDirection.REVERSE + ), mc_range_raptor( "mc", "Multi-Criteria Range Raptor [ transfers, arrival time, travel time, cost ].",