diff --git a/src/main/java/com/conveyal/analysis/results/MultiOriginAssembler.java b/src/main/java/com/conveyal/analysis/results/MultiOriginAssembler.java index dc2b0b150..6aa3c9fad 100644 --- a/src/main/java/com/conveyal/analysis/results/MultiOriginAssembler.java +++ b/src/main/java/com/conveyal/analysis/results/MultiOriginAssembler.java @@ -139,7 +139,8 @@ public MultiOriginAssembler (RegionalAnalysis regionalAnalysis, Job job, FileSto // We might want to record a grid of dual accessibility values, but this will require some serious // refactoring of the GridResultWriter. // if (job.templateTask.dualAccessibilityThreshold > 0) { ... } - throw new IllegalArgumentException("Temporal density of opportunities cannot be recorded for gridded origin points."); + // throw new IllegalArgumentException("Temporal density of opportunities cannot be recorded for " + + // "gridded origin points."); } else { // Freeform origins. // Output includes temporal density of opportunities and optionally dual accessibility. diff --git a/src/main/java/com/conveyal/r5/analyst/TemporalDensityResult.java b/src/main/java/com/conveyal/r5/analyst/TemporalDensityResult.java index 6ba2294b4..7651bba0c 100644 --- a/src/main/java/com/conveyal/r5/analyst/TemporalDensityResult.java +++ b/src/main/java/com/conveyal/r5/analyst/TemporalDensityResult.java @@ -1,6 +1,7 @@ package com.conveyal.r5.analyst; import com.conveyal.r5.analyst.cluster.AnalysisWorkerTask; +import com.conveyal.r5.analyst.cluster.RegionalTask; import com.google.common.base.Preconditions; import static com.conveyal.r5.common.Util.notNullOrEmpty; @@ -89,6 +90,50 @@ public int[][] minutesToReachOpportunities(int n) { return result; } + /** + * Writes dual access travel time values (in minutes) to our standard access grid format. The value returned (for + * an origin) is the number of minutes required to reach a threshold number of opportunities (specified by + * the cutoffs and task.dualAccessibilityThreshold) in the specified destination layer at a given percentile of + * travel time. If the threshold cannot be reached in less than 120 minutes, returns 0. + * This is a temporary experimental feature, (ab)using existing features in the UI and backend so that dual access + * results for grid origins can be obtained without any changes to those other components of our system. It uses + * the supplied task.cutoffsMinutes as dual access thresholds. If a nonzero task.dualAccessibility In place of the + * last + * cutoffsMinutes value, it uses task.dualAccessibilityThreshold (which is initialized to 0, so this is safe even + * if a user does not supply it). + */ + public int[][][] fakeDualAccess (RegionalTask task) { + int nPointSets = task.destinationPointSets.length; + int nCutoffs = task.cutoffsMinutes.length; + int[][][] dualAccess = new int[nPointSets][nPercentiles][nCutoffs]; + for (int d = 0; d < nPointSets; d++) { + for (int p = 0; p < nPercentiles; p++) { + // Hack: use cutoffs as dual access thresholds + for (int c = 0; c < nCutoffs; c++) { + int m = 0; + double sum = 0; + while (sum < task.cutoffsMinutes[c] && m < 120) { + sum += opportunitiesPerMinute[d][p][m]; + m += 1; + } + dualAccess[d][p][c] = m; + } + // But the hack above won't allow thresholds over 120 (see validateCutoffsMinutes()); so overwrite + // the value for the last cutoff if a nonzero task.dualAccessibilityThreshold value is supplied. + if (task.dualAccessibilityThreshold != 0) { + int m = 0; + double sum = 0; + while (sum < task.dualAccessibilityThreshold && m < 120) { + sum += opportunitiesPerMinute[d][p][m]; + m += 1; + } + dualAccess[d][p][nCutoffs - 1] = m; + } + } + } + return dualAccess; + } + public int[][] minutesToReachOpportunities() { return minutesToReachOpportunities(opportunityThreshold); } diff --git a/src/main/java/com/conveyal/r5/analyst/TravelTimeReducer.java b/src/main/java/com/conveyal/r5/analyst/TravelTimeReducer.java index 213091d56..8fe50efc3 100644 --- a/src/main/java/com/conveyal/r5/analyst/TravelTimeReducer.java +++ b/src/main/java/com/conveyal/r5/analyst/TravelTimeReducer.java @@ -145,7 +145,7 @@ public TravelTimeReducer (AnalysisWorkerTask task, TransportNetwork network) { if (task.includePathResults) { pathResult = new PathResult(task, network.transitLayer); } - if (task.includeTemporalDensity) { + if (task.includeTemporalDensity || task.hasFlag("gridDualAccess")) { temporalDensityResult = new TemporalDensityResult(task); } diff --git a/src/main/java/com/conveyal/r5/analyst/cluster/AnalysisWorker.java b/src/main/java/com/conveyal/r5/analyst/cluster/AnalysisWorker.java index 2add61702..46bfed946 100644 --- a/src/main/java/com/conveyal/r5/analyst/cluster/AnalysisWorker.java +++ b/src/main/java/com/conveyal/r5/analyst/cluster/AnalysisWorker.java @@ -41,6 +41,7 @@ import java.util.Arrays; import java.util.List; import java.util.Random; +import java.util.Set; import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -407,6 +408,10 @@ protected void handleOneRegionalTask (RegionalTask task) throws Throwable { LOG.debug("Handling regional task {}", task.toString()); + // Force dual access results for grid (remove once broker has been relaunched to support custom flags) + LOG.info("Forcing dual access results"); + task.flags = Set.of("gridDualAccess"); + if (task.injectFault != null) { task.injectFault.considerShutdownOrException(task.taskId); if (task.injectFault.shouldDropTaskBeforeCompute(task.taskId)) { diff --git a/src/main/java/com/conveyal/r5/analyst/cluster/AnalysisWorkerTask.java b/src/main/java/com/conveyal/r5/analyst/cluster/AnalysisWorkerTask.java index 2698905fa..59a6936a6 100644 --- a/src/main/java/com/conveyal/r5/analyst/cluster/AnalysisWorkerTask.java +++ b/src/main/java/com/conveyal/r5/analyst/cluster/AnalysisWorkerTask.java @@ -323,4 +323,8 @@ public void validateCutoffsMinutes () { } } + public boolean hasFlag (String flag) { + return this.flags != null && this.flags.contains(flag); + } + } diff --git a/src/main/java/com/conveyal/r5/analyst/cluster/RegionalWorkResult.java b/src/main/java/com/conveyal/r5/analyst/cluster/RegionalWorkResult.java index c606acc0f..96755c7fa 100644 --- a/src/main/java/com/conveyal/r5/analyst/cluster/RegionalWorkResult.java +++ b/src/main/java/com/conveyal/r5/analyst/cluster/RegionalWorkResult.java @@ -63,7 +63,13 @@ public RegionalWorkResult(OneOriginResult result, RegionalTask task) { this.jobId = task.jobId; this.taskId = task.taskId; this.travelTimeValues = result.travelTimes == null ? null : result.travelTimes.values; - this.accessibilityValues = result.accessibility == null ? null : result.accessibility.getIntValues(); + if (result.accessibility == null) { + this.accessibilityValues = null; + } else if (task.hasFlag("gridDualAccess")) { + this.accessibilityValues = result.density.fakeDualAccess(task); + } else { + this.accessibilityValues = result.accessibility.getIntValues(); + } this.pathResult = result.paths == null ? null : result.paths.summarizeIterations(PathResult.Stat.MINIMUM); this.opportunitiesPerMinute = result.density == null ? null : result.density.opportunitiesPerMinute; // TODO checkTravelTimeInvariants, checkAccessibilityInvariants to verify that values are monotonically increasing