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

On-demand experiments #786

Draft
wants to merge 22 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3941742
Reduce car linking limit from 30 min to 20 min.
ansoncfit Jan 27, 2022
010970c
Experimental: serialize bike and car linkages
ansoncfit Jan 27, 2022
71bd15a
set kryo NETWORK_FORMAT_VERSION to previous commit
ansoncfit Jan 27, 2022
1a28824
Increase limit on indexing bins
ansoncfit Jan 31, 2022
860221f
Refactor: dedicated package for on-demand services
ansoncfit Feb 6, 2022
70979b8
Feature: allow direct on-demand service to limited zones
ansoncfit Feb 6, 2022
f5c821b
Save transit shapes
ansoncfit Feb 6, 2022
48797a5
Scale driving speeds by 0.5x
ansoncfit Feb 6, 2022
e57f68f
set kryo NETWORK_FORMAT_VERSION to previous commit
ansoncfit Feb 6, 2022
0cec409
Fix: union destination polygons
ansoncfit Feb 7, 2022
a670dac
Merge branch 'dev' into car-egress-tables
ansoncfit Apr 13, 2022
63d9969
Reset speeds to default
ansoncfit Jul 19, 2023
277a37a
Merge branch 'dev' into car-egress-tables
ansoncfit Jul 19, 2023
eaf53ef
feat(on-demand): allow overlapping pickup zones
ansoncfit Jul 30, 2023
2146977
Allow pickup only for first-/last-mile
ansoncfit Aug 14, 2023
a04dc68
Merge branch 'dev' into car-egress-tables
ansoncfit Feb 22, 2024
a456099
Rename variable for clarity
ansoncfit Feb 28, 2024
b9dab23
Remove unused variables
ansoncfit Feb 28, 2024
c71eb5b
set kryo NETWORK_FORMAT_VERSION to previous commit
ansoncfit Feb 28, 2024
1826c92
debug: disable range-RAPTOR optimization
ansoncfit Apr 3, 2024
d89a645
debug: skip test if range RAPTOR disabled
ansoncfit Apr 3, 2024
163eafa
Merge branch 'dev' into car-egress-tables
ansoncfit Apr 10, 2024
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
40 changes: 24 additions & 16 deletions src/main/java/com/conveyal/r5/analyst/TravelTimeComputer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import com.conveyal.r5.analyst.cluster.PathWriter;
import com.conveyal.r5.analyst.cluster.RegionalTask;
import com.conveyal.r5.analyst.fare.InRoutingFareCalculator;
import com.conveyal.r5.analyst.scenario.PickupWaitTimes;
import com.conveyal.r5.analyst.scenario.ondemand.AccessService;
import com.conveyal.r5.api.util.LegMode;
import com.conveyal.r5.point_to_point.builder.PointToPointQuery;
import com.conveyal.r5.profile.DominatingList;
Expand All @@ -21,16 +21,16 @@
import com.conveyal.r5.transit.TransportNetwork;
import com.conveyal.r5.transit.path.Path;
import gnu.trove.map.TIntIntMap;
import gnu.trove.map.hash.TIntIntHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.EnumSet;
import java.util.function.IntFunction;
import java.util.stream.Collectors;

import static com.conveyal.r5.analyst.scenario.PickupWaitTimes.NO_SERVICE_HERE;
import static com.conveyal.r5.analyst.scenario.PickupWaitTimes.NO_WAIT_ALL_STOPS;
import static com.conveyal.r5.common.Util.isNullOrEmpty;
import static com.conveyal.r5.analyst.scenario.ondemand.AccessService.NO_SERVICE_HERE;
import static com.conveyal.r5.analyst.scenario.ondemand.AccessService.NO_WAIT_ALL_STOPS;
import static com.conveyal.r5.profile.PerTargetPropagater.MM_PER_METER;

