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 times from transfers.txt #641 #642

Draft
wants to merge 6 commits into
base: dev
Choose a base branch
from
Draft
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
9 changes: 9 additions & 0 deletions src/main/java/com/conveyal/r5/common/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Collection;
import java.util.Arrays;
import java.util.Map;

public abstract class Util {

Expand Down Expand Up @@ -34,6 +35,14 @@ public static boolean notNullOrEmpty (Collection collection) {
return !isNullOrEmpty(collection);
}

public static boolean isNullOrEmpty (Map map) {
return map == null || map.isEmpty();
}

public static boolean notNullOrEmpty (Map map) {
return !isNullOrEmpty(map);
}

public static <T> boolean isNullOrEmpty (T[] array) {
return array == null || array.length == 0;
}
Expand Down
39 changes: 36 additions & 3 deletions src/main/java/com/conveyal/r5/profile/FastRaptorWorker.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public class FastRaptorWorker {

/**
* Minimum time between alighting from one vehicle and boarding another, in seconds.
* TODO make this configurable, and use loop-transfers from transfers.txt.
* FIXME strangely this appears to only be used in classes for displaying paths, not for routing.
* Apply when finding soonest viable departure.
*/
public static final int BOARD_SLACK_SECONDS = 60;

Expand Down Expand Up @@ -369,7 +370,7 @@ private int[][] runRaptorForDepartureMinute (int departureTime) {
// The transfer step can be skipped in the last round.
if (round < request.maxRides) {
raptorTimer.scheduledSearchTransfers.start();
doTransfers(scheduleState[round]);
doTransfersRefactored(scheduleState[round]);
raptorTimer.scheduledSearchTransfers.stop();
}
}
Expand Down Expand Up @@ -411,7 +412,7 @@ private int[][] runRaptorForDepartureMinute (int departureTime) {
if (round < request.maxRides) {
// Transfers not needed after last round
raptorTimer.frequencySearchTransfers.start();
doTransfers(frequencyState[round]);
doTransfersRefactored(frequencyState[round]);
raptorTimer.frequencySearchTransfers.stop();
}

Expand Down Expand Up @@ -877,6 +878,38 @@ private void doTransfers (RaptorState state) {
}
}

/**
* This has a few differences: staying at the stop is treated as a "loop transfer", which can simplify the algorithm
* and allows imposing minimum transfer times when waiting for another vehicle at the same stop.
* For now we're only applying minimum transfer times as an exhaustive list of available transfers, but eventually
* we'll want to merge them with transfers found through the OSM network.
* THIS DOES NOT WORK WITH SCENARIOS THAT ADD STOPS.
*/
private void doTransfersRefactored (RaptorState state) {
// Compute transfers only from stops updated pre-transfer within this departure minute / randomized schedule.
// These transfers then update the post-transfers bitset (stopsUpdated) to avoid concurrent modification while
// iterating.
final int maxWalkTimeSeconds = request.maxWalkTime * SECONDS_PER_MINUTE;
for (int stop = state.nonTransferStopsUpdated.nextSetBit(0);
stop >= 0;
stop = state.nonTransferStopsUpdated.nextSetBit(stop + 1)
) {
TIntList minTimesFromStop = transit.minTransferTimesFromStop[stop];
if (minTimesFromStop != null) {
for (int i = 0; i < minTimesFromStop.size(); i += 2) {
int targetStop = minTimesFromStop.get(i);
int minTimeSeconds = minTimesFromStop.get(i + 1);
if (minTimeSeconds < BOARD_SLACK_SECONDS) {
minTimeSeconds = BOARD_SLACK_SECONDS;
}
// if (minTimeSeconds < maxWalkTimeSeconds) {
int timeAtTargetStop = state.bestNonTransferTimes[stop] + minTimeSeconds;
state.setTimeAtStop(targetStop, timeAtTargetStop, -1, stop, 0, 0, true);
}
}
}
}

/**
* Find all patterns that could lead to improvements in the next raptor round after the given state's round.
* Specifically, these are the patterns passing through all stops that were updated in the given state's round.
Expand Down
103 changes: 47 additions & 56 deletions src/main/java/com/conveyal/r5/profile/RaptorState.java
Original file line number Diff line number Diff line change
Expand Up @@ -214,75 +214,66 @@ public void minMergePrevious () {
/**
* Check a time against the best known times at a transit stop, and record the new time if it is optimal.
* This same method is used to handle both transit arrivals and transfers, according to the transfer parameter.
* When transfer is false, times can update both the bestNonTransferTime and the bestTime; when transfer is true,
* only bestTimes can be updated.
*
* When transfer is false, times can only update bestNonTransferTime.
* When transfer is true, times can only update bestTimes.
* Note that staying at the same stop to board in the next round is now treated as a trivial "transfer".
* @param transfer if true, we are recording a time obtained via a transfer or the initial access leg in round 0
* @return true if the new time was optimal and the state was updated, false if the existing values were better
*/
public boolean setTimeAtStop(int stop, int time, int fromPattern, int fromStop, int waitTime, int inVehicleTime, boolean transfer) {

// First check whether the supplied travel time exceeds the specified maximum for this search.
if (time >= departureTime + maxDurationSeconds) {
return false;
}
// Method return value: was the new time optimal, leading to a state update?
boolean optimal = false;

// If this is "not a transfer" it is a transit arrival. If it is better than any known transit arrival,
// update the non-transfer time and path information, then consider updating the bestTimes.
// We may want to consider splitting the post-transfer updating out into its own method to make this clearer.
if (!transfer && time < bestNonTransferTimes[stop]) {
bestNonTransferTimes[stop] = time;
previousPatterns[stop] = fromPattern;
previousStop[stop] = fromStop;

// Carry the travel time components (wait and in-vehicle time) from the previous leg and increment them.
int totalWaitTime, totalInVehicleTime;
if (previous == null) {
// first round, there is no previous wait time or in vehicle time
// TODO how and when can this happen? Round zero contains only the access leg and has no transit.
totalWaitTime = waitTime;
totalInVehicleTime = inVehicleTime;
} else {
// TODO it seems like this whole block and the assignment below can be condensed significantly.
if (previous.transferStop[fromStop] != -1) {
// The fromSop was optimally reached via a transfer at the end of the previous round.
// Get the wait and in-vehicle time from the source stop of that transfer.
int preTransferStop = previous.transferStop[fromStop];
totalWaitTime = previous.nonTransferWaitTime[preTransferStop] + waitTime;
totalInVehicleTime = previous.nonTransferInVehicleTravelTime[preTransferStop] + inVehicleTime;

if (transfer) {
if (time < bestTimes[stop]) {
bestTimes[stop] = time;
transferStop[stop] = fromStop;
stopsUpdated.set(stop);
return true;
}
} else {
// If this is "not a transfer" it is a transit arrival. If it is better than any known transit arrival,
// update the non-transfer time and path information.
if (time < bestNonTransferTimes[stop]) {
bestNonTransferTimes[stop] = time;
previousPatterns[stop] = fromPattern;
previousStop[stop] = fromStop;

// Carry the travel time components (wait and in-vehicle time) from the previous leg and increment them.
int totalWaitTime, totalInVehicleTime;
if (previous == null) {
// first round, there is no previous wait time or in vehicle time
// TODO how and when can this happen? Round zero contains only the access leg and has no transit.
totalWaitTime = waitTime;
totalInVehicleTime = inVehicleTime;
} else {
// The stop we boarded at was reached directly by transit in the previous round.
totalWaitTime = previous.nonTransferWaitTime[fromStop] + waitTime;
totalInVehicleTime = previous.nonTransferInVehicleTravelTime[fromStop] + inVehicleTime;
// TODO it seems like this whole block and the assignment below can be condensed significantly.
if (previous.transferStop[fromStop] != -1) {
// The fromSop was optimally reached via a transfer at the end of the previous round.
// Get the wait and in-vehicle time from the source stop of that transfer.
int preTransferStop = previous.transferStop[fromStop];
totalWaitTime = previous.nonTransferWaitTime[preTransferStop] + waitTime;
totalInVehicleTime = previous.nonTransferInVehicleTravelTime[preTransferStop] + inVehicleTime;
} else {
// The stop we boarded at was reached directly by transit in the previous round.
totalWaitTime = previous.nonTransferWaitTime[fromStop] + waitTime;
totalInVehicleTime = previous.nonTransferInVehicleTravelTime[fromStop] + inVehicleTime;
}
}
nonTransferWaitTime[stop] = totalWaitTime;
nonTransferInVehicleTravelTime[stop] = totalInVehicleTime;
checkState(totalInVehicleTime + totalWaitTime <= (time - departureTime), "Components of travel time are greater than total travel time.");
transferStop[stop] = -1; // reached by transit, not transfer
nonTransferStopsUpdated.set(stop);
return true;
}
nonTransferWaitTime[stop] = totalWaitTime;
nonTransferInVehicleTravelTime[stop] = totalInVehicleTime;

checkState(totalInVehicleTime + totalWaitTime <= (time - departureTime),
"Components of travel time are greater than total travel time.");

optimal = true;
nonTransferStopsUpdated.set(stop);
}

// At a given stop, bestTimes is always less than or equal to bestNonTransferTimes. It will always be equal to
// the bestNonTransferTimes unless a transfer from some other stop yields an earlier time.
// If bestTimes is updated due to a transit arrival, the travel time components are already updated by the
// transit-handling block above. If it's due to a transfer, the travel time components were already recorded
// by an optimal arrival at the source station of the transfer.
if (time < bestTimes[stop]) {
bestTimes[stop] = time;
if (transfer) {
transferStop[stop] = fromStop;
} else {
transferStop[stop] = -1;
}
optimal = true;
stopsUpdated.set(stop);
}
return optimal;
// If we reach here, no new optimum was found.
return false;
}

/** Debug function: dump the path to a particular stop as a String. */
Expand Down
54 changes: 44 additions & 10 deletions src/main/java/com/conveyal/r5/transit/TransitLayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.conveyal.gtfs.model.Shape;
import com.conveyal.gtfs.model.Stop;
import com.conveyal.gtfs.model.StopTime;
import com.conveyal.gtfs.model.Transfer;
import com.conveyal.gtfs.model.Trip;
import com.conveyal.r5.api.util.TransitModes;
import com.conveyal.r5.common.GeometryUtils;
Expand Down Expand Up @@ -54,6 +55,11 @@
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;

import static com.conveyal.r5.common.Util.isNullOrEmpty;
import static com.conveyal.r5.common.Util.notNullOrEmpty;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;


/**
* A key simplifying factor is that we don't handle overnight trips. This is fine for analysis at usual times of day.
Expand Down Expand Up @@ -110,10 +116,14 @@ public class TransitLayer implements Serializable, Cloneable {
// Inverse map of streetVertexForStop, and reconstructed from that list.
public transient TIntIntMap stopForStreetVertex;

// For each stop, a packed list of transfers to other stops
// FIXME we may currently be storing weight or time to reach other stop, which we did to avoid floating point division. Instead, store distances in millimeters, and divide by speed in mm/sec.
// For each stop, a packed list of transfers to other stops.
// The list for each origin stop is a series of pairs of (destination stop index, distance in millimeters).
public List<TIntList> transfersForStop = new ArrayList<>();

// For each stop, a packed list of minimum times required to reach other stops. This reflects GTFS transfer type 2.
// The list for each origin stop is a series of pairs of (destination stop index, minimum time in seconds).
public TIntList[] minTransferTimesFromStop;

/** Information about a route */
public List<RouteInfo> routes = new ArrayList<>();

Expand Down Expand Up @@ -458,14 +468,38 @@ public void loadFromGtfs (GTFSFeed gtfs, LoadLevel level) throws DuplicateFeedEx
this.fares = new HashMap<>(gtfs.fares);
}

// Will be useful in naming patterns.
// LOG.info("Finding topology of each route/direction...");
// Multimap<T2<String, Integer>, TripPattern> patternsForRouteDirection = HashMultimap.create();
// tripPatterns.forEach(tp -> patternsForRouteDirection.put(new T2(tp.routeId, tp.directionId), tp));
// for (T2<String, Integer> routeAndDirection : patternsForRouteDirection.keySet()) {
// RouteTopology topology = new RouteTopology(routeAndDirection.first, routeAndDirection.second, patternsForRouteDirection.get(routeAndDirection));
// }

// TODO check how min transfer slack is applied on top of transfer times
// TODO try storing these minimum times separately so they're not affected by walk speed
if (notNullOrEmpty(gtfs.transfers)) {
LOG.info("Loading {} transfers from transfers.txt...", gtfs.transfers.size());
// Currently this only handles type 2 "minimum time to make a transfer"
minTransferTimesFromStop = new TIntList[stopForIndex.size()];
for (Transfer transfer : gtfs.transfers.values()) {
checkState(transfer.transfer_type == 2);
checkState(transfer.min_transfer_time > 0);
checkState(transfer.from_route_id == null);
checkState(transfer.from_trip_id == null);
checkState(transfer.to_route_id == null);
checkState(transfer.to_trip_id == null);
checkNotNull(transfer.from_stop_id);
checkNotNull(transfer.to_stop_id);
int fromStopIndex = indexForUnscopedStopId.get(transfer.from_stop_id);
int toStopIndex = indexForUnscopedStopId.get(transfer.to_stop_id);
// Do not (yet?) support minimum times between stations (rather than stops).
checkState(stopForIndex.get(fromStopIndex).location_type == 0);
checkState(stopForIndex.get(toStopIndex).location_type == 0);
if (minTransferTimesFromStop[fromStopIndex] == null) {
minTransferTimesFromStop[fromStopIndex] = new TIntArrayList();
}
// final double defaultWalkSpeedMetersPerSecond = 1.3888888888888888;
// int millimeters = (int) (transfer.min_transfer_time * defaultWalkSpeedMetersPerSecond * 1000);
// minTransferTimesFromStop[fromStopIndex].add(millimeters);
// LOG.info("Normalized distance from stop {} to stop {} is {} meters.",
// fromStopIndex, toStopIndex, millimeters / 1000d);
minTransferTimesFromStop[fromStopIndex].add(toStopIndex);
minTransferTimesFromStop[fromStopIndex].add(transfer.min_transfer_time);
}
}
}

// The median of all stopTimes would be best but that involves sorting a huge list of numbers.
Expand Down
11 changes: 8 additions & 3 deletions src/main/java/com/conveyal/r5/transit/TransportNetwork.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import static com.conveyal.r5.common.Util.isNullOrEmpty;

/**
* This is a completely new replacement for Graph, Router etc.
* It uses a lot less object pointers and can be built, read, and written orders of magnitude faster.
Expand Down Expand Up @@ -178,9 +180,12 @@ private static TransportNetwork fromFiles (String osmSourceFile, List<String> gt
streetLayer.buildEdgeLists();
transitLayer.rebuildTransientIndexes();

// Create transfers
new TransferFinder(transportNetwork).findTransfers();
new TransferFinder(transportNetwork).findParkRideTransfer();
// Create transfers vis OSM network if none were imported from transfers.txt. Eventually the two should be
// combined, but for now we're doing analyses with an exhaustive list in transfers.txt.
if (isNullOrEmpty(transitLayer.minTransferTimesFromStop)) {
new TransferFinder(transportNetwork).findTransfers();
new TransferFinder(transportNetwork).findParkRideTransfer();
}

transportNetwork.fareCalculator = tnBuilderConfig.analysisFareCalculator;

Expand Down