Skip to content

Commit

Permalink
Merge pull request #5 from naviqore/feature/NAV-15-raptor-data-structure
Browse files Browse the repository at this point in the history
Feature/nav 15 raptor data structure
  • Loading branch information
munterfi authored Apr 28, 2024
2 parents f004c27 + d7c90ee commit 0cc5f65
Show file tree
Hide file tree
Showing 20 changed files with 684 additions and 81 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ build/
.DS_Store

# Benchmark test files
benchmark/input/*.zip
benchmark/
53 changes: 53 additions & 0 deletions src/main/java/ch/naviqore/raptor/GtfsToRaptorConverter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package ch.naviqore.raptor;

import ch.naviqore.gtfs.schedule.model.*;
import ch.naviqore.raptor.model.Raptor;
import ch.naviqore.raptor.model.RaptorBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;

import java.time.LocalDate;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* Maps GTFS schedule to Raptor
*
* @author munterfi
*/
@RequiredArgsConstructor
@Log4j2
public class GtfsToRaptorConverter {

private final Set<Stop> stops = new HashSet<>();
private final Set<Route> routes = new HashSet<>();
private final RaptorBuilder builder;

public Raptor convert(GtfsSchedule schedule, LocalDate date) {
List<Trip> activeTrips = schedule.getActiveTrips(date);
log.info("Converting {} active trips from GTFS schedule to Raptor model", activeTrips.size());
for (Trip trip : activeTrips) {
Route route = trip.getRoute();
if (!routes.contains(route)) {
routes.add(route);
builder.addRoute(route.getId());
// TODO: Add test for consistency of route stops. Since in GTFS are defined per trip, but Raptor
// builder expects them to be the same for all trips of a route.
for (StopTime stopTime : trip.getStopTimes()) {
if (!stops.contains(stopTime.stop())) {
stops.add(stopTime.stop());
builder.addStop(stopTime.stop().getId());
}
builder.addRouteStop(stopTime.stop().getId(), route.getId());
}
}
for (StopTime stopTime : trip.getStopTimes()) {
builder.addStopTime(stopTime.stop().getId(), route.getId(), stopTime.arrival().getTotalSeconds(),
stopTime.departure().getTotalSeconds());
}
}

return builder.build();
}
}
6 changes: 6 additions & 0 deletions src/main/java/ch/naviqore/raptor/model/Lookup.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ch.naviqore.raptor.model;

import java.util.Map;

public record Lookup(Map<String, Integer> stops, Map<String, Integer> routes) {
}
144 changes: 144 additions & 0 deletions src/main/java/ch/naviqore/raptor/model/Raptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package ch.naviqore.raptor.model;