/**
Expand Down Expand Up @@ -122,7 +122,7 @@ public OneOriginResult computeTravelTimes() {
LOG.info("Performing street search for mode: {}", accessMode);

// Look up pick-up service for an access leg.
PickupWaitTimes.AccessService accessService =
AccessService accessService =
network.streetLayer.getAccessService(request.fromLat, request.fromLon, accessMode);

// When an on-demand mobility service is defined, it may not be available at this particular location.
Expand Down Expand Up @@ -179,17 +179,22 @@ public OneOriginResult computeTravelTimes() {
}
// Find access times to transit stops, keeping the minimum across all access street modes.
// Note that getReachedStops() returns the routing variable units, not necessarily seconds.
// TODO add logic here if linkedStops are specified in pickupDelay?
TIntIntMap travelTimesToStopsSeconds = sr.getReachedStops();
TIntIntMap adjustedTravelTimesToStopsSeconds = new TIntIntHashMap();
if (accessService != NO_WAIT_ALL_STOPS) {
LOG.info("Delaying transit access times by {} seconds (to wait for {} pick-up).",
accessService.waitTimeSeconds, accessMode);
if (accessService.stopsReachable != null) {
travelTimesToStopsSeconds.retainEntries((k, v) -> accessService.stopsReachable.contains(k));
}
travelTimesToStopsSeconds.transformValues(i -> i + accessService.waitTimeSeconds);
LOG.info("Delaying access times to {} transit stops (to wait for {} pick-up).",
accessService.waitTimesForStops.size(),
accessMode);
accessService.waitTimesForStops.forEachEntry((stop, wait) -> {
if (travelTimesToStopsSeconds.containsKey(stop)) {
adjustedTravelTimesToStopsSeconds.put(stop, wait + travelTimesToStopsSeconds.get(stop));
}
return true;
});
bestAccessOptions.update(adjustedTravelTimesToStopsSeconds, accessMode);
} else {
bestAccessOptions.update(travelTimesToStopsSeconds, accessMode);
}
bestAccessOptions.update(travelTimesToStopsSeconds, accessMode);
}

// Calculate times to reach destinations directly by this street mode, without using transit.
Expand Down Expand Up @@ -225,9 +230,12 @@ public OneOriginResult computeTravelTimes() {
);

if (accessService != NO_WAIT_ALL_STOPS) {
LOG.info("Delaying direct travel times by {} seconds (to wait for {} pick-up).",
accessService.waitTimeSeconds, accessMode);
if (accessService.stopsReachable != null) {
if (accessService.serviceArea != null) {
LOG.info("Delaying on-demand service by {} seconds (to wait for {} pick-up).",
accessService.waitTimeSeconds, accessMode);
pointSetTimes.incrementWithinAndClip(accessService.serviceArea, accessService.waitTimeSeconds);
}
else if (accessService.stops != null || !accessService.waitTimesForStops.isEmpty()) {
// Disallow direct travel to destination if pickupDelay zones are associated with stops.
pointSetTimes = PointSetTimes.allUnreached(destinations);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,15 @@ public ModificationPolygon getWinningPolygon (Geometry geometry) {
return winner;
}

public List<ModificationPolygon> getIntersectingPolygons (Geometry geometry) {
List<ModificationPolygon> polygons = new ArrayList<ModificationPolygon>();
Envelope envelope = geometry.getEnvelopeInternal();
List<ModificationPolygon> candidatePolygons = polygonSpatialIndex.query(envelope);
for (ModificationPolygon candidate : candidatePolygons) {
if (candidate.polygonal.intersects(geometry)) {
polygons.add(candidate);
}
}
return polygons;
}
}
132 changes: 74 additions & 58 deletions src/main/java/com/conveyal/r5/analyst/scenario/PickupDelay.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.conveyal.r5.analyst.scenario;

import com.conveyal.r5.analyst.scenario.ondemand.EgressService;
import com.conveyal.r5.profile.StreetMode;
import com.conveyal.r5.transit.TransportNetwork;
import com.conveyal.r5.util.ExceptionUtils;
Expand Down Expand Up @@ -33,18 +34,10 @@ public class PickupDelay extends Modification {
/**
* The identifier of the polygon layer containing the on-demand pick-up zones.
* This set of polygons represents areas where an on-demand mobility service will pick up passengers, with
* associated wait times to be picked up. Overlapping zones are not yet supported, only one "winning" zone will
* be chosen using the priority values.
* associated wait times to be picked up.
*/
public String zonePolygons;

