diff --git a/src/main/java/com/conveyal/analysis/models/AnalysisRequest.java b/src/main/java/com/conveyal/analysis/models/AnalysisRequest.java index 10d326656..82f0676a6 100644 --- a/src/main/java/com/conveyal/analysis/models/AnalysisRequest.java +++ b/src/main/java/com/conveyal/analysis/models/AnalysisRequest.java @@ -161,6 +161,16 @@ public class AnalysisRequest { */ public ChaosParameters injectFault; + /** + * Whether to include the number of opportunities reached during each minute of travel in results sent back + * to the broker. Requires both an origin and destination pointset to be specified, and in the case of regional + * analyses the origins must be non-gridded. (Should be possible to make a grid as well.) + */ + public boolean opportunityTemporalDensity = false; + + public int dualAccessibilityOpportunityThreshold = 0; + + /** * Create the R5 `Scenario` from this request. */ @@ -265,6 +275,9 @@ public void populateTask (AnalysisWorkerTask task, UserPermissions userPermissio throw new IllegalArgumentException("Must be admin user to inject faults."); } } + + task.opportunityTemporalDensity = opportunityTemporalDensity; + task.dualAccessibilityOpportunityThreshold = dualAccessibilityOpportunityThreshold; } private EnumSet getEnumSetFromString (String s) { diff --git a/src/main/java/com/conveyal/analysis/results/CsvResultType.java b/src/main/java/com/conveyal/analysis/results/CsvResultType.java index cdf60e410..56612aafc 100644 --- a/src/main/java/com/conveyal/analysis/results/CsvResultType.java +++ b/src/main/java/com/conveyal/analysis/results/CsvResultType.java @@ -5,5 +5,5 @@ * do serve to enumerate the acceptable parameters coming over the HTTP API. */ public enum CsvResultType { - ACCESS, TIMES, PATHS + ACCESS, TIMES, PATHS, OPPORTUNITIES } diff --git a/src/main/java/com/conveyal/analysis/results/MultiOriginAssembler.java b/src/main/java/com/conveyal/analysis/results/MultiOriginAssembler.java index 3bbd5915f..9dcce90e3 100644 --- a/src/main/java/com/conveyal/analysis/results/MultiOriginAssembler.java +++ b/src/main/java/com/conveyal/analysis/results/MultiOriginAssembler.java @@ -123,6 +123,10 @@ public MultiOriginAssembler (RegionalAnalysis regionalAnalysis, Job job, FileSto resultWriters.add(new PathCsvResultWriter(job.templateTask, fileStorage)); } + if (job.templateTask.opportunityTemporalDensity) { + resultWriters.add(new OpportunityCsvResultWriter(job.templateTask, fileStorage)); + } + checkArgument(job.templateTask.makeTauiSite || notNullOrEmpty(resultWriters), "A non-Taui regional analysis should always create at least one grid or CSV file."); diff --git a/src/main/java/com/conveyal/analysis/results/OpportunityCsvResultWriter.java b/src/main/java/com/conveyal/analysis/results/OpportunityCsvResultWriter.java new file mode 100644 index 000000000..dfe741602 --- /dev/null +++ b/src/main/java/com/conveyal/analysis/results/OpportunityCsvResultWriter.java @@ -0,0 +1,90 @@ +package com.conveyal.analysis.results; + +import com.conveyal.file.FileStorage; +import com.conveyal.r5.analyst.cluster.RegionalTask; +import com.conveyal.r5.analyst.cluster.RegionalWorkResult; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * This handles collating regional results into CSV files containing temporal opportunity density + * (number of opportunities reached in each one-minute interval, the derivative of step-function accessibility) + * as well as "dual" accessibility (the amount of time needed to reach n opportunities). + * And maybe the N closest opportunities to each origin? + */ +public class OpportunityCsvResultWriter extends CsvResultWriter { + + private final int dualOpportunityCount; + + public OpportunityCsvResultWriter(RegionalTask task, FileStorage fileStorage) throws IOException { + super(task, fileStorage); + dualOpportunityCount = task.dualAccessibilityOpportunityThreshold; + } + + @Override + public CsvResultType resultType () { + return CsvResultType.OPPORTUNITIES; + } + + @Override + public String[] columnHeaders () { + List headers = new ArrayList<>(); + // The ids of the freeform origin point and destination set + headers.add("originId"); + headers.add("destId"); + headers.add("percentile"); + for (int m = 0; m < 120; m += 1) { + // The opportunity density over travel minute m + headers.add(Integer.toString(m)); + } + // The number of minutes needed to reach d destination opportunities + headers.add("D" + dualOpportunityCount); + return headers.toArray(new String[0]); + } + + @Override + protected void checkDimension (RegionalWorkResult workResult) { + checkDimension(workResult, "destination pointsets", workResult.opportunitiesPerMinute.length, task.destinationPointSetKeys.length); + for (double[][] percentilesForPointset : workResult.opportunitiesPerMinute) { + checkDimension(workResult, "percentiles", percentilesForPointset.length, task.percentiles.length); + for (double[] minutesForPercentile : percentilesForPointset) { + checkDimension(workResult, "minutes", minutesForPercentile.length, 120); + } + } + } + + @Override + public Iterable rowValues (RegionalWorkResult workResult) { + List row = new ArrayList<>(125); + String originId = task.originPointSet.getId(workResult.taskId); + for (int d = 0; d < task.destinationPointSetKeys.length; d++) { + int[][] percentilesForDestPointset = workResult.accessibilityValues[d]; + for (int p = 0; p < task.percentiles.length; p++) { + row.add(originId); + row.add(task.destinationPointSets[d].name); + row.add(Integer.toString(p)); + int[] densitiesPerMinute = percentilesForDestPointset[p]; + for (int m = 0; m < 120; m++) { + row.add(Double.toString(densitiesPerMinute[m])); + } + // Dual accessibility + { + int m = 0; + double sum = 0; + while (sum < dualOpportunityCount) { + sum += densitiesPerMinute[m]; + m += 1; + } + row.add(Integer.toString(m >= 120 ? -1 : m)); + } + } + } + // List.of() or Arrays.asList() don't work without explicitly specifying the generic type because + // they interpret the String[] as varargs in the method signature. + return List.of(row.toArray(new String[0])); + } + +} diff --git a/src/main/java/com/conveyal/r5/analyst/NearestNResult.java b/src/main/java/com/conveyal/r5/analyst/NearestNResult.java index 0237ded76..7b02ffef7 100644 --- a/src/main/java/com/conveyal/r5/analyst/NearestNResult.java +++ b/src/main/java/com/conveyal/r5/analyst/NearestNResult.java @@ -3,8 +3,8 @@ import com.conveyal.r5.analyst.cluster.AnalysisWorkerTask; import com.google.common.base.Preconditions; -import static com.conveyal.r5.common.Util.newObjectArray; import static com.conveyal.r5.common.Util.notNullOrEmpty; +import static com.conveyal.r5.profile.FastRaptorWorker.UNREACHED; /** * An instance of this is included in a OneOriginResult for reporting the nearest N destinations. If we use more than @@ -15,6 +15,9 @@ * function, how many opportunities are encountered during each minute of travel, whose integral is the cumulative * accessibility curve); and the nearest one or more opportunities to a given origin. * (expand from comments on https://github.com/conveyal/r5/pull/884) + * + * Corresponds to OpportunityCsvResultWriter when collating regional results, and + * AnalysisWorkerTask#opportunityTemporalDensity to enable, so maybe the names should be made more coherent. */ public class NearestNResult { @@ -66,14 +69,17 @@ public NearestNResult (AnalysisWorkerTask task) { private int listLength = 0; // increment as lists grow in length; use as initial insert position - public void record (int target, int[] travelTimePercentilesSeconds) { + public void recordOneTarget (int target, int[] travelTimePercentilesSeconds) { // Increment histogram bin for the number of minutes of travel by the number of opportunities at the target. for (int d = 0; d < destinationPointSets.length; d++) { PointSet dps = destinationPointSets[d]; for (int p = 0; p < nPercentiles; p++) { - int i = travelTimePercentilesSeconds[p] / 60; - if (i <= 120) { - opportunitiesPerMinute[d][p][i] += dps.getOpportunityCount(target); + if (travelTimePercentilesSeconds[p] == UNREACHED) { + break; // If any percentile is unreached, all higher ones are also unreached. + } + int m = travelTimePercentilesSeconds[p] / 60; + if (m <= 120) { + opportunitiesPerMinute[d][p][m] += dps.getOpportunityCount(target); } } } @@ -113,7 +119,7 @@ public void record (int target, int[] travelTimePercentilesSeconds) { * @param n the threshold quantity of opportunities * @return the number of minutes it takes to reach n opportunities, for each destination set and percentile of travel time. */ - public int[][] minutesToReachOpporunities (int n) { + public int[][] minutesToReachOpportunities(int n) { int[][] result = new int[destinationPointSets.length][nPercentiles]; for (int d = 0; d < destinationPointSets.length; d++) { for (int p = 0; p < nPercentiles; p++) { diff --git a/src/main/java/com/conveyal/r5/analyst/TravelTimeReducer.java b/src/main/java/com/conveyal/r5/analyst/TravelTimeReducer.java index 990ad0aff..5cb5b5185 100644 --- a/src/main/java/com/conveyal/r5/analyst/TravelTimeReducer.java +++ b/src/main/java/com/conveyal/r5/analyst/TravelTimeReducer.java @@ -138,8 +138,6 @@ public TravelTimeReducer (AnalysisWorkerTask task, TransportNetwork network) { // These are conditionally instantiated because they can consume a lot of memory. if (calculateAccessibility) { accessibilityResult = new AccessibilityResult(task); - // TODO create this more selectively, choose histograms and nearest n independently - nearestNResult = new NearestNResult(task); } if (calculateTravelTimes) { travelTimeResult = new TravelTimeResult(task); @@ -147,6 +145,9 @@ public TravelTimeReducer (AnalysisWorkerTask task, TransportNetwork network) { if (task.includePathResults) { pathResult = new PathResult(task, network.transitLayer); } + if (task.opportunityTemporalDensity) { + nearestNResult = new NearestNResult(task); + } // Validate and copy the travel time cutoffs, converting them to seconds to avoid repeated multiplication // in tight loops. Also find the points where the decay function reaches zero for these cutoffs. @@ -279,7 +280,7 @@ private void recordTravelTimePercentilesForTarget (int target, int[] travelTimeP } } if (nearestNResult != null) { - nearestNResult.record(target, travelTimePercentilesSeconds); + nearestNResult.recordOneTarget(target, travelTimePercentilesSeconds); } } 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 5be3ddcf8..7d951a9a0 100644 --- a/src/main/java/com/conveyal/r5/analyst/cluster/AnalysisWorker.java +++ b/src/main/java/com/conveyal/r5/analyst/cluster/AnalysisWorker.java @@ -540,7 +540,6 @@ public static void addJsonToGrid ( if (oneOriginResult.nearest != null) { jsonBlock.nearby = oneOriginResult.nearest.nearby; jsonBlock.opportunitiesPerMinute = oneOriginResult.nearest.opportunitiesPerMinute; - LOG.info("Dual accessibility: {}", oneOriginResult.nearest.minutesToReachOpporunities(272_500)); LOG.info("Opportunities per minute: {}", oneOriginResult.nearest.opportunitiesPerMinute); LOG.info("Opportunities nearby: {}", oneOriginResult.nearest.nearby); } 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 d1e88f78c..547b4123f 100644 --- a/src/main/java/com/conveyal/r5/analyst/cluster/AnalysisWorkerTask.java +++ b/src/main/java/com/conveyal/r5/analyst/cluster/AnalysisWorkerTask.java @@ -99,6 +99,15 @@ public abstract class AnalysisWorkerTask extends ProfileRequest { */ public boolean includePathResults = false; + /** + * Whether to include the number of opportunities reached during each minute of travel in results sent back + * to the broker. Requires both an origin and destination pointset to be specified, and in the case of regional + * analyses the origins must be non-gridded. (Should be possible to make a grid as well.) + */ + public boolean opportunityTemporalDensity = false; + + public int dualAccessibilityOpportunityThreshold = 0; + /** Whether to build a histogram of travel times to each destination, generally used in testing and debugging. */ public boolean recordTravelTimeHistograms = false; 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 4b6dd5d4e..5fa619c1a 100644 --- a/src/main/java/com/conveyal/r5/analyst/cluster/RegionalWorkResult.java +++ b/src/main/java/com/conveyal/r5/analyst/cluster/RegionalWorkResult.java @@ -45,8 +45,7 @@ public class RegionalWorkResult { public NearbyOpportunity[][] nearby; /** - * The nearest n destinations for each percentile of travel time. - * Each item contains a travel time, target index, and ID. + * The temporal density of opportunities - how many are reached during each minute of travel. */ public double[][][] opportunitiesPerMinute;