diff --git a/matsim/src/main/java/ch/sbb/matsim/config/SwissRailRaptorConfigGroup.java b/matsim/src/main/java/ch/sbb/matsim/config/SwissRailRaptorConfigGroup.java index 3c8040a92fb..c5376acc84b 100644 --- a/matsim/src/main/java/ch/sbb/matsim/config/SwissRailRaptorConfigGroup.java +++ b/matsim/src/main/java/ch/sbb/matsim/config/SwissRailRaptorConfigGroup.java @@ -20,6 +20,9 @@ package ch.sbb.matsim.config; import com.google.common.base.Verify; + +import ch.sbb.matsim.routing.pt.raptor.RaptorStaticConfig.RaptorTransferCalculation; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -61,6 +64,8 @@ public class SwissRailRaptorConfigGroup extends ReflectiveConfigGroup { private static final String PARAM_TRANSFER_WALK_MARGIN_DESC = "time deducted from transfer walk leg during transfers between pt legs in order to avoid missing a vehicle by a few seconds due to delays."; private static final String PARAM_INTERMODAL_LEG_ONLYHANDLING = "intermodalLegOnlyHandling"; private static final String PARAM_INTERMODAL_LEG_ONLYHANDLING_DESC = "Define how routes containing only intermodal legs are handled: Useful options: alllow, avoid, forbid"; + private static final String PARAM_TRANSFER_CALCULATION = "transferCalculation"; + private static final String PARAM_TRANFER_CALCULATION_DESC = "Defines whether all potential transfers are precomputed at the beginning of the simulation (Initial) or whether they are constructed on-demand (Cached). The former incurs potentially long up-front caclulations, but quicker routing. The latter avoids any initial computation, but may require longer routing time."; private boolean useRangeQuery = false; private boolean useIntermodality = false; @@ -74,6 +79,7 @@ public class SwissRailRaptorConfigGroup extends ReflectiveConfigGroup { private double transferPenaltyHourlyCost = 0; private double transferWalkMargin = 5; private IntermodalLegOnlyHandling intermodalLegOnlyHandling = IntermodalLegOnlyHandling.forbid; + private RaptorTransferCalculation transferCalculation = RaptorTransferCalculation.Initial; private ScoringParameters scoringParameters = ScoringParameters.Default; @@ -128,9 +134,19 @@ public void setIntermodalLegOnlyHandling(IntermodalLegOnlyHandling intermodalLeg public String getIntermodalLegOnlyHandlingString() { return intermodalLegOnlyHandling.toString(); } - + public IntermodalLegOnlyHandling getIntermodalLegOnlyHandling() { return intermodalLegOnlyHandling; + } + + @StringSetter(PARAM_TRANSFER_CALCULATION) + public void setTransferCalculation(RaptorTransferCalculation transferCalculation) { + this.transferCalculation = transferCalculation; + } + + @StringGetter(PARAM_TRANSFER_CALCULATION) + public RaptorTransferCalculation getTransferCalculation() { + return transferCalculation; } @StringGetter(PARAM_USE_RANGE_QUERY) @@ -707,6 +723,7 @@ public Map getComments() { comments.put(PARAM_USE_CAPACITY_CONSTRAINTS, PARAM_USE_CAPACITY_CONSTRAINTS_DESC); comments.put(PARAM_TRANSFER_WALK_MARGIN, PARAM_TRANSFER_WALK_MARGIN_DESC); comments.put(PARAM_INTERMODAL_ACCESS_EGRESS_MODE_SELECTION,PARAM_INTERMODAL_ACCESS_EGRESS_MODE_SELECTION_DESC); + comments.put(PARAM_TRANSFER_CALCULATION, PARAM_TRANFER_CALCULATION_DESC); return comments; } diff --git a/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/RaptorStaticConfig.java b/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/RaptorStaticConfig.java index 20b94216349..8f49f121ab2 100644 --- a/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/RaptorStaticConfig.java +++ b/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/RaptorStaticConfig.java @@ -51,6 +51,22 @@ public enum RaptorOptimization { * (see {@link SwissRailRaptor#calcTree(TransitStopFacility, double, RaptorParameters, Person)} ). */ OneToAllRouting } + + public enum RaptorTransferCalculation { + /** + * Use this option if you want the algorithm to calculate all possible transfers + * up-front, which will allow for rapid lookup during routing, but may come with + * significant simulation startup time. + */ + Initial, + + /** + * Use this option if you want the algorithm to calculate transfers on demand, + * which avoids any simulation start-up time but may increase the routing time + * itself. + */ + Cached + } /** @@ -71,6 +87,7 @@ public enum RaptorOptimization { private boolean useCapacityConstraints = false; private RaptorOptimization optimization = RaptorOptimization.OneToOneRouting; + private RaptorTransferCalculation transferCalculation = RaptorTransferCalculation.Initial; private SwissRailRaptorConfigGroup.IntermodalLegOnlyHandling intermodalLegOnlyHandling = SwissRailRaptorConfigGroup.IntermodalLegOnlyHandling.forbid; @@ -167,4 +184,12 @@ public SwissRailRaptorConfigGroup.IntermodalLegOnlyHandling getIntermodalLegOnly public void setIntermodalLegOnlyHandling(SwissRailRaptorConfigGroup.IntermodalLegOnlyHandling intermodalLegOnlyHandling) { this.intermodalLegOnlyHandling = intermodalLegOnlyHandling; } + + public RaptorTransferCalculation getTransferCalculation() { + return this.transferCalculation; + } + + public void setTransferCalculation(RaptorTransferCalculation transferCalculation) { + this.transferCalculation = transferCalculation; + } } diff --git a/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/RaptorUtils.java b/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/RaptorUtils.java index f289b04e486..724af748454 100644 --- a/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/RaptorUtils.java +++ b/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/RaptorUtils.java @@ -63,6 +63,7 @@ public static RaptorStaticConfig createStaticConfig(Config config) { staticConfig.setTransferWalkMargin(srrConfig.getTransferWalkMargin()); staticConfig.setIntermodalLegOnlyHandling(srrConfig.getIntermodalLegOnlyHandling()); staticConfig.setMinimalTransferTime(config.transitRouter().getAdditionalTransferTime()); + staticConfig.setTransferCalculation(srrConfig.getTransferCalculation()); staticConfig.setUseModeMappingForPassengers(srrConfig.isUseModeMappingForPassengers()); if (srrConfig.isUseModeMappingForPassengers()) { diff --git a/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorCore.java b/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorCore.java index 0242f5e91ab..37855f385f0 100644 --- a/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorCore.java +++ b/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorCore.java @@ -752,10 +752,17 @@ private void handleTransfers(boolean strict, RaptorParameters raptorParams) { continue; } RRouteStop fromRouteStop = fromPE.toRouteStop; // this is the route stop we arrive with least cost at stop - int firstTransferIndex = fromRouteStop.indexFirstTransfer; - int lastTransferIndex = firstTransferIndex + fromRouteStop.countTransfers; + + // obtain on-demand transfers if applicable (will return null if transfers are calculated initially) + RTransfer[] transfers = this.data.calculateTransfers(fromRouteStop); + + int firstTransferIndex = transfers == null ? fromRouteStop.indexFirstTransfer : 0; + int lastTransferIndex = transfers == null ? firstTransferIndex + fromRouteStop.countTransfers : transfers.length; + transfers = transfers == null ? this.data.transfers : transfers; + for (int transferIndex = firstTransferIndex; transferIndex < lastTransferIndex; transferIndex++) { - RTransfer transfer = this.data.transfers[transferIndex]; + RTransfer transfer = transfers[transferIndex]; + int toRouteStopIndex = transfer.toRouteStop; transferProvider.reset(transfer); int newArrivalTime = arrivalTime + transfer.transferTime; diff --git a/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorData.java b/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorData.java index 5abe7fb49e0..4813fafd95d 100644 --- a/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorData.java +++ b/matsim/src/main/java/ch/sbb/matsim/routing/pt/raptor/SwissRailRaptorData.java @@ -20,10 +20,26 @@ package ch.sbb.matsim.routing.pt.raptor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import javax.annotation.Nullable; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.matsim.api.core.v01.Coord; import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdMap; import org.matsim.api.core.v01.TransportMode; import org.matsim.api.core.v01.network.Link; import org.matsim.api.core.v01.network.Network; @@ -41,18 +57,8 @@ import org.matsim.vehicles.Vehicle; import org.matsim.vehicles.Vehicles; -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Supplier; +import ch.sbb.matsim.routing.pt.raptor.RaptorStaticConfig.RaptorOptimization; +import ch.sbb.matsim.routing.pt.raptor.RaptorStaticConfig.RaptorTransferCalculation; /** * @author mrieser / SBB @@ -75,12 +81,16 @@ public class SwissRailRaptorData { final QuadTree stopsQT; final Map>> stopFilterAttribute2Value2StopsQT; final OccupancyData occupancyData; + + // data needed if cached transfer construction is activated + final IdMap> staticTransferTimes; + final RTransfer[][] transferCache; private SwissRailRaptorData(RaptorStaticConfig config, int countStops, RRoute[] routes, int[] departures, Vehicle[] departureVehicles, Id[] departureIds, RRouteStop[] routeStops, RTransfer[] transfers, Map stopFacilityIndices, Map routeStopsPerStopFacility, QuadTree stopsQT, - OccupancyData occupancyData) { + OccupancyData occupancyData, IdMap> staticTransferTimes) { this.config = config; this.countStops = countStops; this.countRouteStops = routeStops.length; @@ -95,6 +105,10 @@ private SwissRailRaptorData(RaptorStaticConfig config, int countStops, this.stopsQT = stopsQT; this.stopFilterAttribute2Value2StopsQT = new HashMap<>(); this.occupancyData = occupancyData; + + // data needed if cached transfer construction is activated + this.staticTransferTimes = staticTransferTimes; + this.transferCache = new RTransfer[routeStops.length][]; } public static SwissRailRaptorData create(TransitSchedule schedule, @Nullable Vehicles transitVehicles, RaptorStaticConfig staticConfig, Network network, OccupancyData occupancyData) { @@ -200,7 +214,16 @@ public static SwissRailRaptorData create(TransitSchedule schedule, @Nullable Veh QuadTree stopsQT = TransitScheduleUtils.createQuadTreeOfTransitStopFacilities(stops); int countStopFacilities = stops.size(); - Map allTransfers = calculateRouteStopTransfers(schedule, stopsQT, routeStopsPerStopFacility, routeStops, staticConfig); + // if cached transfer calculation is active, don't generate any transfers here + final Map allTransfers; + + if (staticConfig.getTransferCalculation().equals(RaptorTransferCalculation.Initial)) { + allTransfers = calculateRouteStopTransfers(schedule, stopsQT, routeStopsPerStopFacility, routeStops, + staticConfig); + } else { + allTransfers = Collections.emptyMap(); + } + long countTransfers = 0; for (RTransfer[] transfers : allTransfers.values()) { countTransfers += transfers.length; @@ -221,8 +244,22 @@ public static SwissRailRaptorData create(TransitSchedule schedule, @Nullable Veh indexTransfer += transferCount; } } + + // if cached transfer calculation is used, build a map for quick lookup of minimal transfer times + IdMap> staticTransferTimes = null; + if (staticConfig.getTransferCalculation().equals(RaptorTransferCalculation.Cached)) { + staticTransferTimes = new IdMap<>(TransitStopFacility.class); - SwissRailRaptorData data = new SwissRailRaptorData(staticConfig, countStopFacilities, routes, departures, departureVehicles, departureIds, routeStops, transfers, stopFacilityIndices, routeStopsPerStopFacility, stopsQT, occupancyData); + MinimalTransferTimes.MinimalTransferTimesIterator iterator = schedule.getMinimalTransferTimes().iterator(); + while (iterator.hasNext()) { + iterator.next(); + + staticTransferTimes.computeIfAbsent(iterator.getFromStopId(), id -> new HashMap<>()) + .put(schedule.getFacilities().get(iterator.getToStopId()), iterator.getSeconds()); + } + } + + SwissRailRaptorData data = new SwissRailRaptorData(staticConfig, countStopFacilities, routes, departures, departureVehicles, departureIds, routeStops, transfers, stopFacilityIndices, routeStopsPerStopFacility, stopsQT, occupancyData, staticTransferTimes); long endMillis = System.currentTimeMillis(); log.info("SwissRailRaptor data preparation done. Took " + (endMillis - startMillis) / 1000 + " seconds."); @@ -604,4 +641,77 @@ public Transfer get() { return this.transfer; } } + + RTransfer[] calculateTransfers(RRouteStop fromRouteStop) { + if (config.getTransferCalculation().equals(RaptorTransferCalculation.Initial)) { + return null; + } + + // We tested this in a parallel set-up and things seem to work as they are + // implemented. The routing threads will access the cache as read-only an + // retrieve the cached stop connections. It can happen that two of them try to + // obtain a non-existent entry at the same time. In that case, the calculation + // is performed twice, but this is not critical. Then this function writes the + // connections into the cache. This is a replacement of one address in the array + // from null to a concrete value, and our tests show that this seems to appear + // as atomic to the using threads. However, it is not 100% excluded that there + // is some parallelization issue here and that we rather should shield the cache + // using a lock in some way. But so far, we didn't experience any problem. /sh + // may 2024 + + RTransfer[] cache = transferCache[fromRouteStop.index]; + if (cache != null) return cache; // we had a cache hit + + // setting up useful constants + final double minimalTransferTime = config.getMinimalTransferTime(); + final double beelineWalkConnectionDistance = config.getBeelineWalkConnectionDistance(); + final double beelineDistanceFactor = config.getBeelineWalkDistanceFactor(); + final double beelineWalkSpeed = config.getBeelineWalkSpeed(); + final RaptorOptimization optimization = config.getOptimization(); + + // the facility from which we want to transfer + TransitStopFacility fromRouteFacility = fromRouteStop.routeStop.getStopFacility(); + Collection transferCandidates = new LinkedList<>(); + + // find transfer candidates by distance + transferCandidates.addAll(stopsQT.getDisk(fromRouteFacility.getCoord().getX(), fromRouteFacility.getCoord().getY(), config.getBeelineWalkConnectionDistance())); + + // find transfer candidates with predefined transfer time + Map transferTimes = staticTransferTimes.get(fromRouteFacility.getId()); + + if (transferTimes != null) { // some transfer times are predefined + transferCandidates.addAll(transferTimes.keySet()); + } + + // now evaluate whether transfers are useful, distance, and travel time + List transfers = new LinkedList<>(); + for (TransitStopFacility toRouteFacility : transferCandidates) { + for (int toRouteStopIndex : routeStopsPerStopFacility.get(toRouteFacility)) { + RRouteStop toRouteStop = routeStops[toRouteStopIndex]; + + double beelineDistance = CoordUtils.calcEuclideanDistance(fromRouteFacility.getCoord(), toRouteFacility.getCoord()); + double transferTime = beelineDistance / beelineWalkSpeed; + + if (transferTime < minimalTransferTime) { + transferTime = minimalTransferTime; + } + + if (transferTimes != null) { + // check if we find a predefined transfer time + transferTime = transferTimes.getOrDefault(toRouteFacility, transferTime); + } + + if (SwissRailRaptorData.isUsefulTransfer(fromRouteStop, toRouteStop, beelineWalkConnectionDistance, optimization)) { + transfers.add(new RTransfer(fromRouteStop.index, toRouteStop.index, transferTime, beelineDistance * beelineDistanceFactor)); + } + } + } + + // convert to array + RTransfer[] stopTransfers = transfers.toArray(new RTransfer[transfers.size()]); + + // save to cache (no issue regarding parallel execution because we simply set an element) + transferCache[fromRouteStop.index] = stopTransfers; + return stopTransfers; + } } diff --git a/matsim/src/main/java/org/matsim/pt/transitSchedule/MinimalTransferTimesImpl.java b/matsim/src/main/java/org/matsim/pt/transitSchedule/MinimalTransferTimesImpl.java index f8e5bf92d9f..c78b1b1687e 100644 --- a/matsim/src/main/java/org/matsim/pt/transitSchedule/MinimalTransferTimesImpl.java +++ b/matsim/src/main/java/org/matsim/pt/transitSchedule/MinimalTransferTimesImpl.java @@ -19,9 +19,11 @@ package org.matsim.pt.transitSchedule; +import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.matsim.api.core.v01.Id; @@ -164,4 +166,15 @@ public double getSeconds() { throw new NoSuchElementException(); } } + + @Override + public Set> getCandidates(Id fromStop) { + var inner = minimalTransferTimes.get(fromStop); + + if (inner == null) { + return Collections.emptySet(); + } + + return inner.keySet(); + } } diff --git a/matsim/src/main/java/org/matsim/pt/transitSchedule/api/MinimalTransferTimes.java b/matsim/src/main/java/org/matsim/pt/transitSchedule/api/MinimalTransferTimes.java index 72819f5cf70..c567093f865 100644 --- a/matsim/src/main/java/org/matsim/pt/transitSchedule/api/MinimalTransferTimes.java +++ b/matsim/src/main/java/org/matsim/pt/transitSchedule/api/MinimalTransferTimes.java @@ -19,6 +19,8 @@ package org.matsim.pt.transitSchedule.api; +import java.util.Set; + import org.matsim.api.core.v01.Id; /** @@ -66,6 +68,8 @@ public interface MinimalTransferTimes { * @return the previously set minimal transfer time, or Double.NaN if none was set. */ double remove(Id fromStop, Id toStop); + + Set> getCandidates(Id fromStop); /** * @return an iterator to iterate over all minimal transfer times set.