import lombok.extern.log4j.Log4j2;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
* Raptor algorithm implementation
*
* @author munterfi
*/
@Log4j2
public class Raptor {

public final static int NO_INDEX = -1;
// lookup
private final Map<String, Integer> stopsToIdx;
private final Map<String, Integer> routesToIdx;
// stop context
private final Transfer[] transfers;
private final Stop[] stops;
private final int[] stopRoutes;
// route traversal
private final StopTime[] stopTimes;
private final Route[] routes;
private final RouteStop[] routeStops;

Raptor(Lookup lookup, StopContext stopContext, RouteTraversal routeTraversal) {
this.stopsToIdx = lookup.stops();
this.routesToIdx = lookup.routes();
this.transfers = stopContext.transfers();
this.stops = stopContext.stops();
this.stopRoutes = stopContext.stopRoutes();
this.stopTimes = routeTraversal.stopTimes();
this.routes = routeTraversal.routes();
this.routeStops = routeTraversal.routeStops();
}

public static RaptorBuilder builder() {
return new RaptorBuilder();
}

public void routeEarliestArrival(String sourceStop, String targetStop, int departureTime) {
log.debug("Routing earliest arrival from {} to {} at {}", sourceStop, targetStop, departureTime);

final int sourceIdx = stopsToIdx.get(sourceStop);
final int targetIdx = stopsToIdx.get(targetStop);

// initialization
int[] earliestArrival = new int[stops.length];
Arrays.fill(earliestArrival, Integer.MAX_VALUE);
earliestArrival[sourceIdx] = departureTime;

// add first stop to marked stops
Set<Integer> marked = new HashSet<>();
marked.add(sourceIdx);

// perform rounds
int round = 0;
while (!marked.isEmpty()) {
log.info("Processing round {} (= transfers), marked: {}", round,
marked.stream().map(stopIdx -> stops[stopIdx].id()).toList());
Set<Integer> nextMarked = new HashSet<>();
for (int stopIdx : marked) {
log.debug("Processing marked stop {} - {}", stopIdx, stops[stopIdx].id());
Stop stop = stops[stopIdx];
for (int i = stop.stopRouteIdx(); i < stop.numberOfRoutes(); i++) {
int routeIdx = stopRoutes[i];
Route route = routes[routeIdx];
log.debug("Scanning route {} - {}", routeIdx, route.id());
// iterate until current stop index is found on route
int stopOffset = 0;
for (int j = route.firstRouteStopIdx(); j < route.firstRouteStopIdx() + route.numberOfStops(); j++) {
if (routeStops[j].stopIndex() == stopIdx) {
break;
}
stopOffset++;
}
log.debug("Stop offset on route {} is {} - {}", route.id(), stopOffset,
stops[routeStops[route.firstRouteStopIdx() + stopOffset].stopIndex()].id());
// find active trip: check if possible to hop on trip
int arrivalTimeAtCurrentStop = earliestArrival[stopIdx];
int activeTrip = 0;
for (int k = route.firstStopTimeIdx() + stopOffset; k < route.firstStopTimeIdx() + route.numberOfTrips() * route.numberOfStops(); k += route.numberOfStops()) {
// TODO: Consider dwell time
if (stopTimes[k].departure() >= arrivalTimeAtCurrentStop) {
break;
}
activeTrip++;
}
log.debug("Scanning active trip number {} on route {} - {}", activeTrip, routeIdx, route.id());
int from = route.firstStopTimeIdx() + activeTrip * route.numberOfStops() + stopOffset;
int to = route.firstStopTimeIdx() + (activeTrip + 1) * route.numberOfStops();
int currentRouteStopIdx = route.firstRouteStopIdx() + stopOffset;
for (int k = from; k < to; k++) {
int currentStopIdx = routeStops[currentRouteStopIdx].stopIndex();
if (stopTimes[k].arrival() < earliestArrival[currentStopIdx]) {
earliestArrival[currentStopIdx] = stopTimes[k].arrival();
nextMarked.add(currentStopIdx);
}
currentRouteStopIdx++;
}
}
}

// relax transfers (= footpaths)
for (int stopIdx : marked) {
Stop stop = stops[stopIdx];
if (stop.transferIdx() == NO_INDEX) {
continue;
}
for (int k = stop.transferIdx(); k < stop.numberOfTransfers(); k++) {
Transfer transfer = transfers[k];
int targetStopIdx = transfer.targetStopIdx();
int arrivalTimeAfterTransfer = earliestArrival[stopIdx] + transfer.duration();
if (arrivalTimeAfterTransfer < earliestArrival[targetStopIdx]) {
earliestArrival[targetStopIdx] = arrivalTimeAfterTransfer;
nextMarked.add(targetStopIdx);
}
}
}

// prepare for next round
marked = nextMarked;
round++;
}

// print results for debugging
for (int i = 0; i < earliestArrival.length; i++) {
if (earliestArrival[i] == Integer.MAX_VALUE) {
earliestArrival[i] = -1;
}
}
for (Stop stop : stops) {
System.out.println(stop.id() + ": " + earliestArrival[stopsToIdx.get(stop.id())]);
}

log.debug("Earliest arrival at {}: {}", targetStop, earliestArrival[stopsToIdx.get(targetStop)]);
}

}
181 changes: 181 additions & 0 deletions src/main/java/ch/naviqore/raptor/model/RaptorBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package ch.naviqore.raptor.model;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;

import java.util.*;

