From cf9a0af65148f65bcf1b012f6a94986e1827b7d3 Mon Sep 17 00:00:00 2001 From: ansons Date: Sat, 3 Jun 2023 14:15:21 -0400 Subject: [PATCH] Allow setting seed for frequency offsets Addresses #714 --- .../conveyal/analysis/models/AnalysisRequest.java | 2 ++ .../com/conveyal/r5/profile/FastRaptorWorker.java | 2 +- .../conveyal/r5/profile/FrequencyRandomOffsets.java | 13 +++++++++---- .../McRaptorSuboptimalPathProfileRouter.java | 7 ++++--- .../com/conveyal/r5/profile/ProfileRequest.java | 2 ++ .../r5/transit/FrequencyRandomOffsetsTest.java | 4 ++-- 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/conveyal/analysis/models/AnalysisRequest.java b/src/main/java/com/conveyal/analysis/models/AnalysisRequest.java index 10d326656..7df1df676 100644 --- a/src/main/java/com/conveyal/analysis/models/AnalysisRequest.java +++ b/src/main/java/com/conveyal/analysis/models/AnalysisRequest.java @@ -55,6 +55,7 @@ public class AnalysisRequest { public float toLon; public int fromTime; public int monteCarloDraws = 200; + public boolean lockSchedules = false; public int toTime; public String transitModes; public float walkSpeed; @@ -244,6 +245,7 @@ public void populateTask (AnalysisWorkerTask task, UserPermissions userPermissio task.cutoffsMinutes = cutoffsMinutes; task.logRequest = logRequest; + task.lockSchedules = lockSchedules; task.accessModes = getEnumSetFromString(accessModes); task.directModes = getEnumSetFromString(directModes); diff --git a/src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java b/src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java index 0ac37b77f..e856345f4 100644 --- a/src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java +++ b/src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java @@ -145,7 +145,7 @@ public FastRaptorWorker (TransitLayer transitLayer, AnalysisWorkerTask request, this.accessStops = accessStops; this.servicesActive = transit.getActiveServicesForDate(request.date); - offsets = new FrequencyRandomOffsets(transitLayer); + offsets = new FrequencyRandomOffsets(transitLayer, request); // compute number of minutes for scheduled search nMinutes = request.getTimeWindowLengthMinutes(); diff --git a/src/main/java/com/conveyal/r5/profile/FrequencyRandomOffsets.java b/src/main/java/com/conveyal/r5/profile/FrequencyRandomOffsets.java index ccc04c704..8393870a9 100644 --- a/src/main/java/com/conveyal/r5/profile/FrequencyRandomOffsets.java +++ b/src/main/java/com/conveyal/r5/profile/FrequencyRandomOffsets.java @@ -1,5 +1,6 @@ package com.conveyal.r5.profile; +import com.conveyal.analysis.models.AnalysisRequest; import com.conveyal.r5.transit.TransitLayer; import com.conveyal.r5.transit.TripPattern; import com.conveyal.r5.transit.TripSchedule; @@ -12,8 +13,6 @@ import java.util.HashMap; import java.util.Map; -import static com.google.common.base.Preconditions.checkElementIndex; -import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; /** @@ -44,13 +43,19 @@ public class FrequencyRandomOffsets { private final Map offsetsForTripSchedule = new HashMap<>(); /** The mersenne twister is a higher quality random number generator than the one included with Java */ - private MersenneTwister mt = new MersenneTwister(); + private MersenneTwister mt; - public FrequencyRandomOffsets(TransitLayer data) { + public FrequencyRandomOffsets(TransitLayer data, ProfileRequest request) { this.data = data; if (!data.hasFrequencies) { return; } + if (request == null || !request.lockSchedules) { + this.mt = new MersenneTwister(); + } else { + // Use a fixed seed for each origin + this.mt = new MersenneTwister((int) (request.fromLat * 1e9)); + } // Create skeleton empty data structure with slots for all offsets that will be generated. for (int pattIdx = 0; pattIdx < data.tripPatterns.size(); pattIdx++) { TripPattern tp = data.tripPatterns.get(pattIdx); diff --git a/src/main/java/com/conveyal/r5/profile/McRaptorSuboptimalPathProfileRouter.java b/src/main/java/com/conveyal/r5/profile/McRaptorSuboptimalPathProfileRouter.java index b38380060..c8c0cb8a3 100644 --- a/src/main/java/com/conveyal/r5/profile/McRaptorSuboptimalPathProfileRouter.java +++ b/src/main/java/com/conveyal/r5/profile/McRaptorSuboptimalPathProfileRouter.java @@ -113,7 +113,7 @@ public McRaptorSuboptimalPathProfileRouter (TransportNetwork network, ProfileReq this.touchedPatterns = new BitSet(network.transitLayer.tripPatterns.size()); this.patternsNearDestination = new BitSet(network.transitLayer.tripPatterns.size()); this.servicesActive = network.transitLayer.getActiveServicesForDate(req.date); - this.offsets = new FrequencyRandomOffsets(network.transitLayer); + this.offsets = new FrequencyRandomOffsets(network.transitLayer, request); this.saveFinalStates = saveFinalStates; if (saveFinalStates) this.finalStatesByDepartureTime = new TIntObjectHashMap<>(); @@ -150,7 +150,8 @@ public Collection route () { // We start at end of time window and work backwards (which is what range-RAPTOR does, in case we // re-implement that here). We use a constrained random walk to choose which departure minutes to sample as we - // work backward through the time window. According to others (Owen and Jiang?), this is a good way to reduce + // work backward through the time window. According to Owen and Murphy (2019) + // https://www.jtlu.org/index.php/jtlu/article/view/1379), this is a good way to reduce // the number of samples without causing an issue with variance in results. This value is the constraint // (upper limit) on the walk. // multiply by two because E[random] = 1/2 * max. @@ -535,7 +536,7 @@ private Collection doPropagationToDestination(int departureTime) } private ArrayList generateDepartureTimesToSample (ProfileRequest request) { - // See Owen and Jiang 2016 (unfortunately no longer available online), add between f / 2 and + // See Owen and Murphy 2019 (https://www.jtlu.org/index.php/jtlu/article/view/1379), add between f / 2 and // f + f / 2, where f is the mean step. int randomWalkStepMean = (request.toTime - request.fromTime) / request.monteCarloDraws; int randomWalkStepWidthOneSided = randomWalkStepMean / 2; diff --git a/src/main/java/com/conveyal/r5/profile/ProfileRequest.java b/src/main/java/com/conveyal/r5/profile/ProfileRequest.java index 7eaa6dc5d..24ec40c8c 100644 --- a/src/main/java/com/conveyal/r5/profile/ProfileRequest.java +++ b/src/main/java/com/conveyal/r5/profile/ProfileRequest.java @@ -123,6 +123,8 @@ public class ProfileRequest implements Serializable, Cloneable { /** the maximum number of options presented PER ACCESS MODE */ public int limit; + + public boolean lockSchedules = false; /** The modes used to access transit */ @JsonSerialize(using = LegModeSetSerializer.class) diff --git a/src/test/java/com/conveyal/r5/transit/FrequencyRandomOffsetsTest.java b/src/test/java/com/conveyal/r5/transit/FrequencyRandomOffsetsTest.java index f0fd04763..252ae5756 100644 --- a/src/test/java/com/conveyal/r5/transit/FrequencyRandomOffsetsTest.java +++ b/src/test/java/com/conveyal/r5/transit/FrequencyRandomOffsetsTest.java @@ -69,7 +69,7 @@ public void testPhasing () { layer.tripPatterns.add(pattern2); layer.rebuildTransientIndexes(); - FrequencyRandomOffsets fro = new FrequencyRandomOffsets(layer); + FrequencyRandomOffsets fro = new FrequencyRandomOffsets(layer, null); fro.randomize(); // check that phasing is correct @@ -136,7 +136,7 @@ public void testPhasingAtLastStop () { layer.tripPatterns.add(pattern2); layer.rebuildTransientIndexes(); - FrequencyRandomOffsets fro = new FrequencyRandomOffsets(layer); + FrequencyRandomOffsets fro = new FrequencyRandomOffsets(layer, null); fro.randomize(); // check that phasing is correct