Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use regional tasks in Simpson Desert tests #908

Merged
merged 1 commit into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,9 @@ public OneOriginResult computeTravelTimes() {
) {
// Freeform destinations. Destination PointSet was set by handleOneRequest in the main AnalystWorker.
destinations = request.destinationPointSets[0];
} else if (!isNullOrEmpty(request.destinationPointSets)) {
LOG.warn("ONLY VALID IN TESTING: Using PointSet object embedded in request outside regional analysis.");
destinations = request.destinationPointSets[0];
} else {
// Gridded (non-freeform) destinations. This method finds them differently for regional and single requests.
// We don't support freeform destinations for single point requests, as they must also return gridded travel times.
WebMercatorExtents destinationGridExtents = request.getWebMercatorExtents();
// Make a WebMercatorGridPointSet with the right extents, referring to the network's base grid and linkage.
destinations = AnalysisWorkerTask.gridPointSetCache.get(destinationGridExtents, network.fullExtentGridPointSet);
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/conveyal/r5/analyst/TravelTimeReducer.java
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,8 @@ public OneOriginResult finish () {
/**
* Sanity check: all opportunity data sets should have the same size and location as the points to which we'll
* calculate travel times. They will only be used if we're calculating accessibility.
* TODO: expand to cover all cases where destinationPointSets are present, not just accessibility to Mercator grids.
* Need to verify first: should this be skipped for one-to-one regional requests on FreeFormPointSets?
*/
public void checkOpportunityExtents (PointSet travelTimePointSet) {
if (calculateAccessibility) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ public class RegionalTask extends AnalysisWorkerTask implements Cloneable {
public boolean oneToOne = false;

/**
* Whether to record travel times between origins and destinations
* Whether to record travel times between origins and destinations. This is done automatically for
* TravelTimeSurfaceTask (single point tasks) but must be manually enabled on RegionalTasks using this field.
*/
public boolean recordTimes;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,22 +60,23 @@ public WebMercatorExtents getWebMercatorExtents() {

@Override
public int nTargetsPerOrigin () {
// In TravelTimeSurfaceTasks, the set of destinations is always determined by the web mercator extents in the
// request. A single WebMercatorGridPointSet is created with those extents. A single small freeform PointSet may
// also be present, but only in testing situations. The checkState assertions serve to verify assumptions that
// destinationPointSets are always set and we can polymorphically fetch the number of items for all normal and
// testing use cases. Once that is clearly working the assertions could be removed.
checkState(!isNullOrEmpty(destinationPointSets), "Expected destination PointSets to be present.");
checkState(destinationPointSets.length == 1, "Expected a single destination PointSet in TravelTimeSurfaceTask.");
PointSet destinations = destinationPointSets[0];
int nFeatures = destinations.featureCount();
if (destinations instanceof FreeFormPointSet) {
LOG.warn("Should currently be used only in testing: FreeFormPointSet specified in TravelTimeSurfaceTask.");
checkState(nFeatures == 1);
} else {
checkState(nFeatures == width * height);
// In TravelTimeSurfaceTasks (single-point or single-origin tasks), the set of destinations is always
// determined by the web mercator extents in the request itself. In many cases the destinationPointSets field
// is empty, but when destinationPointSets are present (when we're calculating accessibility), those PointSets
// are required to be gridded and exactly match the task extents (e.g. Grids wrapped in GridTransformWrapper).
// In normal usage this requirement is validated by TravelTimeReducer#checkOpportunityExtents but that function
// only checks when we're computing accessibility, and is only called when destinations are gridded. The
// assertions here serve to further verify this requirement, as we've seen it violated before in tests.
final int nTargets = width * height;
if (destinationPointSets != null) {
for (PointSet pointSet : destinationPointSets) {
checkState(
pointSet.featureCount() == nTargets,
"Destination PointSet expected to exactly match extents embedded in single point task."
);
}
}
return nFeatures;
return nTargets;
}

}
10 changes: 2 additions & 8 deletions src/test/java/com/conveyal/r5/analyst/network/GridLayout.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

import com.conveyal.gtfs.GTFSFeed;
import com.conveyal.osmlib.OSM;
import com.conveyal.r5.analyst.Grid;
import com.conveyal.r5.analyst.WebMercatorGridPointSet;
import com.conveyal.r5.analyst.cluster.AnalysisWorkerTask;
import com.conveyal.r5.common.SphericalDistanceLibrary;
import com.conveyal.r5.profile.StreetMode;
import com.conveyal.r5.transit.TransportNetwork;
Expand All @@ -13,13 +10,10 @@
import org.locationtech.jts.geom.Envelope;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;

import static com.conveyal.r5.analyst.WebMercatorExtents.DEFAULT_ZOOM;

/**
* This is used in testing, to represent and create gridded transport systems with very regular spacing of roads and
* transit stops, yielding highly predictable travel times that can be tested against actual output from the router.
Expand Down Expand Up @@ -165,8 +159,8 @@ public void addVerticalFrequencyRoute (int col, int headwayMinutes) {
}

/** Creates a builder for analysis worker tasks, which represent searches on this grid network. */
public GridSinglePointTaskBuilder newTaskBuilder() {
return new GridSinglePointTaskBuilder(this);
public GridRegionalTaskBuilder newTaskBuilder() {
return new GridRegionalTaskBuilder(this);
}

/** Get the minimum envelope containing all the points in this grid. */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
package com.conveyal.r5.analyst.network;

import com.conveyal.r5.analyst.FreeFormPointSet;
import com.conveyal.r5.analyst.Grid;
import com.conveyal.r5.analyst.PointSet;
import com.conveyal.r5.analyst.WebMercatorExtents;
import com.conveyal.r5.analyst.cluster.AnalysisWorkerTask;
import com.conveyal.r5.analyst.cluster.TravelTimeSurfaceTask;
import com.conveyal.r5.analyst.cluster.RegionalTask;
import com.conveyal.r5.analyst.decay.StepDecayFunction;
import com.conveyal.r5.api.util.LegMode;
import com.conveyal.r5.api.util.TransitModes;
Expand All @@ -20,21 +19,25 @@
import static com.conveyal.r5.analyst.network.GridGtfsGenerator.WEEKEND_DATE;

/**
* This creates a task for use in tests. It uses a builder pattern but for a non-immutable task object.
* It provides convenience methods to set all the necessary fields. This builder may be reused to produce
* several tasks in a row with different settings, but only use the most recently produced one at any time.
* See build() for further explanation.
* This creates a task for use in tests. It uses a builder pattern but modifies a non-immutable task object. It
* provides convenience methods to set all the necessary fields. This builder may be reused to produce several tasks in
* a row with different settings, but only use the most recently produced one at any time. See build() for further
* explanation. We want to use a limited number of destinations at exact points instead of Mercator gridded
* destinations, which would not be exactly aligned with the desert grid. Therefore we create regional tasks rather
* than single-point TravelTimeSurfaceTasks because single-point tasks always have gridded destinations (they always
* return gridded travel times which must be exactly aligned with any accessibility destinations).
*/
public class GridSinglePointTaskBuilder {
public class GridRegionalTaskBuilder {

public static final int DEFAULT_MONTE_CARLO_DRAWS = 4800; // 40 per minute over a two hour window.
private final GridLayout gridLayout;
private final AnalysisWorkerTask task;

public GridSinglePointTaskBuilder (GridLayout gridLayout) {
private final RegionalTask task;

public GridRegionalTaskBuilder(GridLayout gridLayout) {
this.gridLayout = gridLayout;
// We will accumulate settings into this task.
task = new TravelTimeSurfaceTask();
task = new RegionalTask();
task.date = WEEKDAY_DATE;
// Set defaults that can be overridden by calling builder methods.
task.accessModes = EnumSet.of(LegMode.WALK);
Expand All @@ -52,10 +55,11 @@ public GridSinglePointTaskBuilder (GridLayout gridLayout) {
task.monteCarloDraws = DEFAULT_MONTE_CARLO_DRAWS;
// By default, traverse one block in a round predictable number of seconds.
task.walkSpeed = gridLayout.streetGridSpacingMeters / gridLayout.walkBlockTraversalTimeSeconds;
// Unlike single point tasks, travel time recording must be enabled manually on regional tasks.
task.recordTimes = true;
// Record more detailed information to allow comparison to theoretical travel time distributions.
task.recordTravelTimeHistograms = true;
// Set the destination grid extents on the task, otherwise if no freeform PointSet is specified, the task will fail
// checks on the grid dimensions and zoom level.
// Set the grid extents on the task, otherwise the task will fail checks on the grid dimensions and zoom level.
WebMercatorExtents extents = WebMercatorExtents.forWgsEnvelope(gridLayout.gridEnvelope(), DEFAULT_ZOOM);
task.zoom = extents.zoom;
task.north = extents.north;
Expand All @@ -64,38 +68,38 @@ public GridSinglePointTaskBuilder (GridLayout gridLayout) {
task.height = extents.height;
}

public GridSinglePointTaskBuilder setOrigin (int gridX, int gridY) {
public GridRegionalTaskBuilder setOrigin (int gridX, int gridY) {
Coordinate origin = gridLayout.getIntersectionLatLon(gridX, gridY);
task.fromLat = origin.y;
task.fromLon = origin.x;
return this;
}

public GridSinglePointTaskBuilder weekdayMorningPeak () {
public GridRegionalTaskBuilder weekdayMorningPeak () {
task.date = WEEKDAY_DATE;
morningPeak();
return this;
}

public GridSinglePointTaskBuilder weekendMorningPeak () {
public GridRegionalTaskBuilder weekendMorningPeak () {
task.date = WEEKEND_DATE;
morningPeak();
return this;
}

public GridSinglePointTaskBuilder morningPeak () {
public GridRegionalTaskBuilder morningPeak () {
task.fromTime = LocalTime.of(7, 00).toSecondOfDay();
task.toTime = LocalTime.of(9, 00).toSecondOfDay();
return this;
}

public GridSinglePointTaskBuilder departureTimeWindow(int startHour, int startMinute, int durationMinutes) {
public GridRegionalTaskBuilder departureTimeWindow(int startHour, int startMinute, int durationMinutes) {
task.fromTime = LocalTime.of(startHour, startMinute).toSecondOfDay();
task.toTime = LocalTime.of(startHour, startMinute + durationMinutes).toSecondOfDay();
return this;
}

public GridSinglePointTaskBuilder maxRides(int rides) {
public GridRegionalTaskBuilder maxRides(int rides) {
task.maxRides = rides;
return this;
}
Expand All @@ -105,7 +109,7 @@ public GridSinglePointTaskBuilder maxRides(int rides) {
* Increasing the number of draws will yield a better approximation of the true travel time distribution
* (while making the tests run slower).
*/
public GridSinglePointTaskBuilder monteCarloDraws (int draws) {
public GridRegionalTaskBuilder monteCarloDraws (int draws) {
task.monteCarloDraws = draws;
return this;
}
Expand All @@ -120,7 +124,7 @@ public GridSinglePointTaskBuilder monteCarloDraws (int draws) {
* web Mercator grid pixels. Using a single measurement point also greatly reduces the amount of travel time
* histograms that must be computed and retained, improving the memory and run time cost of tests.
*/
public GridSinglePointTaskBuilder singleFreeformDestination(int x, int y) {
public GridRegionalTaskBuilder singleFreeformDestination(int x, int y) {
FreeFormPointSet ps = new FreeFormPointSet(gridLayout.getIntersectionLatLon(x, y));
// Downstream code expects to see the same number of keys and PointSet objects so initialize both.
task.destinationPointSetKeys = new String[] { "POINT_SET" };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
* version to the next of R5 (a form of snapshot testing) this checks that they match theoretically expected travel
* times given headways, transfer points, distances, common trunks and competing lines, etc.
*
* Originally we were using web Mercator gridded destinations, as this was the only option in single point analyses.
* Originally we were using web Mercator gridded destinations, as this was the only option in single point tasks.
* Because these tests record travel time distributions at the destinations using a large number of Monte Carlo draws,
* this was doing a lot of work and storing a lot of data for up to thousands of destinations we weren't actually using.
* A testing code path now exists to measure travel times to one or more freeform destinations in single point mode.
* However, it is still possible to measure times to the whole grid if singleFreeformDestination is not called.
* Regional tasks with freeform destinations are now used to measure travel times to a limited number of points.
* This could be extended to use gridded destination PointSets for future tests.
*/
public class SimpsonDesertTests {

Expand Down Expand Up @@ -64,8 +64,7 @@ public void testGridScheduled () throws Exception {
// always 10 minutes. So scheduled range is expected to be 1 minute slack, 0-20 minutes wait, 10 minutes ride,
// 10 minutes wait, 10 minutes ride, giving 31 to 51 minutes.
// This estimation logic could be better codified as something like TravelTimeEstimate.waitWithHeadaway(20) etc.

// TODO For some reason observed is off by 1 minute, figure out why.
// TODO: For some reason observed is dealyed by 1 minute. Figure out why, perhaps due to integer minute binning.
Distribution expected = new Distribution(31, 20).delay(1);
expected.multiAssertSimilar(oneOriginResult.travelTimes, 0);
}
Expand Down Expand Up @@ -179,7 +178,7 @@ public void testOvertakingCases () throws Exception {
TransportNetwork network = gridLayout.generateNetwork();

// 0. Reuse this task builder to produce several tasks. See caveats on build() method.
GridSinglePointTaskBuilder taskBuilder = gridLayout.newTaskBuilder()
GridRegionalTaskBuilder taskBuilder = gridLayout.newTaskBuilder()
.departureTimeWindow(7, 0, 5)
.maxRides(1)
.setOrigin(30, 50)
Expand Down