From d3895b1cc16f7718a372e2c18584263df3972e17 Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Tue, 5 Nov 2024 22:47:38 +0100 Subject: [PATCH 1/4] FIX: 145 - Make getActualBestTime actually return actual best time. --- .../ch/naviqore/raptor/router/QueryState.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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; } /** From 48fb7c372901c370981423f34f7c0ce4a3c59fcc Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Tue, 5 Nov 2024 22:48:08 +0100 Subject: [PATCH 2/4] FIX: 145 - Make sure only pareto optimal solutions are returned for range raptor. --- .../ch/naviqore/raptor/router/LabelPostprocessor.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java b/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java index e8e83373..4f17d71d 100644 --- a/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java +++ b/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java @@ -70,6 +70,11 @@ List reconstructParetoOptimalSolutions(List best Map targetStops, LocalDate referenceDate) { final List connections = new ArrayList<>(); + // this additional tracking variable is needed to filter out non pareto optimal connections from range raptor, + // as the pareto optimal solution for a later departure might have actually been fastest with more rounds where + // the final best solution has fewer rounds and is faster + int overallBestTime = timeType == TimeType.DEPARTURE ? INFINITY : -INFINITY; + // iterate over all rounds for (QueryState.Label[] labels : bestLabelsPerRound) { @@ -86,15 +91,17 @@ List reconstructParetoOptimalSolutions(List best if (timeType == TimeType.DEPARTURE) { int actualArrivalTime = currentLabel.targetTime() + targetStopWalkingTime; - if (actualArrivalTime < bestTime) { + if (actualArrivalTime < bestTime && actualArrivalTime < overallBestTime) { label = currentLabel; bestTime = actualArrivalTime; + overallBestTime = actualArrivalTime; } } else { int actualDepartureTime = currentLabel.targetTime() - targetStopWalkingTime; - if (actualDepartureTime > bestTime) { + if (actualDepartureTime > bestTime && actualDepartureTime > overallBestTime) { label = currentLabel; bestTime = actualDepartureTime; + overallBestTime = actualDepartureTime; } } } From 23a2265a018ca2cae58b2244b8a613dd4f394987 Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Tue, 5 Nov 2024 22:54:04 +0100 Subject: [PATCH 3/4] REFACTOR: 145 - Remove duplicate bestTime variable in reconstructParetoOptimalSolutions. --- .../naviqore/raptor/router/LabelPostprocessor.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java b/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java index 4f17d71d..ecc5fffb 100644 --- a/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java +++ b/src/main/java/ch/naviqore/raptor/router/LabelPostprocessor.java @@ -70,16 +70,12 @@ List reconstructParetoOptimalSolutions(List best Map targetStops, LocalDate referenceDate) { final List connections = new ArrayList<>(); - // this additional tracking variable is needed to filter out non pareto optimal connections from range raptor, - // as the pareto optimal solution for a later departure might have actually been fastest with more rounds where - // the final best solution has fewer rounds and is faster - int overallBestTime = timeType == TimeType.DEPARTURE ? INFINITY : -INFINITY; + 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(); @@ -91,17 +87,15 @@ List reconstructParetoOptimalSolutions(List best if (timeType == TimeType.DEPARTURE) { int actualArrivalTime = currentLabel.targetTime() + targetStopWalkingTime; - if (actualArrivalTime < bestTime && actualArrivalTime < overallBestTime) { + if (actualArrivalTime < bestTime) { label = currentLabel; bestTime = actualArrivalTime; - overallBestTime = actualArrivalTime; } } else { int actualDepartureTime = currentLabel.targetTime() - targetStopWalkingTime; - if (actualDepartureTime > bestTime && actualDepartureTime > overallBestTime) { + if (actualDepartureTime > bestTime) { label = currentLabel; bestTime = actualDepartureTime; - overallBestTime = actualDepartureTime; } } } From 732f829e4238fac3f66080d2e2944666c9a129f7 Mon Sep 17 00:00:00 2001 From: Lukas Connolly Date: Tue, 5 Nov 2024 23:31:37 +0100 Subject: [PATCH 4/4] TEST: 145 - Add test for range raptor to ensure pareto optimal solutions are returned. --- .../raptor/router/RangeRaptorTest.java | 48 +++++++++++++++++++ .../router/RaptorRouterTestBuilder.java | 5 ++ 2 files changed, 53 insertions(+) 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;