diff --git a/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java b/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java index e8e83373..ecc5fffb 100644 --- a/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java +++ b/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java @@ -70,11 +70,12 @@ List reconstructParetoOptimalSolutions(List best Map targetStops, LocalDate referenceDate) { final List connections = new ArrayList<>(); + int bestTime = timeType == TimeType.DEPARTURE ? INFINITY : -INFINITY; + // iterate over all rounds for (QueryState.Label[] labels : bestLabelsPerRound) { QueryState.Label label = null; - int bestTime = timeType == TimeType.DEPARTURE ? INFINITY : -INFINITY; for (Map.Entry entry : targetStops.entrySet()) { int targetStopIdx = entry.getKey(); diff --git a/src/main/java/ch/naviqore/raptor/router/QueryState.java b/src/main/java/ch/naviqore/raptor/router/QueryState.java index d30c00ce..45105a73 100644 --- a/src/main/java/ch/naviqore/raptor/router/QueryState.java +++ b/src/main/java/ch/naviqore/raptor/router/QueryState.java @@ -111,14 +111,22 @@ int getComparableBestTime(int stopIdx) { * different label types (transfer vs. route), as the same stop transfer time is not considered. */ int getActualBestTime(int stopIdx) { - for (int i = bestLabelsPerRound.size() - 1; i >= 0; i--) { - Label label = bestLabelsPerRound.get(i)[stopIdx]; + int best_time = (timeType == TimeType.DEPARTURE) ? INFINITY : -INFINITY; + + // because range raptor potentially fills target times in higher rounds which are not the best solutions, every + // round has to be looked at. + for (Label[] labels : bestLabelsPerRound) { + Label label = labels[stopIdx]; if (label != null) { - return label.targetTime; + if (timeType == TimeType.DEPARTURE) { + best_time = Math.min(best_time, label.targetTime); + } else { + best_time = Math.max(best_time, label.targetTime); + } } } - return (timeType == TimeType.DEPARTURE) ? INFINITY : -INFINITY; + return best_time; } /** diff --git a/src/test/java/ch/naviqore/raptor/router/RangeRaptorTest.java b/src/test/java/ch/naviqore/raptor/router/RangeRaptorTest.java index 299ce9f3..fd0d3785 100644 --- a/src/test/java/ch/naviqore/raptor/router/RangeRaptorTest.java +++ b/src/test/java/ch/naviqore/raptor/router/RangeRaptorTest.java @@ -18,6 +18,7 @@ public class RangeRaptorTest { private static final String STOP_A = "A"; private static final String STOP_I = "I"; + private static final String STOP_K = "K"; private static final String STOP_N = "N"; private static final LocalDateTime START_OF_DAY = LocalDateTime.of(2021, 1, 1, 0, 0); @@ -261,6 +262,53 @@ void findArrivalConnections_withSourceTransferFirst() { STOP_A, STOP_N); } + @Test + void ensureParetoOptimalConnections() { + // this test is based on a previous bug, where the range raptor router returned connections which were not + // pareto optimal (later arrival time and more rounds). This is owed to the fact that the range raptor spawns + // at different time points (range offsets) and potentially finds earliest arrival connections for the given + // offset with more rounds than the final best arrival time. + // this test reproduces this case by introducing a low frequency fast connection and a high frequency slower + // connection. + + int headwayRoute1 = 15; + int headwayRoute2 = 60; + int headwayRoute3and4 = 5; + + int dwellTime = 0; // simplification to better calculate times by hand + + RaptorAlgorithm rangeRaptor = new RaptorRouterTestBuilder().withAddRoute1_AG( + RaptorRouterTestBuilder.DEFAULT_OFFSET, headwayRoute1, + RaptorRouterTestBuilder.DEFAULT_TIME_BETWEEN_STOPS, dwellTime) + .withAddRoute2_HL(RaptorRouterTestBuilder.DEFAULT_OFFSET, headwayRoute2, + RaptorRouterTestBuilder.DEFAULT_TIME_BETWEEN_STOPS, dwellTime) + .withAddRoute3_MQ(RaptorRouterTestBuilder.DEFAULT_OFFSET, headwayRoute3and4, + RaptorRouterTestBuilder.DEFAULT_TIME_BETWEEN_STOPS, dwellTime) + .withAddRoute4_RS(RaptorRouterTestBuilder.DEFAULT_OFFSET, headwayRoute3and4, + RaptorRouterTestBuilder.DEFAULT_TIME_BETWEEN_STOPS, dwellTime) + .withSameStopTransferTime(0) + .withRaptorRange(900) // departures at 08:00 and 08:15 + .withMaxDaysToScan(1) + .build(); + + // departure at 8:00 will yield only one fastest connection + // 08:00 A --> R1 --> 08:05 B + // 08:05 B --> R2 --> 08:20 K + LocalDateTime expectedDepartureTime = EIGHT_AM; + LocalDateTime expectedArrivalTime = expectedDepartureTime.plusMinutes(20); + + // however since spawning at 08:15 (first range checked) will find following best solution, this test must + // ensure that this connection is not returned as it is not pareto optimal. + // 08:15 A --> R1 --> 08:40 F + // 08:40 F --> R4 --> 08:45 P + // 08:45 P --> R3 --> 09:00 K + List connections = RaptorRouterTestHelpers.routeEarliestArrival(rangeRaptor, STOP_A, STOP_K, + EIGHT_AM); + assertEquals(1, connections.size()); + RangeRaptorHelpers.assertConnection(connections.getFirst(), expectedDepartureTime, expectedArrivalTime, 2, + STOP_A, STOP_K); + } + static class RangeRaptorHelpers { static void assertConnection(Connection connection, LocalDateTime expectedDepartureTime, diff --git a/src/test/java/ch/naviqore/raptor/router/RaptorRouterTestBuilder.java b/src/test/java/ch/naviqore/raptor/router/RaptorRouterTestBuilder.java index 4baba259..38a461b4 100644 --- a/src/test/java/ch/naviqore/raptor/router/RaptorRouterTestBuilder.java +++ b/src/test/java/ch/naviqore/raptor/router/RaptorRouterTestBuilder.java @@ -160,6 +160,11 @@ public RaptorRouterTestBuilder withAddRoute4_RS() { return this; } + public RaptorRouterTestBuilder withAddRoute4_RS(int offset, int headway, int travelTime, int dwellTime) { + routes.add(new Route("R4", List.of("R", "P", "F", "S"), offset, headway, travelTime, dwellTime)); + return this; + } + public RaptorRouterTestBuilder withAddRoute5_AH_selfIntersecting() { routes.add(new Route("R5", List.of("A", "B", "C", "D", "E", "F", "P", "O", "N", "K", "J", "I", "B", "H"))); return this;