/**
* The identifier of the polygon layer containing the stops associated with the zones, if any.
* Having two different polygon files raises questions about whether the attribute names can be set separately,
* and how we would factor out that aspect of the polygon loading code.
*/
// public String stopPolygons;

/**
* The name of the attribute (floating-point) within the polygon layer that contains the pick-up wait time
* (in minutes). Negative waiting times mean the area is not served at all.
Expand All @@ -65,9 +58,17 @@ public class PickupDelay extends Modification {
public String idAttribute = "id";

/**
* A JSON map from polygon IDs to lists of polygon IDs. If any stop_id is specified for a polygon, service is
* only allowed between the polygon and the stops (i.e. no direct trips). If no stop_ids are specified,
* passengers boarding an on-demand service in a pick-up zone should be able to alight anywhere.
* A JSON map from polygon IDs to lists of polygon IDs, representing origin zones and allowable destination zones
* for direct legs. For example, "a":["b","c"] allows passengers to travel from zone a to destinations in zones
* b and c. For now, this does not enable use of stops in zones b and c for onward travel, so in most cases,
* these zones should be explicitly repeated in stopsForZone.
*/
public Map<String, Set<String>> destinationsForZone;

/**
* A JSON map from polygon IDs to lists of polygon IDs, representing origin zones and allowable boarding stops
* for access legs. For example "a":["d","e"] allows passengers to travel from zone a to transit stops in zones d
* and e.
*/
public Map<String, Set<String>> stopsForZone;