/**
* Builds the Raptor and its internal data structures
* <p>
* Note: The builder expects that stops, routes, route stops and stop times are added in the correct order.
*
* @author munterfi
*/
@NoArgsConstructor(access = AccessLevel.PACKAGE)
@Log4j2
public class RaptorBuilder {

private final Map<String, Integer> stops = new HashMap<>();
private final Map<String, Integer> routes = new HashMap<>();
private final Map<String, List<String>> routeStops = new HashMap<>();
private final Map<String, List<StopTime>> stopTimes = new HashMap<>();
private final Map<String, Set<String>> stopRoutes = new HashMap<>();
private final Map<String, List<Transfer>> transfers = new HashMap<>();

private int stopSize = 0;
private int routeSize = 0;
private int routeStopSize = 0;
private int stopTimeSize = 0;
private int transferSize = 0;

public RaptorBuilder addStop(String id) {
if (stops.containsKey(id)) {
throw new IllegalArgumentException("Stop " + id + " already exists");
}
log.debug("Adding stop: id={}", id);
stops.put(id, stops.size());
stopSize++;
return this;
}

public RaptorBuilder addRoute(String id) {
if (routes.containsKey(id)) {
throw new IllegalArgumentException("Route " + id + " already exists");
}
log.debug("Adding route: id={}", id);
routes.put(id, routes.size());
routeSize++;
return this;
}

public RaptorBuilder addRouteStop(String stopId, String routeId) {
log.debug("Adding route stop: stopId={}, routeId={}", stopId, routeId);
if (!stops.containsKey(stopId)) {
throw new IllegalArgumentException("Stop " + stopId + " does not exist");
}
if (!routes.containsKey(routeId)) {
throw new IllegalArgumentException("Route " + routeId + " does not exist");
}
routeStops.computeIfAbsent(routeId, k -> new ArrayList<>()).add(stopId);
stopRoutes.computeIfAbsent(stopId, k -> new HashSet<>()).add(routeId);
routeStopSize++;
return this;
}

public RaptorBuilder addStopTime(String stopId, String routeId, int arrival, int departure) {
log.debug("Adding stop time: stopId={}, routeId={}, arrival={}, departure={}", stopId, routeId, arrival,
departure);
if (!stops.containsKey(stopId)) {
log.error("Stop {} does not exist", stopId);
// TODO: Reactivate after test for consistency of route stops.
// throw new IllegalArgumentException("Stop " + stopId + " does not exist");
}
if (!routes.containsKey(routeId)) {
throw new IllegalArgumentException("Route " + routeId + " does not exist");
}
stopTimes.computeIfAbsent(routeId, k -> new ArrayList<>()).add(new StopTime(arrival, departure));
stopTimeSize++;
return this;
}

public RaptorBuilder addTransfer(String sourceStopId, String targetStopId, int duration) {
log.debug("Adding transfer: sourceStopId={}, targetStopId={}, duration={}", sourceStopId, targetStopId,
duration);
if (!stops.containsKey(sourceStopId)) {
throw new IllegalArgumentException("Source stop " + sourceStopId + " does not exist");
}
if (!stops.containsKey(targetStopId)) {
throw new IllegalArgumentException("Target stop " + targetStopId + " does not exist");
}
transfers.computeIfAbsent(sourceStopId, k -> new ArrayList<>())
.add(new Transfer(stops.get(targetStopId), duration));
transferSize++;
return this;
}

public Raptor build() {
Lookup lookup = buildLookup();
StopContext stopContext = buildStopContext();
RouteTraversal routeTraversal = buildRouteTraversal();
log.info("Initialize Raptor with {} stops, {} routes, {} route stops, {} stop times, {} transfers", stopSize,
routeSize, routeStopSize, stopTimeSize, transferSize);
return new Raptor(lookup, stopContext, routeTraversal);
}

private Lookup buildLookup() {
log.info("Building lookup with {} stops and {} routes", stopSize, routeSize);
return new Lookup(new HashMap<>(stops), new HashMap<>(routes));
}

private StopContext buildStopContext() {
log.info("Building stop context with {} stops and {} transfers", stopSize, transferSize);
Stop[] stopArr = new Stop[stopSize];
int[] stopRouteArr = new int[stopRoutes.values().stream().mapToInt(Set::size).sum()];
Transfer[] transferArr = new Transfer[transferSize];

int transferCnt = 0;
int stopRouteCnt = 0;
for (Map.Entry<String, Integer> entry : stops.entrySet()) {
String stopId = entry.getKey();
int stopIdx = entry.getValue();

List<Transfer> currentTransfers = transfers.get(stopId);
int currentTransferCnt = 0;
if (currentTransfers != null) {
for (Transfer transfer : currentTransfers) {
transferArr[transferCnt++] = transfer;
currentTransferCnt++;
}
}

Set<String> currentStopRoutes = stopRoutes.get(stopId);
if (currentStopRoutes == null) {
throw new IllegalStateException("Stop " + stopId + " has no routes");
}
for (String routeId : currentStopRoutes) {
stopRouteArr[stopRouteCnt++] = routes.get(routeId);
}

stopArr[stopIdx] = new Stop(stopId, stopRouteCnt - currentStopRoutes.size(), currentStopRoutes.size(),
currentTransferCnt == 0 ? Raptor.NO_INDEX : transferCnt - currentTransferCnt, currentTransferCnt);
}
return new StopContext(transferArr, stopArr, stopRouteArr);
}

private RouteTraversal buildRouteTraversal() {
log.info("Building route traversal with {} routes, {} route stops, {} stop times", routeSize, routeStopSize,
stopTimeSize);
Route[] routeArr = new Route[routeSize];
RouteStop[] routeStopArr = new RouteStop[routeStopSize];
StopTime[] stopTimeArr = new StopTime[stopTimeSize];

int routeStopCnt = 0;
int stopTimeCnt = 0;
for (Map.Entry<String, Integer> entry : routes.entrySet()) {
String routeId = entry.getKey();
int routeIdx = entry.getValue();

List<String> currentRouteStops = routeStops.get(routeId);
if (currentRouteStops == null) {
throw new IllegalStateException("Route " + routeId + " has no route stops");
}
for (String routeStop : currentRouteStops) {
routeStopArr[routeStopCnt++] = new RouteStop(stops.get(routeStop), routeIdx);
}

List<StopTime> currentStopTimes = stopTimes.get(routeId);
if (currentStopTimes == null) {
throw new IllegalStateException("Route " + routeId + " has no stop times");
}
for (StopTime stopTime : currentStopTimes) {
stopTimeArr[stopTimeCnt++] = stopTime;
}

routeArr[routeIdx] = new Route(routeId, routeStopCnt - currentRouteStops.size(), currentRouteStops.size(),
stopTimeCnt - currentStopTimes.size(), currentStopTimes.size());
}
return new RouteTraversal(stopTimeArr, routeArr, routeStopArr);
}
}
Loading

0 comments on commit 0cc5f65

Please sign in to comment.