Expand All @@ -88,12 +89,6 @@ public class PickupDelay extends Modification {
/** The result of resolving this modification against the TransportNetwork. */
private transient PickupWaitTimes pickupWaitTimes;

/**
* These polygons contain the stops served by the zones. Wait times are also loaded for these polygons and will be
* used on the egress end of the itinerary.
*/
//private transient IndexedPolygonCollection stopPolygons;

// Implementations of methods for the Modification interface

@Override
Expand All @@ -113,58 +108,71 @@ public boolean resolve (TransportNetwork network) {
// Collect any errors from the IndexedPolygonCollection construction, so they can be seen in the UI.
addErrors(polygons.getErrors());
// Handle pickup service to stop mapping if supplied in the modification JSON.
if (stopsForZone == null) {
this.pickupWaitTimes = new PickupWaitTimes(polygons, null, Collections.emptySet(), this.streetMode);
if (stopsForZone == null && destinationsForZone == null) {
this.pickupWaitTimes = new PickupWaitTimes(
polygons,
null,
null,
Collections.emptySet(),
this.streetMode
);
} else {
// Iterate over all zone-stop mappings and resolve them against the network.
// Iterate over all zone-zone mappings and resolve them against the network.
// Because they are used in lambda functions, these variables must be final and non-null.
// That is why there's a separate PickupWaitTimes constructor call above with a null parameter.
final Map<ModificationPolygon, TIntSet> stopNumbersForZonePolygon = new HashMap<>();
final Map<ModificationPolygon, PickupWaitTimes.EgressService> egressServices = new HashMap<>();
if (stopsForZone.isEmpty()) {
addError("If stopsForZone is specified, it must be non-empty.");
}
stopsForZone.forEach((zonePolygonId, stopPolygonIds) -> {
ModificationPolygon zonePolygon = polygons.getById(zonePolygonId);
if (zonePolygon == null) {
addError("Could not find zone polygon with ID: " + zonePolygonId);
}
TIntSet stopNumbers = stopNumbersForZonePolygon.get(zonePolygon);
if (stopNumbers == null) {
stopNumbers = new TIntHashSet();
stopNumbersForZonePolygon.put(zonePolygon, stopNumbers);
}
for (String stopPolygonId : stopPolygonIds) {
ModificationPolygon stopPolygon = polygons.getById(stopPolygonId);
if (stopPolygon == null) {
addError("Could not find stop polygon with ID: " + stopPolygonId);
final Map<ModificationPolygon, Geometry> destinationsForZonePolygon = new HashMap<>();
final Map<ModificationPolygon, EgressService> egressServices = new HashMap<>();
if (destinationsForZone != null) {
destinationsForZone.forEach((zonePolygonId, destinationPolygonIds) -> {
ModificationPolygon zonePolygon = tryToGetPolygon(polygons, zonePolygonId, "zone");
for (String id : destinationPolygonIds) {
ModificationPolygon destinationPolygon = tryToGetPolygon(polygons, id, "destination");
Geometry polygon = destinationsForZonePolygon.get(zonePolygon) == null ?
destinationPolygon.polygonal :
destinationsForZonePolygon.get(zonePolygon).union(destinationPolygon.polygonal);
destinationsForZonePolygon.put(zonePolygon, polygon);
}
TIntSet stops = network.transitLayer.findStopsInGeometry(stopPolygon.polygonal);
if (stops.isEmpty()) {
addError("Stop polygon did not contain any stops: " + stopPolygonId);
});
}
if (stopsForZone != null) {
stopsForZone.forEach((zonePolygonId, stopPolygonIds) -> {
ModificationPolygon zonePolygon = tryToGetPolygon(polygons, zonePolygonId, "zone");
TIntSet stopNumbers = stopNumbersForZonePolygon.get(zonePolygon);
if (stopNumbers == null) {
stopNumbers = new TIntHashSet();
stopNumbersForZonePolygon.put(zonePolygon, stopNumbers);
}
stopNumbers.addAll(stops);
// Derive egress services from this pair of polygons
double egressWaitMinutes = stopPolygon.data;
if (egressWaitMinutes >= 0) {
// This stop polygon can be used on the egress end of a trip.
int egressWaitSeconds = (int) (egressWaitMinutes * 60);
Geometry serviceArea = zonePolygon.polygonal;
PickupWaitTimes.EgressService egressService = egressServices.get(stopPolygon);
if (egressService != null) {
// Merge service are with any other service polygons associated with this stop polygon.
serviceArea = serviceArea.union(egressService.serviceArea);
for (String stopPolygonId : stopPolygonIds) {
ModificationPolygon stopPolygon = tryToGetPolygon(polygons, stopPolygonId, "stop");
TIntSet stops = network.transitLayer.findStopsInGeometry(stopPolygon.polygonal);
if (stops.isEmpty()) {
errors.add("Stop polygon did not contain any stops: " + stopPolygonId);
}
stopNumbers.addAll(stops);
// Derive egress services from this pair of polygons
double egressWaitMinutes = stopPolygon.data;
if (egressWaitMinutes >= 0) {
// This stop polygon can be used on the egress end of a trip.
int egressWaitSeconds = (int) (egressWaitMinutes * 60);
Geometry serviceArea = zonePolygon.polygonal;
EgressService egressService = egressServices.get(stopPolygon);
if (egressService != null) {
// Merge service area with any other service polygons associated with this stop polygon.
serviceArea = serviceArea.union(egressService.serviceArea);
}
egressService = new EgressService(egressWaitSeconds, stops, serviceArea);
egressServices.put(stopPolygon, egressService);
}
egressService = new PickupWaitTimes.EgressService(egressWaitSeconds, stops, serviceArea);
egressServices.put(stopPolygon, egressService);
}
}
});

});
}
// TODO filter out polygons that aren't keys in stopsForZone using new IndexedPolygonCollection constructor
// egress wait times for stop numbers
this.pickupWaitTimes = new PickupWaitTimes(
polygons,
stopNumbersForZonePolygon,
destinationsForZonePolygon,
egressServices.values(),
this.streetMode
);
Expand All @@ -176,6 +184,14 @@ public boolean resolve (TransportNetwork network) {
return hasErrors();
}

private ModificationPolygon tryToGetPolygon (IndexedPolygonCollection polygons, String id, String label) {
ModificationPolygon polygon = polygons.getById(id);
if (polygon == null) {
errors.add("Could not find " + label + " polygon with ID: " + id);
}
return polygon;
}

@Override
public boolean apply (TransportNetwork network) {
// network.streetLayer is already a protective copy made by method Scenario.applyToTransportNetwork.
Expand Down
Loading
Loading