diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/LinearStopDurationModule.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/LinearStopDurationModule.java new file mode 100644 index 00000000000..f36bdaa49bb --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/LinearStopDurationModule.java @@ -0,0 +1,24 @@ + +package org.matsim.contrib.drt.extension.preplanned.optimizer; + +import org.matsim.contrib.drt.run.DrtConfigGroup; +import org.matsim.contrib.drt.stops.CumulativeStopTimeCalculator; +import org.matsim.contrib.drt.stops.DefaultPassengerStopDurationProvider; +import org.matsim.contrib.drt.stops.StopTimeCalculator; +import org.matsim.contrib.dvrp.run.AbstractDvrpModeModule; + +public class LinearStopDurationModule extends AbstractDvrpModeModule { + private final DrtConfigGroup drtConfigGroup; + + public LinearStopDurationModule(DrtConfigGroup drtConfigGroup) { + super(drtConfigGroup.getMode()); + this.drtConfigGroup = drtConfigGroup; + } + + @Override + public void install() { + StopTimeCalculator stopTimeCalculator = new CumulativeStopTimeCalculator(new DefaultPassengerStopDurationProvider(drtConfigGroup.stopDuration)); + bindModal(StopTimeCalculator.class).toInstance(stopTimeCalculator); + } +} + diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/InsertionCalculator.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/InsertionCalculator.java new file mode 100644 index 00000000000..7d8339d63e3 --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/InsertionCalculator.java @@ -0,0 +1,290 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.FleetSchedules; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.GeneralRequest; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.OnlineVehicleInfo; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.TimetableEntry; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.LinkToLinkTravelTimeMatrix; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; + +import java.util.ArrayList; +import java.util.List; + +public record InsertionCalculator(Network network, double stopDuration, LinkToLinkTravelTimeMatrix linkToLinkTravelTimeMatrix) { + public final static double NOT_FEASIBLE_COST = 1e6; + + /** + * Compute the cost to insert the request in to the vehicle. + */ + public InsertionData computeInsertionData(OnlineVehicleInfo vehicleInfo, GeneralRequest request, + FleetSchedules previousSchedules) { + Link fromLink = network.getLinks().get(request.getFromLinkId()); + Link toLink = network.getLinks().get(request.getToLinkId()); + Link currentLink = vehicleInfo.currentLink(); + double divertableTime = vehicleInfo.divertableTime(); + double serviceEndTime = vehicleInfo.vehicle().getServiceEndTime() - stopDuration; // the last stop must start on or before this time step + List originalTimetable = previousSchedules.vehicleToTimetableMap().get(vehicleInfo.vehicle().getId()); + + // 1. if timetable is empty + if (originalTimetable.isEmpty()) { + double timeToPickup = linkToLinkTravelTimeMatrix.getTravelTime(currentLink, fromLink, divertableTime); + double arrivalTimePickUp = divertableTime + timeToPickup; + if (arrivalTimePickUp > request.getLatestDepartureTime() || arrivalTimePickUp > serviceEndTime) { + return new InsertionData(null, NOT_FEASIBLE_COST, vehicleInfo); + } + + double departureTimePickUp = Math.max(request.getEarliestDepartureTime(), arrivalTimePickUp) + stopDuration; + double tripTravelTime = linkToLinkTravelTimeMatrix.getTravelTime(fromLink, toLink, departureTimePickUp); + double arrivalTimeDropOff = departureTimePickUp + tripTravelTime; + double totalInsertionCost = timeToPickup + tripTravelTime; + + List updatedTimetable = new ArrayList<>(); + updatedTimetable.add(new TimetableEntry(request, TimetableEntry.StopType.PICKUP, arrivalTimePickUp, departureTimePickUp, 0, stopDuration, vehicleInfo.vehicle())); + updatedTimetable.add(new TimetableEntry(request, TimetableEntry.StopType.DROP_OFF, arrivalTimeDropOff, arrivalTimeDropOff + stopDuration, 1, stopDuration, vehicleInfo.vehicle())); + // Note: The departure time of the last stop is actually not meaningful, but this stop may become non-last stop later, therefore, we set the departure time of this stop as if it is a middle stop + return new InsertionData(updatedTimetable, totalInsertionCost, vehicleInfo); + } + + // 2. If original timetable is non-empty + double insertionCost = NOT_FEASIBLE_COST; + List candidateTimetable = null; + + for (int i = 0; i < originalTimetable.size() + 1; i++) { + double pickUpInsertionCost; + List temporaryTimetable; + + // Insert pickup + if (i < originalTimetable.size()) { + if (originalTimetable.get(i).isVehicleFullBeforeThisStop()) { + continue; + } + double detourA; + double arrivalTimePickUpStop; + Link linkOfStopBeforePickUpInsertion; + double departureTimeOfStopBeforePickUpInsertion; + if (i == 0) { + // insert pickup before the first stop + // no stop before pickup insertion --> use current location of the vehicle + linkOfStopBeforePickUpInsertion = currentLink; + // no stop before pickup insertion --> use divertable time of the vehicle + departureTimeOfStopBeforePickUpInsertion = divertableTime; + detourA = linkToLinkTravelTimeMatrix.getTravelTime(currentLink, fromLink, divertableTime); + arrivalTimePickUpStop = divertableTime + detourA; + } else { + TimetableEntry stopBeforePickUpInsertion = originalTimetable.get(i - 1); + linkOfStopBeforePickUpInsertion = network.getLinks().get(stopBeforePickUpInsertion.getLinkId()); + departureTimeOfStopBeforePickUpInsertion = stopBeforePickUpInsertion.getDepartureTime(); + detourA = linkToLinkTravelTimeMatrix.getTravelTime(linkOfStopBeforePickUpInsertion, fromLink, stopBeforePickUpInsertion.getDepartureTime()); + arrivalTimePickUpStop = departureTimeOfStopBeforePickUpInsertion + detourA; + } + if (arrivalTimePickUpStop > request.getLatestDepartureTime() || arrivalTimePickUpStop > serviceEndTime) { + break; + // Vehicle can no longer reach the pickup location in time. No need to continue with this vehicle + } + double departureTimePickUpStop = Math.max(arrivalTimePickUpStop, request.getEarliestDepartureTime()) + stopDuration; + TimetableEntry stopAfterPickUpInsertion = originalTimetable.get(i); + Link linkOfStopAfterPickUpInsertion = network.getLinks().get(stopAfterPickUpInsertion.getLinkId()); + double detourB = linkToLinkTravelTimeMatrix.getTravelTime(fromLink, linkOfStopAfterPickUpInsertion, departureTimePickUpStop); + double newArrivalTimeOfNextStop = departureTimePickUpStop + detourB; + double delayCausedByInsertingPickUp = newArrivalTimeOfNextStop - stopAfterPickUpInsertion.getArrivalTime(); + if (isInsertionNotFeasible(originalTimetable, i, delayCausedByInsertingPickUp, serviceEndTime)) { + continue; + } + pickUpInsertionCost = detourA + detourB - linkToLinkTravelTimeMatrix.getTravelTime(linkOfStopBeforePickUpInsertion, linkOfStopAfterPickUpInsertion, departureTimeOfStopBeforePickUpInsertion); + TimetableEntry pickupStopToInsert = new TimetableEntry(request, TimetableEntry.StopType.PICKUP, + arrivalTimePickUpStop, departureTimePickUpStop, stopAfterPickUpInsertion.getOccupancyBeforeStop(), stopDuration, vehicleInfo.vehicle()); + temporaryTimetable = insertPickup(originalTimetable, i, pickupStopToInsert, delayCausedByInsertingPickUp); + } else { + // Append pickup at the end + TimetableEntry stopBeforePickUpInsertion = originalTimetable.get(i - 1); + Link linkOfStopBeforePickUpInsertion = network.getLinks().get(stopBeforePickUpInsertion.getLinkId()); + double departureTimeOfStopBeforePickUpInsertion = stopBeforePickUpInsertion.getDepartureTime(); + double travelTimeToPickUp = linkToLinkTravelTimeMatrix.getTravelTime(linkOfStopBeforePickUpInsertion, fromLink, departureTimeOfStopBeforePickUpInsertion); + double arrivalTimePickUpStop = travelTimeToPickUp + departureTimeOfStopBeforePickUpInsertion; + if (arrivalTimePickUpStop > request.getLatestDepartureTime() || arrivalTimePickUpStop > serviceEndTime) { + break; + } + double departureTimePickUpStop = Math.max(arrivalTimePickUpStop, request.getEarliestDepartureTime()) + stopDuration; + pickUpInsertionCost = travelTimeToPickUp; + TimetableEntry pickupStopToInsert = new TimetableEntry(request, TimetableEntry.StopType.PICKUP, + arrivalTimePickUpStop, departureTimePickUpStop, 0, stopDuration, vehicleInfo.vehicle()); + temporaryTimetable = insertPickup(originalTimetable, i, pickupStopToInsert, 0); + //Appending pickup at the end will not cause any delay to the original timetable + } + + // Insert drop off + for (int j = i + 1; j < temporaryTimetable.size() + 1; j++) { + // Check occupancy feasibility + if (temporaryTimetable.get(j - 1).isVehicleOverloaded()) { + // If the stop before the drop-off insertion is overloaded, then it is not feasible to insert drop off at or after current location + break; + } + + TimetableEntry stopBeforeDropOffInsertion = temporaryTimetable.get(j - 1); + Link linkOfStopBeforeDropOffInsertion = network.getLinks().get(stopBeforeDropOffInsertion.getLinkId()); + double departureTimeOfStopBeforeDropOffInsertion = stopBeforeDropOffInsertion.getDepartureTime(); + if (j < temporaryTimetable.size()) { // Insert drop off between two stops + double detourC = linkToLinkTravelTimeMatrix.getTravelTime(linkOfStopBeforeDropOffInsertion, toLink, departureTimeOfStopBeforeDropOffInsertion); + double arrivalTimeDropOffStop = departureTimeOfStopBeforeDropOffInsertion + detourC; + if (arrivalTimeDropOffStop > request.getLatestArrivalTime() || arrivalTimeDropOffStop > serviceEndTime) { + break; + } + double departureTimeDropOffStop = arrivalTimeDropOffStop + stopDuration; + TimetableEntry stopAfterDropOffInsertion = temporaryTimetable.get(j); + Link linkOfStopAfterDropOffInsertion = network.getLinks().get(stopAfterDropOffInsertion.getLinkId()); + double detourD = linkToLinkTravelTimeMatrix.getTravelTime(toLink, linkOfStopAfterDropOffInsertion, departureTimeDropOffStop); + double newArrivalTimeOfStopAfterDropOffInsertion = departureTimeDropOffStop + detourD; + double delayCausedByDropOffInsertion = newArrivalTimeOfStopAfterDropOffInsertion - stopAfterDropOffInsertion.getArrivalTime(); + if (isInsertionNotFeasible(temporaryTimetable, j, delayCausedByDropOffInsertion, serviceEndTime)) { + continue; + } + double dropOffInsertionCost = detourC + detourD - linkToLinkTravelTimeMatrix.getTravelTime(linkOfStopBeforeDropOffInsertion, linkOfStopAfterDropOffInsertion, departureTimeOfStopBeforeDropOffInsertion); + double totalInsertionCost = dropOffInsertionCost + pickUpInsertionCost; + if (totalInsertionCost < insertionCost) { + insertionCost = totalInsertionCost; + TimetableEntry dropOffStopToInsert = new TimetableEntry(request, TimetableEntry.StopType.DROP_OFF, + arrivalTimeDropOffStop, departureTimeDropOffStop, stopAfterDropOffInsertion.getOccupancyBeforeStop(), stopDuration, vehicleInfo.vehicle()); //Attention: currently, the occupancy before next stop is already increased! + candidateTimetable = insertDropOff(temporaryTimetable, j, dropOffStopToInsert, delayCausedByDropOffInsertion); + } + } else { + // Append drop off at the end + double travelTimeToDropOffStop = linkToLinkTravelTimeMatrix.getTravelTime(linkOfStopBeforeDropOffInsertion, toLink, departureTimeOfStopBeforeDropOffInsertion); + double arrivalTimeDropOffStop = departureTimeOfStopBeforeDropOffInsertion + travelTimeToDropOffStop; + if (arrivalTimeDropOffStop > request.getLatestArrivalTime() || arrivalTimeDropOffStop > serviceEndTime) { + continue; + } + double totalInsertionCost = pickUpInsertionCost + travelTimeToDropOffStop; + double departureTimeDropOffStop = arrivalTimeDropOffStop + stopDuration; + if (totalInsertionCost < insertionCost) { + insertionCost = totalInsertionCost; + TimetableEntry dropOffStopToInsert = new TimetableEntry(request, TimetableEntry.StopType.DROP_OFF, + arrivalTimeDropOffStop, departureTimeDropOffStop, 1, stopDuration, vehicleInfo.vehicle()); + candidateTimetable = insertDropOff(temporaryTimetable, j, dropOffStopToInsert, 0); + } + } + } + } + return new InsertionData(candidateTimetable, insertionCost, vehicleInfo); + } + + public void removeRequestFromSchedule(OnlineVehicleInfo vehicleInfo, GeneralRequest requestToRemove, + FleetSchedules previousSchedule) { + Id vehicleId = previousSchedule.requestIdToVehicleMap().get(requestToRemove.getPassengerIds()); + List timetable = previousSchedule.vehicleToTimetableMap().get(vehicleId); + + // remove the request from the timetable + // First identify the pick-up and drop-off index of the request, and update the occupancy of those impacted stops + int pickUpIdx = timetable.size(); + int dropOffIdx = timetable.size(); + for (int i = 0; i < timetable.size(); i++) { + TimetableEntry stop = timetable.get(i); + if (stop.getRequest().getPassengerIds().toString().equals(requestToRemove.getPassengerIds().toString())) { + if (stop.getStopType() == TimetableEntry.StopType.PICKUP) { + pickUpIdx = i; + } else { + dropOffIdx = i; + } + } + + if (i > pickUpIdx && i < dropOffIdx) { + // Reduce the occupancy before the stop by 1 + stop.decreaseOccupancyByOne(); + } + } + + // Remove the 2 stops + // Hint: remove the drop-off stop first, as drop-off stop is after the pick-up stop (i.e., no interference on the idx) + timetable.remove(dropOffIdx); + timetable.remove(pickUpIdx); + + // Update the timetable + for (int i = 0; i < timetable.size(); i++) { + TimetableEntry stop = timetable.get(i); + double departureTimeFromPreviousStop; + double updatedArrivalTime; + if (i == 0) { + departureTimeFromPreviousStop = vehicleInfo.divertableTime(); + updatedArrivalTime = departureTimeFromPreviousStop + linkToLinkTravelTimeMatrix. + getTravelTime(vehicleInfo.currentLink(), network.getLinks().get(stop.getLinkId()), departureTimeFromPreviousStop); + } else { + TimetableEntry previousStop = timetable.get(i - 1); + departureTimeFromPreviousStop = previousStop.getDepartureTime(); + updatedArrivalTime = departureTimeFromPreviousStop + linkToLinkTravelTimeMatrix. + getTravelTime(network.getLinks().get(previousStop.getLinkId()), network.getLinks().get(stop.getLinkId()), departureTimeFromPreviousStop); + } + stop.updateArrivalTime(updatedArrivalTime); + } + + // put the request in the rejection list + previousSchedule.requestIdToVehicleMap().remove(requestToRemove.getPassengerIds()); + previousSchedule.pendingRequests().put(requestToRemove.getPassengerIds(), requestToRemove); + } + + // Nested classes / Records + public record InsertionData(List candidateTimetable, double cost, OnlineVehicleInfo vehicleInfo) { + } + + // Private methods + private boolean isInsertionNotFeasible(List originalTimetable, int insertionIdx, double delay, double serviceEndTime) { + for (int i = insertionIdx; i < originalTimetable.size(); i++) { + TimetableEntry stop = originalTimetable.get(i); + double newArrivalTime = stop.getArrivalTime() + delay; + if (stop.isTimeConstraintViolated(delay) || newArrivalTime > serviceEndTime) { + return true; + } + delay = stop.getEffectiveDelayIfStopIsDelayedBy(delay); // Update the delay after this stop (as stop time of some stops may be squeezed) + if (delay <= 0) { + return false; // The delay becomes 0, then there will be no impact on the following stops --> feasible (not feasible = false) + } + } + return false; // If we reach here, then every stop is feasible (not feasible = false) + } + + + private List insertPickup(List originalTimetable, int pickUpIdx, + TimetableEntry stopToInsert, double delay) { + // Create a copy of the original timetable + List temporaryTimetable = FleetSchedules.copyTimetable(originalTimetable); + if (pickUpIdx < temporaryTimetable.size()) { + temporaryTimetable.add(pickUpIdx, stopToInsert); + for (int i = pickUpIdx + 1; i < temporaryTimetable.size(); i++) { + double effectiveDelay = temporaryTimetable.get(i).getEffectiveDelayIfStopIsDelayedBy(delay); + temporaryTimetable.get(i).delayTheStopBy(delay); + temporaryTimetable.get(i).increaseOccupancyByOne(); + delay = effectiveDelay; + // Update the delay carry over to the next stop + } + } else { + // insert at the end + temporaryTimetable.add(stopToInsert); + } + return temporaryTimetable; + } + + private List insertDropOff(List temporaryTimetable, int dropOffIdx, + TimetableEntry stopToInsert, double delay) { + // Note: Delay includes the Drop-off time + List candidateTimetable = new ArrayList<>(); + for (TimetableEntry timetableEntry : temporaryTimetable) { + candidateTimetable.add(new TimetableEntry(timetableEntry)); + } + + if (dropOffIdx < candidateTimetable.size()) { + candidateTimetable.add(dropOffIdx, stopToInsert); + for (int i = dropOffIdx + 1; i < candidateTimetable.size(); i++) { + double effectiveDelay = candidateTimetable.get(i).getEffectiveDelayIfStopIsDelayedBy(delay); + candidateTimetable.get(i).delayTheStopBy(delay); + candidateTimetable.get(i).decreaseOccupancyByOne(); + delay = effectiveDelay; // Update the delay carry over to the next stop + } + } else { + candidateTimetable.add(stopToInsert); // insert at the end + } + return candidateTimetable; + } + + +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/RollingHorizonDrtOperationModule.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/RollingHorizonDrtOperationModule.java new file mode 100644 index 00000000000..79ce9369128 --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/RollingHorizonDrtOperationModule.java @@ -0,0 +1,86 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization; + +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.population.Population; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.VrpSolver; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.VrpSolverJsprit; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.VrpSolverRegretHeuristic; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.VrpSolverSeqInsertion; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.ruin_and_recreate.VrpSolverRuinAndRecreate; +import org.matsim.contrib.drt.optimizer.DrtOptimizer; +import org.matsim.contrib.drt.optimizer.QSimScopeForkJoinPoolHolder; +import org.matsim.contrib.drt.optimizer.VehicleDataEntryFactoryImpl; +import org.matsim.contrib.drt.optimizer.VehicleEntry; +import org.matsim.contrib.drt.run.DrtConfigGroup; +import org.matsim.contrib.drt.schedule.DrtTaskFactory; +import org.matsim.contrib.dvrp.fleet.Fleet; +import org.matsim.contrib.dvrp.run.AbstractDvrpModeQSimModule; +import org.matsim.contrib.dvrp.schedule.ScheduleTimingUpdater; +import org.matsim.core.api.experimental.events.EventsManager; +import org.matsim.core.mobsim.framework.MobsimTimer; +import org.matsim.core.router.costcalculators.TravelDisutilityFactory; +import org.matsim.core.router.util.TravelTime; + +import java.util.Random; + +public class RollingHorizonDrtOperationModule extends AbstractDvrpModeQSimModule { + private final Population prebookedPlans; + private final DrtConfigGroup drtConfigGroup; + private final double horizon; + private final double interval; + private final int maxIteration; + private final boolean multiThread; + private final long seed; + private final OfflineSolverType offlineSolverType; + + public RollingHorizonDrtOperationModule(Population prebookedPlans, DrtConfigGroup drtConfigGroup, double horizon, + double interval, int maxIterations, boolean multiThread, long seed, OfflineSolverType type) { + super(drtConfigGroup.getMode()); + this.prebookedPlans = prebookedPlans; + this.drtConfigGroup = drtConfigGroup; + this.horizon = horizon; + this.interval = interval; + this.maxIteration = maxIterations; + this.multiThread = multiThread; + this.seed = seed; + this.offlineSolverType = type; + } + + public enum OfflineSolverType {JSPRIT, SEQ_INSERTION, REGRET_INSERTION, RUIN_AND_RECREATE} + + @Override + protected void configureQSim() { + addModalComponent(DrtOptimizer.class, this.modalProvider((getter) -> new RollingHorizonOfflineDrtOptimizer(getter.getModal(Network.class), getter.getModal(TravelTime.class), + getter.get(MobsimTimer.class), getter.getModal(DrtTaskFactory.class), + getter.get(EventsManager.class), getter.getModal(ScheduleTimingUpdater.class), + getter.getModal(TravelDisutilityFactory.class).createTravelDisutility(getter.getModal(TravelTime.class)), + drtConfigGroup, getter.getModal(Fleet.class), + getter.getModal(QSimScopeForkJoinPoolHolder.class).getPool(), + getter.getModal(VehicleEntry.EntryFactory.class), + getter.getModal(VrpSolver.class), + getter.get(Population.class), horizon, interval, prebookedPlans))); + + switch (offlineSolverType) { + case JSPRIT -> bindModal(VrpSolver.class).toProvider(modalProvider( + getter -> new VrpSolverJsprit( + new VrpSolverJsprit.Options(maxIteration, multiThread, new Random(seed)), + drtConfigGroup, getter.getModal(Network.class), getter.getModal(TravelTime.class)))); + case SEQ_INSERTION -> bindModal(VrpSolver.class).toProvider(modalProvider( + getter -> new VrpSolverSeqInsertion( + getter.getModal(Network.class), getter.getModal(TravelTime.class), drtConfigGroup))); + case REGRET_INSERTION -> bindModal(VrpSolver.class).toProvider(modalProvider( + getter -> new VrpSolverRegretHeuristic( + getter.getModal(Network.class), getter.getModal(TravelTime.class), drtConfigGroup))); + case RUIN_AND_RECREATE -> bindModal(VrpSolver.class).toProvider(modalProvider( + getter -> new VrpSolverRuinAndRecreate(maxIteration, + getter.getModal(Network.class), getter.getModal(TravelTime.class), drtConfigGroup, + new Random(seed)))); + default -> throw new RuntimeException("The solver is not implemented!"); + } + + addModalComponent(QSimScopeForkJoinPoolHolder.class, + () -> new QSimScopeForkJoinPoolHolder(drtConfigGroup.numberOfThreads)); + bindModal(VehicleEntry.EntryFactory.class).toInstance(new VehicleDataEntryFactoryImpl()); + + } +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/RollingHorizonOfflineDrtOptimizer.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/RollingHorizonOfflineDrtOptimizer.java new file mode 100644 index 00000000000..2c2c7413449 --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/RollingHorizonOfflineDrtOptimizer.java @@ -0,0 +1,412 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization; + +import com.google.common.base.Preconditions; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.population.Person; +import org.matsim.api.core.v01.population.Population; +import org.matsim.contrib.drt.extension.preplanned.optimizer.WaitForStopTask; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.FleetSchedules; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.GeneralRequest; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.OnlineVehicleInfo; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.TimetableEntry; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.VrpSolver; +import org.matsim.contrib.drt.optimizer.DrtOptimizer; +import org.matsim.contrib.drt.optimizer.VehicleEntry; +import org.matsim.contrib.drt.passenger.AcceptedDrtRequest; +import org.matsim.contrib.drt.passenger.DrtRequest; +import org.matsim.contrib.drt.run.DrtConfigGroup; +import org.matsim.contrib.drt.schedule.DrtDriveTask; +import org.matsim.contrib.drt.schedule.DrtStayTask; +import org.matsim.contrib.drt.schedule.DrtStopTask; +import org.matsim.contrib.drt.schedule.DrtTaskFactory; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; +import org.matsim.contrib.dvrp.fleet.Fleet; +import org.matsim.contrib.dvrp.optimizer.Request; +import org.matsim.contrib.dvrp.passenger.PassengerRequestRejectedEvent; +import org.matsim.contrib.dvrp.passenger.PassengerRequestScheduledEvent; +import org.matsim.contrib.dvrp.path.VrpPathWithTravelData; +import org.matsim.contrib.dvrp.path.VrpPaths; +import org.matsim.contrib.dvrp.schedule.*; +import org.matsim.contrib.dvrp.tracker.OnlineDriveTaskTracker; +import org.matsim.contrib.dvrp.util.LinkTimePair; +import org.matsim.core.api.experimental.events.EventsManager; +import org.matsim.core.mobsim.framework.MobsimTimer; +import org.matsim.core.mobsim.framework.events.MobsimBeforeSimStepEvent; +import org.matsim.core.router.TripStructureUtils; +import org.matsim.core.router.speedy.SpeedyALTFactory; +import org.matsim.core.router.util.LeastCostPathCalculator; +import org.matsim.core.router.util.TravelDisutility; +import org.matsim.core.router.util.TravelTime; + +import java.util.*; +import java.util.concurrent.ForkJoinPool; +import java.util.stream.Collectors; + +import static org.matsim.contrib.drt.schedule.DrtTaskBaseType.STAY; + +public class RollingHorizonOfflineDrtOptimizer implements DrtOptimizer { + private final Logger log = LogManager.getLogger(RollingHorizonOfflineDrtOptimizer.class); + private final Network network; + private final TravelTime travelTime; + private final MobsimTimer timer; + private final DrtTaskFactory taskFactory; + private final EventsManager eventsManager; + private final ScheduleTimingUpdater scheduleTimingUpdater; + private final LeastCostPathCalculator router; + private final double stopDuration; + private final String mode; + private final DrtConfigGroup drtCfg; + + private final Fleet fleet; + private final ForkJoinPool forkJoinPool; + private final VehicleEntry.EntryFactory vehicleEntryFactory; + + private final Map>, DrtRequest> openRequests = new HashMap<>(); + private final VrpSolver solver; + + private final double horizon; + private final double interval; + // Must be smaller than or equal to the horizon + private double serviceStartTime = Double.MAX_VALUE; + // Start time of the whole DRT service (will be set to the earliest starting time of all the fleet) + private double serviceEndTime = 0; + // End time of the whole DRT service (will be set to the latest ending time of all the fleet) + + private final List prebookedRequests = new ArrayList<>(); + + private double lastUpdateTimeOfFleetStatus; + + private FleetSchedules fleetSchedules; + Map, OnlineVehicleInfo> realTimeVehicleInfoMap = new LinkedHashMap<>(); + + /** + * This DRT optimizer handles both pre-booked requests and the spontaneous requests. + * Pre-booked requests will be optimized via rolling horizon approach with jsprit (later can + * work with other VRP solver). The spontaneous requests will be inserted to the timetable + * via a simple insertion heuristic* * + */ + public RollingHorizonOfflineDrtOptimizer(Network network, TravelTime travelTime, MobsimTimer timer, DrtTaskFactory taskFactory, + EventsManager eventsManager, ScheduleTimingUpdater scheduleTimingUpdater, + TravelDisutility travelDisutility, DrtConfigGroup drtCfg, + Fleet fleet, ForkJoinPool forkJoinPool, VehicleEntry.EntryFactory vehicleEntryFactory, + VrpSolver solver, Population plans, + double horizon, double interval, Population prebookedTrips) { + this.network = network; + this.travelTime = travelTime; + this.timer = timer; + this.taskFactory = taskFactory; + this.eventsManager = eventsManager; + this.scheduleTimingUpdater = scheduleTimingUpdater; + this.router = new SpeedyALTFactory().createPathCalculator(network, travelDisutility, travelTime); + this.stopDuration = drtCfg.stopDuration; + this.mode = drtCfg.getMode(); + this.drtCfg = drtCfg; + this.fleet = fleet; + this.forkJoinPool = forkJoinPool; + this.vehicleEntryFactory = vehicleEntryFactory; + this.solver = solver; + this.horizon = horizon; + this.interval = interval; + + initDrtSchedules(); + readPrebookedRequests(plans, prebookedTrips); + assert interval <= horizon : "Interval of optimization must be smaller than or equal to the horizon length!"; + + } + + @Override + public void requestSubmitted(Request request) { + assert timer.getTimeOfDay() != 0 : "Currently, we cannot deal with request submitted at t = 0. Please remove such requests!"; + + DrtRequest drtRequest = (DrtRequest) request; + var passengerIds = drtRequest.getPassengerIds(); + + if (fleetSchedules == null) { + eventsManager.processEvent(new PassengerRequestRejectedEvent(timer.getTimeOfDay(), mode, request.getId(), + passengerIds, "DRT fleet not yet starts its service")); + return; + } + + openRequests.put(((DrtRequest) request).getPassengerIds(), drtRequest); + + if (fleetSchedules.requestIdToVehicleMap().containsKey(passengerIds) + || fleetSchedules.pendingRequests().containsKey(passengerIds)) { + // This is a pre-booked request + Id vehicleId = fleetSchedules.requestIdToVehicleMap().get(passengerIds); + if (vehicleId == null) { + Preconditions.checkState(fleetSchedules.pendingRequests().containsKey(passengerIds), + "Pre-planned request (%s) not assigned to any vehicle and not marked as unassigned.", + passengerIds); + eventsManager.processEvent(new PassengerRequestRejectedEvent(timer.getTimeOfDay(), mode, request.getId(), + passengerIds, "Marked as unassigned")); + fleetSchedules.pendingRequests().remove(passengerIds); + return; + } + + eventsManager.processEvent( + new PassengerRequestScheduledEvent(timer.getTimeOfDay(), drtRequest.getMode(), drtRequest.getId(), + drtRequest.getPassengerIds(), vehicleId, Double.NaN, Double.NaN)); + // Currently, we don't provide the expected pickup / drop off time. Maybe update this in the future. + + } else { + // This is a spontaneous request + double now = timer.getTimeOfDay(); + log.error("At time = " + now + ", there is a spontaneous request, which is not yet supported. Aborting..."); + throw new RuntimeException("Spontaneous requests currently not supported..."); + } + } + + @Override + public void nextTask(DvrpVehicle vehicle) { + // TODO potential place to update vehicle timetable + scheduleTimingUpdater.updateBeforeNextTask(vehicle); + var schedule = vehicle.getSchedule(); + + if (schedule.getStatus() == Schedule.ScheduleStatus.PLANNED) { + schedule.nextTask(); + return; + } + + var currentTask = schedule.getCurrentTask(); + var currentLink = Tasks.getEndLink(currentTask); + double currentTime = timer.getTimeOfDay(); + + var stopsToVisit = fleetSchedules.vehicleToTimetableMap().get(vehicle.getId()); + + if (stopsToVisit.isEmpty()) { + // no preplanned stops for the vehicle within current horizon + if (currentTime < vehicle.getServiceEndTime()) { + // fill the time gap with STAY + schedule.addTask(taskFactory.createStayTask(vehicle, currentTime, vehicle.getServiceEndTime(), currentLink)); + } else if (!STAY.isBaseTypeOf(currentTask)) { + // we need to end the schedule with STAY task even if it is delayed + schedule.addTask(taskFactory.createStayTask(vehicle, currentTime, currentTime, currentLink)); + } + } else { + var nextStop = stopsToVisit.get(0); + if (!nextStop.getLinkId().equals(currentLink.getId())) { + // Next stop is at another location? --> Add a drive task + var nextLink = network.getLinks().get(nextStop.getLinkId()); + VrpPathWithTravelData path = VrpPaths.calcAndCreatePath(currentLink, nextLink, currentTime, router, + travelTime); + schedule.addTask(taskFactory.createDriveTask(vehicle, path, DrtDriveTask.TYPE)); + } else if (nextStop.getRequest().getEarliestDepartureTime() >= timer.getTimeOfDay()) { + // We are at the stop location. But we are too early. --> Add a wait for stop task + // Currently assuming the mobsim time step is 1 s + schedule.addTask(new WaitForStopTask(currentTime, + nextStop.getRequest().getEarliestDepartureTime() + 1, currentLink)); + } else { + // We are ready for the stop task! --> Add stop task to the schedule + var stopTask = taskFactory.createStopTask(vehicle, currentTime, currentTime + stopDuration, currentLink); + if (nextStop.getStopType() == TimetableEntry.StopType.PICKUP) { + var request = Preconditions.checkNotNull(openRequests.get(nextStop.getRequest().getPassengerIds()), + "Request (%s) has not been yet submitted", nextStop.getRequest()); + stopTask.addPickupRequest(AcceptedDrtRequest.createFromOriginalRequest(request)); + } else { + var request = Preconditions.checkNotNull(openRequests.remove(nextStop.getRequest().getPassengerIds()), + "Request (%s) has not been yet submitted", nextStop.getRequest()); + stopTask.addDropoffRequest(AcceptedDrtRequest.createFromOriginalRequest(request)); + fleetSchedules.requestIdToVehicleMap().remove(request.getPassengerIds()); + } + schedule.addTask(stopTask); + stopsToVisit.remove(0); //remove the first entry in the stops to visit list + } + } + + // switch to the next task and update currentTasks + schedule.nextTask(); + } + + @Override + public void notifyMobsimBeforeSimStep(MobsimBeforeSimStepEvent mobsimBeforeSimStepEvent) { + double now = mobsimBeforeSimStepEvent.getSimulationTime(); + + if (now % interval == 1 && now >= serviceStartTime && now < serviceEndTime) { + // Update vehicle current information + updateFleetStatus(now); + + // Read new requests + List newRequests = readRequestsFromTimeBin(now); + + // Calculate the new preplanned schedule + double endTime = now + horizon; + log.info("Calculating the plan for t =" + now + " to t = " + endTime); + log.info("There are " + newRequests.size() + " new request within this horizon"); + fleetSchedules = solver.calculate(fleetSchedules, realTimeVehicleInfoMap, newRequests, now); + + // Update vehicles schedules (i.e., current task) + for (OnlineVehicleInfo onlineVehicleInfo : realTimeVehicleInfoMap.values()) { + updateVehicleCurrentTask(onlineVehicleInfo, now); + } + } + } + + // Static functions + static GeneralRequest createFromDrtRequest(DrtRequest drtRequest) { + return new GeneralRequest(drtRequest.getPassengerIds(), drtRequest.getFromLink().getId(), + drtRequest.getToLink().getId(), drtRequest.getEarliestStartTime(), drtRequest.getLatestStartTime(), + drtRequest.getLatestArrivalTime()); + } + + // Private functions + private void initDrtSchedules() { + for (DvrpVehicle veh : fleet.getVehicles().values()) { + // Identify the earliest starting time and/or latest ending time of the fleet + if (veh.getServiceBeginTime() < serviceStartTime) { + serviceStartTime = veh.getServiceBeginTime(); + } + if (veh.getServiceEndTime() > serviceEndTime) { + serviceEndTime = veh.getServiceEndTime(); + } + + veh.getSchedule().addTask(taskFactory.createStayTask(veh, veh.getServiceBeginTime(), veh.getServiceEndTime(), veh.getStartLink())); + } + } + + private void readPrebookedRequests(Population plans, Population prebookedTrips) { + int counter = 0; + for (Person person : plans.getPersons().values()) { + if (!prebookedTrips.getPersons().containsKey(person.getId())) { + continue; + } + for (var leg : TripStructureUtils.getLegs(person.getSelectedPlan())) { + if (!leg.getMode().equals(mode)) { + continue; + } + var startLink = network.getLinks().get(leg.getRoute().getStartLinkId()); + var endLink = network.getLinks().get(leg.getRoute().getEndLinkId()); + double earliestPickupTime = leg.getDepartureTime().seconds(); + double latestPickupTime = earliestPickupTime + drtCfg.maxWaitTime; + double estimatedDirectTravelTime = VrpPaths.calcAndCreatePath(startLink, endLink, earliestPickupTime, router, travelTime).getTravelTime(); + double latestArrivalTime = earliestPickupTime + drtCfg.maxTravelTimeAlpha * estimatedDirectTravelTime + drtCfg.maxTravelTimeBeta; + DrtRequest drtRequest = DrtRequest.newBuilder() + .id(Id.create(person.getId().toString() + "_" + counter, Request.class)) + .submissionTime(earliestPickupTime) + .earliestStartTime(earliestPickupTime) + .latestStartTime(latestPickupTime) + .latestArrivalTime(latestArrivalTime) + .passengerIds(List.of(person.getId())) + .mode(mode) + .fromLink(startLink) + .toLink(endLink) + .build(); + prebookedRequests.add(drtRequest); + counter++; + } + } + log.info("There are " + counter + " pre-booked trips"); + } + + private List readRequestsFromTimeBin(double now) { + List newRequests = new ArrayList<>(); + for (DrtRequest prebookedRequest : prebookedRequests) { + double latestDepartureTime = now + horizon; + if (prebookedRequest.getEarliestStartTime() < latestDepartureTime) { + newRequests.add(prebookedRequest); + } + } + prebookedRequests.removeAll(newRequests); + return newRequests.stream().map(RollingHorizonOfflineDrtOptimizer::createFromDrtRequest).collect(Collectors.toList()); + } + + private void updateFleetStatus(double now) { + // TODO potential place to update vehicle timetable + // This function only needs to be performed once for each time step + if (now != lastUpdateTimeOfFleetStatus) { + for (DvrpVehicle v : fleet.getVehicles().values()) { + scheduleTimingUpdater.updateTimings(v); + } + + var vehicleEntries = forkJoinPool.submit(() -> fleet.getVehicles() + .values() + .parallelStream() + .map(v -> vehicleEntryFactory.create(v, now)) + .filter(Objects::nonNull) + .collect(Collectors.toMap(e -> e.vehicle.getId(), e -> e))).join(); + + for (VehicleEntry vehicleEntry : vehicleEntries.values()) { + Schedule schedule = vehicleEntry.vehicle.getSchedule(); + Task currentTask = schedule.getCurrentTask(); + + Link currentLink = null; + double divertableTime = Double.NaN; + + if (currentTask instanceof DrtStayTask) { + currentLink = ((DrtStayTask) currentTask).getLink(); + divertableTime = now; + } + + if (currentTask instanceof WaitForStopTask) { + currentLink = ((WaitForStopTask) currentTask).getLink(); + divertableTime = now; + } + + if (currentTask instanceof DriveTask) { + LinkTimePair diversion = ((OnlineDriveTaskTracker) currentTask.getTaskTracker()).getDiversionPoint(); + currentLink = diversion.link; + divertableTime = diversion.time; + } + + if (currentTask instanceof DrtStopTask) { + currentLink = ((DrtStopTask) currentTask).getLink(); + divertableTime = currentTask.getEndTime(); + } + + Preconditions.checkState(currentLink != null, "Current link should not be null! Vehicle ID = " + vehicleEntry.vehicle.getId().toString()); + Preconditions.checkState(!Double.isNaN(divertableTime), "Divertable time should not be NaN! Vehicle ID = " + vehicleEntry.vehicle.getId().toString()); + OnlineVehicleInfo onlineVehicleInfo = new OnlineVehicleInfo(vehicleEntry.vehicle, currentLink, divertableTime); + realTimeVehicleInfoMap.put(vehicleEntry.vehicle.getId(), onlineVehicleInfo); + } + + lastUpdateTimeOfFleetStatus = now; + } + } + + private void updateVehicleCurrentTask(OnlineVehicleInfo onlineVehicleInfo, double now) { + DvrpVehicle vehicle = onlineVehicleInfo.vehicle(); + Schedule schedule = vehicle.getSchedule(); + Task currentTask = schedule.getCurrentTask(); + Link currentLink = onlineVehicleInfo.currentLink(); + double divertableTime = onlineVehicleInfo.divertableTime(); + List timetable = fleetSchedules.vehicleToTimetableMap().get(vehicle.getId()); + + // Stay task: end stay task now if timetable is non-empty + if (currentTask instanceof DrtStayTask && !timetable.isEmpty()) { + currentTask.setEndTime(now); + } + + // Wait for stop task: end this task if first timetable entry has changed + if (currentTask instanceof WaitForStopTask) { + currentTask.setEndTime(now); + //Note: currently, it's not easy to check if the first entry in timetable is changed. + // We just end this task (a new wait for stop task will be generated at "nextTask" section if needed) + } + + // Drive task: Divert the drive task when needed + if (currentTask instanceof DrtDriveTask) { + if (timetable.isEmpty()) { + // stop the vehicle at divertable location and time (a stay task will be appended in the "nextTask" section) + var dummyPath = VrpPaths.calcAndCreatePath(currentLink, currentLink, divertableTime, router, travelTime); + ((OnlineDriveTaskTracker) currentTask.getTaskTracker()).divertPath(dummyPath); + } else { + // Divert the vehicle if destination has changed + assert timetable.get(0) != null; + Id newDestination = timetable.get(0).getLinkId(); + Id oldDestination = ((DrtDriveTask) currentTask).getPath().getToLink().getId(); + if (!oldDestination.toString().equals(newDestination.toString())) { + var newPath = VrpPaths.calcAndCreatePath(currentLink, + network.getLinks().get(newDestination), divertableTime, router, travelTime); + ((OnlineDriveTaskTracker) currentTask.getTaskTracker()).divertPath(newPath); + } + } + } + + // Stop task: nothing need to be done here + + } + +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/FleetSchedules.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/FleetSchedules.java new file mode 100644 index 00000000000..0255f13422d --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/FleetSchedules.java @@ -0,0 +1,87 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; + +public record FleetSchedules( + Map, List> vehicleToTimetableMap, + Map>, Id> requestIdToVehicleMap, + Map>, GeneralRequest> pendingRequests) { + + public static List copyTimetable(List timetable) { + List timetableCopy = new ArrayList<>(); + for (TimetableEntry timetableEntry : timetable) { + timetableCopy.add(new TimetableEntry(timetableEntry)); + } + return timetableCopy; + } + + public static FleetSchedules initializeFleetSchedules(Map, OnlineVehicleInfo> onlineVehicleInfoMap) { + Map, List> vehicleToTimetableMap = new LinkedHashMap<>(); + for (OnlineVehicleInfo vehicleInfo : onlineVehicleInfoMap.values()) { + vehicleToTimetableMap.put(vehicleInfo.vehicle().getId(), new ArrayList<>()); + } + Map>, Id> requestIdToVehicleMap = new HashMap<>(); + Map>, GeneralRequest> rejectedRequests = new LinkedHashMap<>(); + return new FleetSchedules(vehicleToTimetableMap, requestIdToVehicleMap, rejectedRequests); + } + + public FleetSchedules copySchedule() { + Map, List> vehicleToTimetableMapCopy = new LinkedHashMap<>(); + for (Id vehicleId : this.vehicleToTimetableMap().keySet()) { + vehicleToTimetableMapCopy.put(vehicleId, copyTimetable(this.vehicleToTimetableMap.get(vehicleId))); + } + var requestIdToVehicleMapCopy = new HashMap<>(this.requestIdToVehicleMap); + var rejectedRequestsCopy = new LinkedHashMap<>(this.pendingRequests); + + return new FleetSchedules(vehicleToTimetableMapCopy, requestIdToVehicleMapCopy, rejectedRequestsCopy); + } + + public void updateFleetSchedule(Network network, LinkToLinkTravelTimeMatrix linkToLinkTravelTimeMatrix, + Map, OnlineVehicleInfo> onlineVehicleInfoMap) { + for (Id vehicleId : onlineVehicleInfoMap.keySet()) { + // When new vehicle enters service, create a new entry for it + this.vehicleToTimetableMap().computeIfAbsent(vehicleId, t -> new ArrayList<>()); + if (!onlineVehicleInfoMap.containsKey(vehicleId)) { + // When a vehicle ends service, remove it from the schedule + this.vehicleToTimetableMap().remove(vehicleId); + } + } + + for (Id vehicleId : this.vehicleToTimetableMap().keySet()) { + List timetable = this.vehicleToTimetableMap().get(vehicleId); + if (!timetable.isEmpty()) { + Link currentLink = onlineVehicleInfoMap.get(vehicleId).currentLink(); + double currentTime = onlineVehicleInfoMap.get(vehicleId).divertableTime(); + for (TimetableEntry timetableEntry : timetable) { + Id stopLinkId = timetableEntry.getStopType() == TimetableEntry.StopType.PICKUP ? + timetableEntry.getRequest().getFromLinkId() : timetableEntry.getRequest().getToLinkId(); + Link stopLink = network.getLinks().get(stopLinkId); + double newArrivalTime = currentTime + linkToLinkTravelTimeMatrix.getTravelTime(currentLink, stopLink, currentTime); + timetableEntry.updateArrivalTime(newArrivalTime); + + // Delay the latest arrival time of the stop when necessary (e.g., due to traffic uncertainty), in order to make sure assigned requests will remain feasible + if (timetableEntry.getStopType() == TimetableEntry.StopType.PICKUP) { + double originalLatestDepartureTime = timetableEntry.getRequest().getLatestDepartureTime(); + timetableEntry.getRequest().setLatestDepartureTime(Math.max(originalLatestDepartureTime, newArrivalTime)); + } else { + double originalLatestArrivalTime = timetableEntry.getRequest().getLatestArrivalTime(); + timetableEntry.getRequest().setLatestArrivalTime(Math.max(originalLatestArrivalTime, newArrivalTime)); + } + + currentTime = timetableEntry.getDepartureTime(); + currentLink = stopLink; + } + } + } + } +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/GeneralRequest.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/GeneralRequest.java new file mode 100644 index 00000000000..110902cbb30 --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/GeneralRequest.java @@ -0,0 +1,62 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures; + +import java.util.List; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.population.Person; + +public class GeneralRequest { + private final List> passengerIds; + private final Id fromLinkId; + private final Id toLinkId; + private final double earliestDepartureTime; + + // latest departure time (flexibility is needed to account for traffic uncertainty) + private double latestDepartureTime; + // latest arrival time (flexibility is needed to account for traffic uncertainty) + private double latestArrivalTime; + + public GeneralRequest(List> passengerIds, Id fromLinkId, Id toLinkId, double earliestDepartureTime, + double latestStartTime, double latestArrivalTime) { + this.passengerIds = passengerIds; + this.fromLinkId = fromLinkId; + this.toLinkId = toLinkId; + this.earliestDepartureTime = earliestDepartureTime; + this.latestDepartureTime = latestStartTime; + this.latestArrivalTime = latestArrivalTime; + } + + public List> getPassengerIds() { + return passengerIds; + } + + public double getEarliestDepartureTime() { + return earliestDepartureTime; + } + + public double getLatestArrivalTime() { + return latestArrivalTime; + } + + public double getLatestDepartureTime() { + return latestDepartureTime; + } + + public Id getFromLinkId() { + return fromLinkId; + } + + public Id getToLinkId() { + return toLinkId; + } + + public void setLatestArrivalTime(double latestArrivalTime) { + this.latestArrivalTime = latestArrivalTime; + } + + public void setLatestDepartureTime(double latestDepartureTime) { + this.latestDepartureTime = latestDepartureTime; + } +} + diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/LinkToLinkTravelTimeMatrix.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/LinkToLinkTravelTimeMatrix.java new file mode 100644 index 00000000000..87a7ed1f91a --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/LinkToLinkTravelTimeMatrix.java @@ -0,0 +1,123 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures; + +import one.util.streamex.EntryStream; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.network.Node; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; +import org.matsim.contrib.dvrp.path.VrpPaths; +import org.matsim.contrib.dvrp.router.TimeAsTravelDisutility; +import org.matsim.contrib.zone.Zone; +import org.matsim.contrib.zone.skims.Matrix; +import org.matsim.contrib.zone.skims.TravelTimeMatrices; +import org.matsim.contrib.zone.skims.TravelTimeMatrix; +import org.matsim.core.router.util.TravelTime; + +import java.util.*; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toMap; +import static org.matsim.contrib.dvrp.path.VrpPaths.FIRST_LINK_TT; + +/** + * Link to link travel time to be used by the offline solver * + */ +public class LinkToLinkTravelTimeMatrix { + private final TravelTimeMatrix nodeToNodeTravelTimeMatrix; + private final TravelTime travelTime; + private final Network network; + + LinkToLinkTravelTimeMatrix(Network network, TravelTime travelTime, Set> relevantLinks, double time) { + this.network = network; + this.travelTime = travelTime; + this.nodeToNodeTravelTimeMatrix = calculateTravelTimeMatrix(relevantLinks, time); + } + + public static LinkToLinkTravelTimeMatrix prepareLinkToLinkTravelMatrix(Network network, TravelTime travelTime, FleetSchedules previousSchedules, + Map, OnlineVehicleInfo> onlineVehicleInfoMap, List newRequests, + double time) { + Set> relevantLinks = new HashSet<>(); + + // Vehicle locations + for (OnlineVehicleInfo onlineVehicleInfo : onlineVehicleInfoMap.values()) { + relevantLinks.add(onlineVehicleInfo.currentLink().getId()); + } + + // Requests locations + // requests on the timetable + for (List timetable : previousSchedules.vehicleToTimetableMap().values()) { + for (TimetableEntry timetableEntry : timetable) { + if (timetableEntry.getStopType() == TimetableEntry.StopType.PICKUP) { + relevantLinks.add(timetableEntry.getRequest().getFromLinkId()); + } else { + relevantLinks.add(timetableEntry.getRequest().getToLinkId()); + } + } + } + + // new requests + for (GeneralRequest request : newRequests) { + relevantLinks.add(request.getFromLinkId()); + relevantLinks.add(request.getToLinkId()); + } + + // Pending rejected requests (i.e., not yet properly inserted and not yet formally rejected) + for (GeneralRequest request : previousSchedules.pendingRequests().values()) { + relevantLinks.add(request.getFromLinkId()); + relevantLinks.add(request.getToLinkId()); + } + + return new LinkToLinkTravelTimeMatrix(network, travelTime, relevantLinks, time); + } + + @Deprecated + public void updateFleetSchedule(FleetSchedules previousSchedules, + Map, OnlineVehicleInfo> onlineVehicleInfoMap) { + for (Id vehicleId : onlineVehicleInfoMap.keySet()) { + previousSchedules.vehicleToTimetableMap().computeIfAbsent(vehicleId, t -> new ArrayList<>()); // When new vehicle enters service, create a new entry for it + if (!onlineVehicleInfoMap.containsKey(vehicleId)) { + previousSchedules.vehicleToTimetableMap().remove(vehicleId); // When a vehicle ends service, remove it from the schedule + } + } + + for (Id vehicleId : previousSchedules.vehicleToTimetableMap().keySet()) { + List timetable = previousSchedules.vehicleToTimetableMap().get(vehicleId); + if (!timetable.isEmpty()) { + Link currentLink = onlineVehicleInfoMap.get(vehicleId).currentLink(); + double currentTime = onlineVehicleInfoMap.get(vehicleId).divertableTime(); + for (TimetableEntry timetableEntry : timetable) { + Id stopLinkId = timetableEntry.getStopType() == TimetableEntry.StopType.PICKUP ? + timetableEntry.getRequest().getFromLinkId() : timetableEntry.getRequest().getToLinkId(); + Link stopLink = network.getLinks().get(stopLinkId); + double newArrivalTime = currentTime + this.getTravelTime(currentLink, stopLink, currentTime); + timetableEntry.updateArrivalTime(newArrivalTime); + currentTime = timetableEntry.getDepartureTime(); + currentLink = stopLink; + } + } + } + } + + public double getTravelTime(Link fromLink, Link toLink, double departureTime) { + if (fromLink.getId().toString().equals(toLink.getId().toString())) { + return 0; + } + double travelTimeFromNodeToNode = nodeToNodeTravelTimeMatrix.getTravelTime(fromLink.getToNode(), toLink.getFromNode(), departureTime); + return FIRST_LINK_TT + travelTimeFromNodeToNode + + VrpPaths.getLastLinkTT(travelTime, toLink, departureTime + travelTimeFromNodeToNode); + } + + private TravelTimeMatrix calculateTravelTimeMatrix(Set> relevantLinks, double time) { + Map zoneByNode = relevantLinks + .stream() + .flatMap(linkId -> Stream.of(network.getLinks().get(linkId).getFromNode(), network.getLinks().get(linkId).getToNode())) + .collect(toMap(n -> n, node -> new Zone(Id.create(node.getId(), Zone.class), "node", node.getCoord()), + (zone1, zone2) -> zone1)); + var nodeByZone = EntryStream.of(zoneByNode).invert().toMap(); + Matrix nodeToNodeMatrix = TravelTimeMatrices.calculateTravelTimeMatrix(new TravelTimeMatrices.RoutingParams(network, travelTime, + new TimeAsTravelDisutility(travelTime), Runtime.getRuntime().availableProcessors()), nodeByZone, time); + + return (fromNode, toNode, departureTime) -> nodeToNodeMatrix.get(zoneByNode.get(fromNode), zoneByNode.get(toNode)); + } +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/OnlineVehicleInfo.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/OnlineVehicleInfo.java new file mode 100644 index 00000000000..3cd386e3806 --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/OnlineVehicleInfo.java @@ -0,0 +1,7 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures; + +import org.matsim.api.core.v01.network.Link; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; + +public record OnlineVehicleInfo(DvrpVehicle vehicle, Link currentLink, double divertableTime) { +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/TimetableEntry.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/TimetableEntry.java new file mode 100644 index 00000000000..3cab994090e --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/basic_structures/TimetableEntry.java @@ -0,0 +1,156 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.network.Link; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; + +public class TimetableEntry { + + public enum StopType {PICKUP, DROP_OFF} + + private final GeneralRequest request; + private final StopType stopType; + private double arrivalTime; + private double departureTime; + private int occupancyBeforeStop; + private final double stopDuration; + private final int capacity; + private double slackTime; + + public TimetableEntry(GeneralRequest request, StopType stopType, double arrivalTime, + double departureTime, int occupancyBeforeStop, double stopDuration, + DvrpVehicle vehicle) { + this.request = request; + this.stopType = stopType; + this.arrivalTime = arrivalTime; + this.departureTime = departureTime; + this.occupancyBeforeStop = occupancyBeforeStop; + this.stopDuration = stopDuration; + this.capacity = vehicle.getCapacity(); + this.slackTime = departureTime - (stopDuration + arrivalTime); + } + + /** + * Make a copy of the object + */ + public TimetableEntry(TimetableEntry timetableEntry) { + this.request = timetableEntry.request; + this.stopType = timetableEntry.stopType; + this.arrivalTime = timetableEntry.arrivalTime; + this.departureTime = timetableEntry.departureTime; + this.occupancyBeforeStop = timetableEntry.occupancyBeforeStop; + this.stopDuration = timetableEntry.stopDuration; + this.capacity = timetableEntry.capacity; + this.slackTime = timetableEntry.slackTime; + } + + @Deprecated + double delayTheStop(double delay) { + double effectiveDelay = getEffectiveDelayIfStopIsDelayedBy(delay); + arrivalTime += delay; + departureTime += effectiveDelay; + return effectiveDelay; + } + + public void increaseOccupancyByOne() { + occupancyBeforeStop += 1; + } + + public void decreaseOccupancyByOne() { + occupancyBeforeStop -= 1; + } + + public double getEffectiveDelayIfStopIsDelayedBy(double delay) { + double departureTimeAfterAddingDelay = Math.max(arrivalTime + delay, getEarliestDepartureTime()) + stopDuration; + return departureTimeAfterAddingDelay - departureTime; + } + + public void delayTheStopBy(double delay) { + // Note: delay can be negative (i.e., bring forward) + arrivalTime += delay; + departureTime = Math.max(arrivalTime, getEarliestDepartureTime()) + stopDuration; + slackTime = departureTime - (arrivalTime + stopDuration); + } + + public void updateArrivalTime(double newArrivalTime) { + arrivalTime = newArrivalTime; + departureTime = Math.max(arrivalTime, getEarliestDepartureTime()) + stopDuration; + slackTime = departureTime - (arrivalTime + stopDuration); + } + + // Checking functions + public boolean isTimeConstraintViolated(double delay) { + return arrivalTime + delay > getLatestArrivalTime(); + } + + @Deprecated + double checkDelayFeasibilityAndReturnEffectiveDelay(double delay) { + double effectiveDelay = getEffectiveDelayIfStopIsDelayedBy(delay); + if (isTimeConstraintViolated(delay)) { + return -1; // if not feasible, then return -1 //TODO do not use this anymore, as delay can now be negative also!!! + } + return effectiveDelay; + } + + public boolean isVehicleFullBeforeThisStop() { + return occupancyBeforeStop >= capacity; + } + + public boolean isVehicleOverloaded() { + return stopType == StopType.PICKUP ? occupancyBeforeStop >= capacity : occupancyBeforeStop > capacity; + } + + // Getter functions + public GeneralRequest getRequest() { + return request; + } + + public double getArrivalTime() { + return arrivalTime; + } + + public double getDepartureTime() { + return departureTime; + } + + public Id getLinkId() { + if (stopType == StopType.PICKUP) { + return request.getFromLinkId(); + } + return request.getToLinkId(); + } + + public int getOccupancyBeforeStop() { + return occupancyBeforeStop; + } + + public double getEarliestDepartureTime() { + return request.getEarliestDepartureTime(); + } + + public double getSlackTime() { + return slackTime; + } + + public double getLatestArrivalTime() { + return stopType == StopType.PICKUP ? request.getLatestDepartureTime() : request.getLatestArrivalTime(); + } + + public StopType getStopType() { + return stopType; + } + + @Override + public String toString() { + return "TimetableEntry{" + + "request=" + request.getPassengerIds().toString() + + ", stopType=" + stopType + + ", arrivalTime=" + arrivalTime + + ", departureTime=" + departureTime + + ", occupancyBeforeStop=" + occupancyBeforeStop + + ", stopDuration=" + stopDuration + + ", capacity=" + capacity + + ", slackTime=" + slackTime + + '}'; + } +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/VrpSolver.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/VrpSolver.java new file mode 100644 index 00000000000..6437225668d --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/VrpSolver.java @@ -0,0 +1,16 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver; + +import org.matsim.api.core.v01.Id; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.FleetSchedules; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.GeneralRequest; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.OnlineVehicleInfo; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; + +import java.util.List; +import java.util.Map; + +public interface VrpSolver { + FleetSchedules calculate(FleetSchedules previousSchedules, + Map, OnlineVehicleInfo> onlineVehicleInfoMap, + List newRequests, double time); +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/VrpSolverJsprit.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/VrpSolverJsprit.java new file mode 100644 index 00000000000..ef8d99a603c --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/VrpSolverJsprit.java @@ -0,0 +1,406 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver; + +import com.graphhopper.jsprit.core.algorithm.box.Jsprit; +import com.graphhopper.jsprit.core.problem.Location; +import com.graphhopper.jsprit.core.problem.VehicleRoutingProblem; +import com.graphhopper.jsprit.core.problem.cost.VehicleRoutingTransportCosts; +import com.graphhopper.jsprit.core.problem.driver.Driver; +import com.graphhopper.jsprit.core.problem.job.Job; +import com.graphhopper.jsprit.core.problem.job.Shipment; +import com.graphhopper.jsprit.core.problem.solution.SolutionCostCalculator; +import com.graphhopper.jsprit.core.problem.solution.VehicleRoutingProblemSolution; +import com.graphhopper.jsprit.core.problem.solution.route.VehicleRoute; +import com.graphhopper.jsprit.core.problem.solution.route.activity.PickupShipment; +import com.graphhopper.jsprit.core.problem.solution.route.activity.TimeWindow; +import com.graphhopper.jsprit.core.problem.solution.route.activity.TourActivity; +import com.graphhopper.jsprit.core.problem.vehicle.Vehicle; +import com.graphhopper.jsprit.core.problem.vehicle.VehicleImpl; +import com.graphhopper.jsprit.core.problem.vehicle.VehicleTypeImpl; +import com.graphhopper.jsprit.core.util.Coordinate; +import com.graphhopper.jsprit.core.util.Solutions; +import one.util.streamex.EntryStream; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.IdMap; +import org.matsim.api.core.v01.network.Link; +import org.matsim.api.core.v01.network.Network; +import org.matsim.api.core.v01.network.Node; +import org.matsim.api.core.v01.population.Person; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.FleetSchedules; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.OnlineVehicleInfo; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.GeneralRequest; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.TimetableEntry; +import org.matsim.contrib.drt.run.DrtConfigGroup; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; +import org.matsim.contrib.dvrp.path.VrpPaths; +import org.matsim.contrib.dvrp.router.TimeAsTravelDisutility; +import org.matsim.contrib.zone.Zone; +import org.matsim.contrib.zone.skims.Matrix; +import org.matsim.contrib.zone.skims.TravelTimeMatrices; +import org.matsim.contrib.zone.skims.TravelTimeMatrix; +import org.matsim.core.router.util.TravelDisutility; +import org.matsim.core.router.util.TravelTime; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toMap; +import static org.matsim.contrib.dvrp.path.VrpPaths.FIRST_LINK_TT; + +public class VrpSolverJsprit implements VrpSolver { + private final Options options; + private final DrtConfigGroup drtCfg; + private final Network network; + private final TravelTime travelTime; + private final TravelDisutility travelDisutility; + private final Map, Location> locationByLinkId = new IdMap<>(Link.class); + + public static final double REJECTION_COST = 100000; + + public VrpSolverJsprit(Options options, DrtConfigGroup drtCfg, Network network, TravelTime travelTime) { + this.options = options; + this.drtCfg = drtCfg; + this.network = network; + this.travelTime = travelTime; + this.travelDisutility = new TimeAsTravelDisutility(travelTime); + } + + @Override + public FleetSchedules calculate(FleetSchedules previousSchedules, + Map, OnlineVehicleInfo> onlineVehicleInfoMap, + List newRequests, double time) { + locationByLinkId.clear(); + // Create PDPTW problem + var vrpBuilder = new VehicleRoutingProblem.Builder(); + // 1. Vehicle + Map, VehicleImpl> vehicleIdToJSpritVehicleMap = new HashMap<>(); + for (OnlineVehicleInfo vehicleInfo : onlineVehicleInfoMap.values()) { + DvrpVehicle vehicle = vehicleInfo.vehicle(); + Link currentLink = vehicleInfo.currentLink(); + double divertableTime = vehicleInfo.divertableTime(); + + int capacity = vehicle.getCapacity(); + var vehicleType = VehicleTypeImpl.Builder.newInstance(drtCfg.getMode() + "-vehicle-" + capacity + "-seats") + .addCapacityDimension(0, capacity) + .build(); + double serviceEndTime = vehicle.getServiceEndTime(); + var vehicleBuilder = VehicleImpl.Builder.newInstance(vehicle.getId() + ""); + vehicleBuilder.setEarliestStart(divertableTime); + vehicleBuilder.setLatestArrival(serviceEndTime); + vehicleBuilder.setStartLocation(collectLocationIfAbsent(currentLink)); + vehicleBuilder.setReturnToDepot(false); + vehicleBuilder.setType(vehicleType); + vehicleBuilder.addSkill(vehicle.getId().toString()); // Vehicle skills can be used to make sure the request already onboard will be matched to the same vehicle + VehicleImpl jSpritVehicle = vehicleBuilder.build(); + vrpBuilder.addVehicle(jSpritVehicle); + vehicleIdToJSpritVehicleMap.put(vehicle.getId(), jSpritVehicle); + } + + // 2. Request + var preplannedRequestByShipmentId = new HashMap(); + Map, List> requestsOnboardEachVehicles = new HashMap<>(); + newRequests.forEach(drtRequest -> collectLocationIfAbsent(network.getLinks().get(drtRequest.getFromLinkId()))); + newRequests.forEach(drtRequest -> collectLocationIfAbsent(network.getLinks().get(drtRequest.getToLinkId()))); + + if (previousSchedules != null) { + for (Id vehicleId : previousSchedules.vehicleToTimetableMap().keySet()) { + for (TimetableEntry stop : previousSchedules.vehicleToTimetableMap().get(vehicleId)) { + if (stop.getStopType() == TimetableEntry.StopType.PICKUP) { + Id getFromLinkId = stop.getRequest().getFromLinkId(); + collectLocationIfAbsent(network.getLinks().get(getFromLinkId)); + } else { + Id getToLinkId = stop.getRequest().getToLinkId(); + collectLocationIfAbsent(network.getLinks().get(getToLinkId)); + } + } + } + } + + // Calculate link to link travel time matrix and initialize VRP costs + TravelTimeMatrix travelTimeMatrix = createTravelTimeMatrix(time); + MatrixBasedVrpCosts vrpCosts = new MatrixBasedVrpCosts(travelTimeMatrix, time, network, travelTime); + vrpBuilder.setRoutingCost(vrpCosts); + List routesForInitialSolutions = new ArrayList<>(); + List unassignedShipments = new ArrayList<>(); //Used for initial solution + + Map requestToShipmentMap = new HashMap<>(); + // 2.1 Passengers already assigned + // When creating the shipment, we may need to postpone the pickup/delivery deadlines, in order to keep the original solution still remains feasible (due to potential delays) + if (previousSchedules != null) { + for (Id vehicleId : previousSchedules.vehicleToTimetableMap().keySet()) { + Map requestPickUpTimeMap = new HashMap<>(); + List requestsOnboardThisVehicle = new ArrayList<>(); + Link vehicleStartLink = onlineVehicleInfoMap.get(vehicleId).currentLink(); + double vehicleStartTime = onlineVehicleInfoMap.get(vehicleId).divertableTime(); + Link currentLink = vehicleStartLink; + double currentTime = vehicleStartTime; + + for (TimetableEntry stop : previousSchedules.vehicleToTimetableMap().get(vehicleId)) { + Link stopLink; + GeneralRequest request = stop.getRequest(); + if (stop.getStopType() == TimetableEntry.StopType.PICKUP) { + // This is an already accepted request, we will record the updated the earliest "latest pick-up time" (i.e., due to delay, the latest pick-up time may need to be extended) + // We still need the drop-off information to generate the shipment + stopLink = network.getLinks().get(request.getFromLinkId()); + double travelTime = vrpCosts.getTransportTime(collectLocationIfAbsent(currentLink), collectLocationIfAbsent(stopLink), currentTime, null, null); + currentTime += travelTime; + currentLink = stopLink; + requestPickUpTimeMap.put(request, currentTime); + } else { + // Now we have the drop-off information, we can generate special shipments for already accepted requests + stopLink = network.getLinks().get(request.getToLinkId()); + double travelTime = vrpCosts.getTransportTime(collectLocationIfAbsent(currentLink), collectLocationIfAbsent(stopLink), currentTime, null, null); + currentTime += travelTime; + currentLink = stopLink; + + double earliestLatestDropOffTime = currentTime; + if (!requestPickUpTimeMap.containsKey(request)) { + // The request is already onboard + requestsOnboardThisVehicle.add(request); + var shipmentId = request.getPassengerIds().toString() + "_dummy_" + vehicleStartTime; + var shipment = Shipment.Builder.newInstance(shipmentId). + setPickupLocation(collectLocationIfAbsent(vehicleStartLink)). + setDeliveryLocation(collectLocationIfAbsent(network.getLinks().get(request.getToLinkId()))). + setPickupTimeWindow(new TimeWindow(vehicleStartTime, vehicleStartTime)). + setPickupServiceTime(0). + setDeliveryServiceTime(drtCfg.stopDuration). + setDeliveryTimeWindow(new TimeWindow(vehicleStartTime, Math.max(request.getLatestArrivalTime(), earliestLatestDropOffTime))). + addSizeDimension(0, 1). + addRequiredSkill(vehicleId.toString()). + setPriority(1). + build(); + // Priority: 1 --> top priority. 10 --> the lowest priority + vrpBuilder.addJob(shipment); + requestToShipmentMap.put(request, shipment); + preplannedRequestByShipmentId.put(shipmentId, request); + } else { + // The request is waiting to be picked up: retrieve the earliestLatestPickUpTime + double earliestLatestPickUpTime = requestPickUpTimeMap.get(request); + + var shipmentId = request.getPassengerIds().toString() + "_repeat_" + vehicleStartTime; + var shipment = Shipment.Builder.newInstance(shipmentId). + setPickupLocation(collectLocationIfAbsent(network.getLinks().get(request.getFromLinkId()))). + setDeliveryLocation(collectLocationIfAbsent(network.getLinks().get(request.getToLinkId()))). + setPickupTimeWindow(new TimeWindow(request.getEarliestDepartureTime(), Math.max(request.getLatestDepartureTime(), earliestLatestPickUpTime))). + setPickupServiceTime(drtCfg.stopDuration). + setDeliveryServiceTime(drtCfg.stopDuration). + setDeliveryTimeWindow(new TimeWindow(vehicleStartTime, Math.max(request.getLatestArrivalTime(), earliestLatestDropOffTime))). + addSizeDimension(0, 1). + setPriority(2). + build(); + // Priority: 1 --> top priority. 10 --> the lowest priority + vrpBuilder.addJob(shipment); + requestToShipmentMap.put(request, shipment); + preplannedRequestByShipmentId.put(shipmentId, request); + } + } + currentTime += drtCfg.stopDuration; + } + // Add the request onboard this vehicle to the main pool + requestsOnboardEachVehicles.put(vehicleId, requestsOnboardThisVehicle); + } + } + + // 2.2 New requests + for (GeneralRequest newRequest : newRequests) { + var shipmentId = newRequest.getPassengerIds().toString(); + var shipment = Shipment.Builder.newInstance(shipmentId). + setPickupLocation(collectLocationIfAbsent(network.getLinks().get(newRequest.getFromLinkId()))). + setDeliveryLocation(collectLocationIfAbsent(network.getLinks().get(newRequest.getToLinkId()))). + setPickupTimeWindow(new TimeWindow(newRequest.getEarliestDepartureTime(), newRequest.getLatestDepartureTime())). + setDeliveryTimeWindow(new TimeWindow(newRequest.getEarliestDepartureTime(), newRequest.getLatestArrivalTime())). + setPickupServiceTime(drtCfg.stopDuration). + setDeliveryServiceTime(drtCfg.stopDuration). + addSizeDimension(0, 1). + setPriority(10). + build(); + vrpBuilder.addJob(shipment); + preplannedRequestByShipmentId.put(shipmentId, newRequest); + unassignedShipments.add(shipment); + } + + // Solve VRP problem + var problem = vrpBuilder.setFleetSize(VehicleRoutingProblem.FleetSize.FINITE).build(); + + String numOfThreads = "1"; + if (options.multiThread) { + numOfThreads = Runtime.getRuntime().availableProcessors() + ""; + } + + VehicleRoutingProblemSolution initialSolution = null; + if (previousSchedules != null) { + for (Id vehicleId : previousSchedules.vehicleToTimetableMap().keySet()) { + // Now we need to create the initial solution for this vehicle for the VRP problem + VehicleRoute.Builder initialRouteBuilder = VehicleRoute.Builder + .newInstance(vehicleIdToJSpritVehicleMap.get(vehicleId)) + .setJobActivityFactory(problem.getJobActivityFactory()); + // First pick up all the dummy requests (onboard request) + for (GeneralRequest request : requestsOnboardEachVehicles.get(vehicleId)) { + initialRouteBuilder.addPickup(requestToShipmentMap.get(request)); + } + // Then add each stops according to the previous plan + for (TimetableEntry stop : previousSchedules.vehicleToTimetableMap().get(vehicleId)) { + Shipment shipment = requestToShipmentMap.get(stop.getRequest()); + if (stop.getStopType() == TimetableEntry.StopType.PICKUP) { + initialRouteBuilder.addPickup(shipment); + } else { + initialRouteBuilder.addDelivery(shipment); + } + } + // Build the route and store in the map + VehicleRoute iniRoute = initialRouteBuilder.build(); + routesForInitialSolutions.add(iniRoute); + } + + initialSolution = new VehicleRoutingProblemSolution(routesForInitialSolutions, unassignedShipments, 0); + initialSolution.setCost(new DefaultRollingHorizonObjectiveFunction(problem).getCosts(initialSolution)); + } + + var algorithm = Jsprit.Builder.newInstance(problem) + .setProperty(Jsprit.Parameter.THREADS, numOfThreads) + .setObjectiveFunction(new DefaultRollingHorizonObjectiveFunction(problem)) + .setRandom(options.random) + .buildAlgorithm(); + algorithm.setMaxIterations(options.maxIterations); + if (previousSchedules != null) { + algorithm.addInitialSolution(initialSolution); + } + var solutions = algorithm.searchSolutions(); + var bestSolution = Solutions.bestOf(solutions); + + // Collect results + Set>> personsOnboard = new HashSet<>(); + requestsOnboardEachVehicles.values().forEach(l -> l.forEach(r -> personsOnboard.add(r.getPassengerIds()))); + + Map>, Id> assignedPassengerToVehicleMap = new HashMap<>(); + Map, List> vehicleToPreplannedStops = problem.getVehicles() + .stream() + .collect(Collectors.toMap(v -> Id.create(v.getId(), DvrpVehicle.class), v -> new LinkedList<>())); + + for (var route : bestSolution.getRoutes()) { + var vehicleId = Id.create(route.getVehicle().getId(), DvrpVehicle.class); + DvrpVehicle vehicle = onlineVehicleInfoMap.get(vehicleId).vehicle(); + int occupancy = 0; + for (var activity : route.getActivities()) { + var preplannedRequest = preplannedRequestByShipmentId.get(((TourActivity.JobActivity) activity).getJob().getId()); + boolean isPickup = activity instanceof PickupShipment; + if (isPickup) { + if (!personsOnboard.contains(preplannedRequest.getPassengerIds())) { + // Add pick up stop if passenger is not yet onboard + var preplannedStop = new TimetableEntry(preplannedRequest, TimetableEntry.StopType.PICKUP, + activity.getArrTime(), activity.getEndTime(), occupancy, drtCfg.stopDuration, vehicle); + vehicleToPreplannedStops.get(vehicleId).add(preplannedStop); + } + assignedPassengerToVehicleMap.put(preplannedRequest.getPassengerIds(), vehicleId); + occupancy++; + } else { + // Add drop off stop + var preplannedStop = new TimetableEntry(preplannedRequest, TimetableEntry.StopType.DROP_OFF, + activity.getArrTime(), activity.getEndTime(), occupancy, drtCfg.stopDuration, vehicle); + vehicleToPreplannedStops.get(vehicleId).add(preplannedStop); + occupancy--; + } + } + } + + Map>, GeneralRequest> rejectedRequests = new HashMap<>(); + for (Job job : bestSolution.getUnassignedJobs()) { + GeneralRequest rejectedRequest = preplannedRequestByShipmentId.get(job.getId()); + rejectedRequests.put(rejectedRequest.getPassengerIds(), rejectedRequest); + } + + if (previousSchedules != null) { + rejectedRequests.putAll(previousSchedules.pendingRequests()); + // Previously rejected requests whose "departure time" is not yet reached (i.e., not yet formally rejected in the DRT system) + } + + return new FleetSchedules(vehicleToPreplannedStops, assignedPassengerToVehicleMap, rejectedRequests); + } + + // Inner classes / records + public record Options(int maxIterations, boolean multiThread, Random random) { + } + + record MatrixBasedVrpCosts(TravelTimeMatrix travelTimeMatrix, double now, + Network network, TravelTime travelTime) implements VehicleRoutingTransportCosts { + private double getTravelTime(Location from, Location to) { + if (from.getId().equals(to.getId())) { + return 0; + } + Link fromLink = network.getLinks().get(Id.createLinkId(from.getId())); + Link toLink = network.getLinks().get(Id.createLinkId(to.getId())); + return FIRST_LINK_TT + travelTimeMatrix.getTravelTime(fromLink.getToNode(), toLink.getFromNode(), now) + + VrpPaths.getLastLinkTT(travelTime, toLink, now); + } + + @Override + public double getTransportCost(Location from, Location to, double departureTime, Driver driver, Vehicle vehicle) { + return getTravelTime(from, to); + } + + @Override + public double getTransportTime(Location from, Location to, double departureTime, Driver driver, Vehicle vehicle) { + return getTravelTime(from, to); + } + + @Override + public double getBackwardTransportCost(Location from, Location to, double arrivalTime, Driver driver, Vehicle vehicle) { + return getTravelTime(to, from); + } + + @Override + public double getBackwardTransportTime(Location from, Location to, double arrivalTime, Driver driver, Vehicle vehicle) { + return getTravelTime(to, from); + } + + @Override + public double getDistance(Location from, Location to, double departureTime, Vehicle vehicle) { + throw new RuntimeException("Get distance is not yet implemented. Use travel time or cost instead!"); + } + } + + private record DefaultRollingHorizonObjectiveFunction(VehicleRoutingProblem vrp) implements SolutionCostCalculator { + @Override + public double getCosts(VehicleRoutingProblemSolution solution) { + double costs = 0; + for (VehicleRoute route : solution.getRoutes()) { + costs += route.getVehicle().getType().getVehicleCostParams().fix; + TourActivity prevAct = route.getStart(); + for (TourActivity act : route.getActivities()) { + costs += vrp.getTransportCosts().getTransportCost(prevAct.getLocation(), act.getLocation(), prevAct.getEndTime(), route.getDriver(), route.getVehicle()); + costs += vrp.getActivityCosts().getActivityCost(act, act.getArrTime(), route.getDriver(), route.getVehicle()); + prevAct = act; + } + } + + for (Job j : solution.getUnassignedJobs()) { + costs += REJECTION_COST * (11 - j.getPriority()) * (11 - j.getPriority()) * (11 - j.getPriority()); // Make sure the cost to "reject" request onboard is prohibitively large + } + + return costs; + } + } + + // private methods + private Location collectLocationIfAbsent(Link link) { + return locationByLinkId.computeIfAbsent(link.getId(), linkId -> Location.Builder.newInstance() + .setId(link.getId() + "") + .setIndex(locationByLinkId.size()) + .setCoordinate(Coordinate.newInstance(link.getCoord().getX(), link.getCoord().getY())) + .build()); + } + + private TravelTimeMatrix createTravelTimeMatrix(double time) { + Map zoneByNode = locationByLinkId.keySet() + .stream() + .flatMap(linkId -> Stream.of(network.getLinks().get(linkId).getFromNode(), network.getLinks().get(linkId).getToNode())) + .collect(toMap(n -> n, node -> new Zone(Id.create(node.getId(), Zone.class), "node", node.getCoord()), + (zone1, zone2) -> zone1)); + var nodeByZone = EntryStream.of(zoneByNode).invert().toMap(); + Matrix nodeToNodeMatrix = TravelTimeMatrices.calculateTravelTimeMatrix( + new TravelTimeMatrices.RoutingParams(network, travelTime, travelDisutility, Runtime.getRuntime().availableProcessors()), nodeByZone, time); + + return (fromNode, toNode, departureTime) -> nodeToNodeMatrix.get(zoneByNode.get(fromNode), zoneByNode.get(toNode)); + } + +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/VrpSolverRegretHeuristic.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/VrpSolverRegretHeuristic.java new file mode 100644 index 00000000000..e4ba5e04f72 --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/VrpSolverRegretHeuristic.java @@ -0,0 +1,132 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver; + +import com.google.common.base.Preconditions; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.network.Network; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.InsertionCalculator; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.FleetSchedules; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.LinkToLinkTravelTimeMatrix; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.OnlineVehicleInfo; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.GeneralRequest; +import org.matsim.contrib.drt.run.DrtConfigGroup; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; +import org.matsim.core.router.util.TravelTime; + + +import java.util.*; + +/** + * The parallel insertion strategy with regression heuristic * + */ +public class VrpSolverRegretHeuristic implements VrpSolver { + private final Network network; + private final TravelTime travelTime; + private final double stopDuration; + + public VrpSolverRegretHeuristic(Network network, TravelTime travelTime, DrtConfigGroup drtConfigGroup) { + this.network = network; + this.travelTime = travelTime; + this.stopDuration = drtConfigGroup.stopDuration; + } + + @Override + public FleetSchedules calculate(FleetSchedules previousSchedules, Map, OnlineVehicleInfo> onlineVehicleInfoMap, List newRequests, double time) { + // Initialize fleet schedule when it is null + if (previousSchedules == null) { + previousSchedules = FleetSchedules.initializeFleetSchedules(onlineVehicleInfoMap); + } + + // If there is no new request, simply return the previous fleet schedule + if (newRequests.isEmpty()) { + return previousSchedules; + } + + // Prepare link to link travel time matrix based on all relevant locations (links) + LinkToLinkTravelTimeMatrix linkToLinkTravelTimeMatrix = LinkToLinkTravelTimeMatrix. + prepareLinkToLinkTravelMatrix(network, travelTime, previousSchedules, onlineVehicleInfoMap, newRequests, time); + + // Update the schedule to the current situation (e.g., errors caused by those 1s differences; traffic situation...) + previousSchedules.updateFleetSchedule(network, linkToLinkTravelTimeMatrix, onlineVehicleInfoMap); + + // Create insertion calculator + InsertionCalculator insertionCalculator = new InsertionCalculator(network, stopDuration, linkToLinkTravelTimeMatrix); + + // Perform regret insertion + return performRegretInsertion(insertionCalculator, previousSchedules, onlineVehicleInfoMap, newRequests); + } + + public FleetSchedules performRegretInsertion(InsertionCalculator insertionCalculator, FleetSchedules previousSchedules, + Map, OnlineVehicleInfo> onlineVehicleInfoMap, List newRequests) { + Preconditions.checkArgument(!newRequests.isEmpty(), "There is no new request to insert!"); + // Initialize the matrix (LinkedHashMap is used to preserved order of the matrix -> reproducible results even if there are plans with same max regret/score) + Map> insertionMatrix = new LinkedHashMap<>(); + for (GeneralRequest request : newRequests) { + insertionMatrix.put(request, new LinkedHashMap<>()); + for (OnlineVehicleInfo vehicleInfo : onlineVehicleInfoMap.values()) { + InsertionCalculator.InsertionData insertionData = insertionCalculator.computeInsertionData(vehicleInfo, request, previousSchedules); + insertionMatrix.get(request).put(vehicleInfo, insertionData); + } + } + + // Insert each request recursively + boolean finished = false; + while (!finished) { + // Get the request with the highest regret and insert it to the best vehicle + double largestRegret = -1; + GeneralRequest requestWithLargestRegret = null; + for (GeneralRequest request : insertionMatrix.keySet()) { + double regret = getRegret(request, insertionMatrix); + if (regret > largestRegret) { + largestRegret = regret; + requestWithLargestRegret = request; + } + } + + assert requestWithLargestRegret != null; + InsertionCalculator.InsertionData bestInsertionData = getBestInsertionForRequest(requestWithLargestRegret, insertionMatrix); + + if (bestInsertionData.cost() < InsertionCalculator.NOT_FEASIBLE_COST) { + // Formally insert the request to the timetable + previousSchedules.requestIdToVehicleMap().put(requestWithLargestRegret.getPassengerIds(), bestInsertionData.vehicleInfo().vehicle().getId()); + previousSchedules.vehicleToTimetableMap().put(bestInsertionData.vehicleInfo().vehicle().getId(), bestInsertionData.candidateTimetable()); + + // Remove the request from the insertion matrix + insertionMatrix.remove(requestWithLargestRegret); + + // Update insertion data for the rest of the request and the selected vehicle + for (GeneralRequest request : insertionMatrix.keySet()) { + InsertionCalculator.InsertionData updatedInsertionData = insertionCalculator.computeInsertionData(bestInsertionData.vehicleInfo(), request, previousSchedules); + insertionMatrix.get(request).put(bestInsertionData.vehicleInfo(), updatedInsertionData); + } + } else { + // The best insertion is already infeasible. Reject this request + previousSchedules.pendingRequests().put(requestWithLargestRegret.getPassengerIds(), requestWithLargestRegret); + // Remove the request from the insertion matrix + insertionMatrix.remove(requestWithLargestRegret); + } + finished = insertionMatrix.isEmpty(); + } + return previousSchedules; + } + + // private methods + private double getRegret(GeneralRequest request, Map> insertionMatrix) { + List insertionDataList = new ArrayList<>(insertionMatrix.get(request).values()); + insertionDataList.sort(Comparator.comparingDouble(InsertionCalculator.InsertionData::cost)); + return insertionDataList.get(1).cost() + insertionDataList.get(2).cost() - 2 * insertionDataList.get(0).cost(); + // regret-3 is used here. It can also be switched to regret-2, regret-4, regret-5 ... regret-q + } + + private InsertionCalculator.InsertionData getBestInsertionForRequest( + GeneralRequest request, Map> insertionMatrix) { + double minInsertionCost = Double.MAX_VALUE; + InsertionCalculator.InsertionData bestInsertion = null; + for (InsertionCalculator.InsertionData insertionData : insertionMatrix.get(request).values()) { + if (insertionData.cost() < minInsertionCost) { + minInsertionCost = insertionData.cost(); + bestInsertion = insertionData; + } + } + return bestInsertion; + } +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/VrpSolverSeqInsertion.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/VrpSolverSeqInsertion.java new file mode 100644 index 00000000000..f204abc4eb2 --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/VrpSolverSeqInsertion.java @@ -0,0 +1,73 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver; + +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.network.Network; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.InsertionCalculator; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.*; +import org.matsim.contrib.drt.run.DrtConfigGroup; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; +import org.matsim.core.router.util.TravelTime; + + +import java.util.List; +import java.util.Map; + +public class VrpSolverSeqInsertion implements VrpSolver { + private final Network network; + private final TravelTime travelTime; + private final double stopDuration; + + public VrpSolverSeqInsertion(Network network, TravelTime travelTime, DrtConfigGroup drtConfigGroup) { + this.network = network; + this.travelTime = travelTime; + this.stopDuration = drtConfigGroup.stopDuration; + } + + @Override + public FleetSchedules calculate(FleetSchedules previousSchedules, + Map, OnlineVehicleInfo> onlineVehicleInfoMap, + List newRequests, double time) { + if (previousSchedules == null) { + previousSchedules = FleetSchedules.initializeFleetSchedules(onlineVehicleInfoMap); + } + + if (newRequests.isEmpty()) { + return previousSchedules; + } + + // Prepare link to link travel time matrix based on all relevant locations (links) + LinkToLinkTravelTimeMatrix linkToLinkTravelTimeMatrix = LinkToLinkTravelTimeMatrix. + prepareLinkToLinkTravelMatrix(network, travelTime, previousSchedules, onlineVehicleInfoMap, newRequests, time); + + // Update the schedule to the current situation (e.g., errors caused by those 1s differences; traffic situation...) + previousSchedules.updateFleetSchedule(network, linkToLinkTravelTimeMatrix, onlineVehicleInfoMap); + + // Initialize insertion calculator + InsertionCalculator insertionCalculator = new InsertionCalculator(network, stopDuration, linkToLinkTravelTimeMatrix); + + // Perform insertion + for (GeneralRequest request : newRequests) { + // Try to find the best insertion + double bestInsertionCost = Double.MAX_VALUE; + Id selectedVehicleId = null; + List updatedTimetable = null; + + for (Id vehicleId : previousSchedules.vehicleToTimetableMap().keySet()) { + InsertionCalculator.InsertionData insertionData = insertionCalculator.computeInsertionData(onlineVehicleInfoMap.get(vehicleId), request, previousSchedules); + if (insertionData.cost() < bestInsertionCost) { + bestInsertionCost = insertionData.cost(); + selectedVehicleId = vehicleId; + updatedTimetable = insertionData.candidateTimetable(); + } + } + + if (selectedVehicleId == null) { + previousSchedules.pendingRequests().put(request.getPassengerIds(), request); + } else { + previousSchedules.vehicleToTimetableMap().put(selectedVehicleId, updatedTimetable); + previousSchedules.requestIdToVehicleMap().put(request.getPassengerIds(), selectedVehicleId); + } + } + return previousSchedules; + } +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/DefaultSolutionCostCalculator.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/DefaultSolutionCostCalculator.java new file mode 100644 index 00000000000..027e09b7261 --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/DefaultSolutionCostCalculator.java @@ -0,0 +1,28 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.ruin_and_recreate; + + +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.FleetSchedules; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.TimetableEntry; + +import java.util.List; + +/** + * The default solution cost calculator return the total drive time of the fleet counted from "now", plus a + * high penalty for each rejected requests * + */ +public class DefaultSolutionCostCalculator implements SolutionCostCalculator { + private static final double REJECTION_COST = 1e6; + + @Override + public double calculateSolutionCost(FleetSchedules fleetSchedules, double now) { + double totalDrivingTime = 0; + for (List timetable : fleetSchedules.vehicleToTimetableMap().values()) { + double departureTime = now; + for (TimetableEntry stop : timetable) { + totalDrivingTime += stop.getArrivalTime() - departureTime; + departureTime = stop.getDepartureTime(); + } + } + return totalDrivingTime + REJECTION_COST * fleetSchedules.pendingRequests().size(); + } +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/RandomRuinSelector.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/RandomRuinSelector.java new file mode 100644 index 00000000000..6bdf8dec8d0 --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/RandomRuinSelector.java @@ -0,0 +1,38 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.ruin_and_recreate; + +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.GeneralRequest; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.FleetSchedules; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.TimetableEntry; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +public class RandomRuinSelector implements RuinSelector { + private final Random random; + private final static double PROPORTION_TO_REMOVE = 0.3; + + public RandomRuinSelector(Random random) { + this.random = random; + } + + @Override + public List selectRequestsToBeRuined(FleetSchedules fleetSchedules) { + List openRequests = new ArrayList<>(); + for (List timetable : fleetSchedules.vehicleToTimetableMap().values()) { + timetable.stream().filter(s -> s.getStopType() == TimetableEntry.StopType.PICKUP).forEach(s -> openRequests.add(s.getRequest())); + } + + Collections.shuffle(openRequests, random); + int numToRemoved = (int) (openRequests.size() * PROPORTION_TO_REMOVE) + 1; + int maxRemoval = 1000; + numToRemoved = Math.min(numToRemoved, maxRemoval); + numToRemoved = Math.min(numToRemoved, openRequests.size()); + List requestsToBeRuined = new ArrayList<>(); + for (int i = 0; i < numToRemoved; i++) { + requestsToBeRuined.add(openRequests.get(i)); + } + return requestsToBeRuined; + } +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/RecreateSolutionAcceptor.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/RecreateSolutionAcceptor.java new file mode 100644 index 00000000000..596268a9208 --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/RecreateSolutionAcceptor.java @@ -0,0 +1,6 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.ruin_and_recreate; + +public interface RecreateSolutionAcceptor { + + boolean acceptSolutionOrNot(double currentScore, double previousScore, int currentIteration, int totalIterations); +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/RuinSelector.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/RuinSelector.java new file mode 100644 index 00000000000..7a6bdb69b68 --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/RuinSelector.java @@ -0,0 +1,10 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.ruin_and_recreate; + +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.FleetSchedules; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.GeneralRequest; + +import java.util.List; + +public interface RuinSelector { + List selectRequestsToBeRuined(FleetSchedules fleetSchedules); +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/SimpleAnnealingThresholdAcceptor.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/SimpleAnnealingThresholdAcceptor.java new file mode 100644 index 00000000000..247980af258 --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/SimpleAnnealingThresholdAcceptor.java @@ -0,0 +1,16 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.ruin_and_recreate; + +public class SimpleAnnealingThresholdAcceptor implements RecreateSolutionAcceptor { + + @Override + public boolean acceptSolutionOrNot(double currentScore, double previousScore, int currentIteration, int totalIterations) { + // Parameters for the annealing acceptor + double initialThreshold = 0.5; + double halfLife = 0.1; + + double x = (double) currentIteration / (double) totalIterations; + double threshold = initialThreshold * Math.exp(-Math.log(2) * x / halfLife); + + return currentScore < (1 + threshold) * previousScore; + } +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/SolutionCostCalculator.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/SolutionCostCalculator.java new file mode 100644 index 00000000000..9b5b2fb5ee4 --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/SolutionCostCalculator.java @@ -0,0 +1,7 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.ruin_and_recreate; + +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.FleetSchedules; + +public interface SolutionCostCalculator { + double calculateSolutionCost(FleetSchedules fleetSchedules, double now); +} diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/VrpSolverRuinAndRecreate.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/VrpSolverRuinAndRecreate.java new file mode 100644 index 00000000000..ec36e1d0b2b --- /dev/null +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimization/vrp_solver/ruin_and_recreate/VrpSolverRuinAndRecreate.java @@ -0,0 +1,114 @@ +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.ruin_and_recreate; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.network.Network; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.InsertionCalculator; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.FleetSchedules; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.GeneralRequest; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.LinkToLinkTravelTimeMatrix; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.basic_structures.OnlineVehicleInfo; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.VrpSolver; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.vrp_solver.VrpSolverRegretHeuristic; +import org.matsim.contrib.drt.run.DrtConfigGroup; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; +import org.matsim.core.router.util.TravelTime; + + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; + +public record VrpSolverRuinAndRecreate(int maxIterations, Network network, TravelTime travelTime, + DrtConfigGroup drtConfigGroup, Random random) implements VrpSolver { + private static final Logger log = LogManager.getLogger(VrpSolverRuinAndRecreate.class); + + @Override + public FleetSchedules calculate(FleetSchedules previousSchedules, + Map, OnlineVehicleInfo> onlineVehicleInfoMap, List newRequests, + double time) { + // Initialize fleet schedule when it is null + if (previousSchedules == null) { + previousSchedules = FleetSchedules.initializeFleetSchedules(onlineVehicleInfoMap); + } + + // If there is no new request, simply keep the schedules unchanged + if (newRequests.isEmpty()) { + return previousSchedules; + } + + // Initialize all the necessary objects + RecreateSolutionAcceptor solutionAcceptor = new SimpleAnnealingThresholdAcceptor(); + RuinSelector ruinSelector = new RandomRuinSelector(random); + SolutionCostCalculator solutionCostCalculator = new DefaultSolutionCostCalculator(); + + // Prepare link to link travel time matrix for relevant links + LinkToLinkTravelTimeMatrix linkToLinkTravelTimeMatrix = LinkToLinkTravelTimeMatrix. + prepareLinkToLinkTravelMatrix(network, travelTime, previousSchedules, onlineVehicleInfoMap, newRequests, time); + + // update schedules based on the latest travel time estimation and current locations + previousSchedules.updateFleetSchedule(network, linkToLinkTravelTimeMatrix, onlineVehicleInfoMap); + + // Create insertion calculator + InsertionCalculator insertionCalculator = new InsertionCalculator(network, drtConfigGroup.stopDuration, linkToLinkTravelTimeMatrix); + + // Initialize regret inserter + VrpSolverRegretHeuristic regretInserter = new VrpSolverRegretHeuristic(network, travelTime, drtConfigGroup); + + // Calculate initial solution + FleetSchedules initialSolution = regretInserter.performRegretInsertion(insertionCalculator, previousSchedules, onlineVehicleInfoMap, newRequests); + double initialScore = solutionCostCalculator.calculateSolutionCost(initialSolution, time); + + // Initialize the best solution (set to initial solution) + FleetSchedules currentBestSolution = initialSolution; + double currentBestScore = initialScore; + + // Initialize the fall back solution (set to the initial solution) + FleetSchedules currentSolution = initialSolution; + double currentScore = initialScore; + + int displayCounter = 1; + for (int i = 1; i < maxIterations + 1; i++) { + // Create a copy of current solution + FleetSchedules newSolution = currentSolution.copySchedule(); + + // Ruin the plan by removing some requests from the schedule + List requestsToRemove = ruinSelector.selectRequestsToBeRuined(newSolution); + if (requestsToRemove.isEmpty()) { + log.info("There is no request to remove! All the following iterations will be skipped"); + break; + } + for (GeneralRequest request : requestsToRemove) { + Id vehicleId = newSolution.requestIdToVehicleMap().get(request.getPassengerIds()); + insertionCalculator.removeRequestFromSchedule(onlineVehicleInfoMap.get(vehicleId), request, newSolution); + } + + // Recreate: try to re-insert all the removed requests, along with rejected requests, back into the schedule + List requestsToReinsert = new ArrayList<>(newSolution.pendingRequests().values()); + newSolution.pendingRequests().clear(); + newSolution = regretInserter.performRegretInsertion(insertionCalculator, newSolution, onlineVehicleInfoMap, requestsToReinsert); + + // Score the new solution + double newScore = solutionCostCalculator.calculateSolutionCost(newSolution, time); + if (solutionAcceptor.acceptSolutionOrNot(newScore, currentScore, i, maxIterations)) { + currentSolution = newSolution; + currentScore = newScore; + if (newScore < currentBestScore) { + currentBestScore = newScore; + currentBestSolution = newSolution; + } + } + + if (i % displayCounter == 0) { + log.info("Ruin and Recreate iterations #" + i + ": new score = " + newScore + ", accepted = " + solutionAcceptor.acceptSolutionOrNot(newScore, currentScore, i, maxIterations) + ", current best score = " + currentBestScore); + displayCounter *= 2; + } + + } + log.info(maxIterations + " ruin and Recreate iterations complete!"); + + return currentBestSolution; + } +} diff --git a/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/preplanned/optimizer/RunPreplannedDrtExampleIT.java b/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/preplanned/optimizer/RunPreplannedDrtExampleIT.java index e5477142a92..7711a1a72b1 100644 --- a/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/preplanned/optimizer/RunPreplannedDrtExampleIT.java +++ b/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/preplanned/optimizer/RunPreplannedDrtExampleIT.java @@ -26,7 +26,6 @@ import java.net.URL; import java.util.*; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.Test; diff --git a/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimizer/RunOfflineOptimizationTest.java b/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimizer/RunOfflineOptimizationTest.java new file mode 100644 index 00000000000..39e76592ce4 --- /dev/null +++ b/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimizer/RunOfflineOptimizationTest.java @@ -0,0 +1,99 @@ +/* *********************************************************************** * + * project: org.matsim.* + * *********************************************************************** * + * * + * copyright : (C) 2018 by the members listed in the COPYING, * + * LICENSE and WARRANTY file. * + * email : info at matsim dot org * + * * + * *********************************************************************** * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * See also COPYING, LICENSE and WARRANTY file * + * * + * *********************************************************************** */ + +package org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimizer; + +import org.junit.Assert; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.population.Population; +import org.matsim.contrib.drt.extension.preplanned.optimizer.LinearStopDurationModule; +import org.matsim.contrib.drt.extension.preplanned.optimizer.offline_optimization.RollingHorizonDrtOperationModule; +import org.matsim.contrib.drt.extension.preplanned.run.PreplannedDrtControlerCreator; +import org.matsim.contrib.drt.run.DrtConfigGroup; +import org.matsim.contrib.drt.run.MultiModeDrtConfigGroup; +import org.matsim.contrib.dvrp.benchmark.DvrpBenchmarkTravelTimeModule; +import org.matsim.contrib.dvrp.run.DvrpConfigGroup; +import org.matsim.contrib.dvrp.run.DvrpModule; +import org.matsim.core.config.Config; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.controler.Controler; +import org.matsim.core.events.EventsUtils; +import org.matsim.core.population.PopulationUtils; +import org.matsim.core.utils.io.IOUtils; +import org.matsim.examples.ExamplesUtils; +import org.matsim.testcases.MatsimTestUtils; +import org.matsim.utils.eventsfilecomparison.EventsFileComparator; +import org.matsim.vis.otfvis.OTFVisConfigGroup; + +/** + * @author michalm + */ +public class RunOfflineOptimizationTest { + @RegisterExtension + private MatsimTestUtils utils = new MatsimTestUtils(); + + @Test + public void mielecTest() { + String configPath = IOUtils.extendUrl(ExamplesUtils.getTestScenarioURL("mielec"), "mielec_drt_config.xml").toString(); + runScenario(configPath); + } + + private void runScenario(String configPath) { + Id.resetCaches(); + + Config config = ConfigUtils.loadConfig(configPath, new MultiModeDrtConfigGroup(), new DvrpConfigGroup(), new OTFVisConfigGroup()); + MultiModeDrtConfigGroup multiModeDrtConfig = MultiModeDrtConfigGroup.get(config); + DrtConfigGroup drtConfigGroup = DrtConfigGroup.getSingleModeDrtConfig(config); + drtConfigGroup.vehiclesFile = "vehicles-10-cap-4-offline.xml"; + if (drtConfigGroup.getRebalancingParams().isPresent()) { + drtConfigGroup.removeParameterSet(drtConfigGroup.getRebalancingParams().get()); + } + config.controller().setOutputDirectory(utils.getOutputDirectory()); + Controler controler = PreplannedDrtControlerCreator.createControler(config, false); + controler.addOverridingModule(new DvrpModule(new DvrpBenchmarkTravelTimeModule())); + + Population prebookedPlans = controler.getScenario().getPopulation(); + + for (DrtConfigGroup drtCfg : multiModeDrtConfig.getModalElements()) { + controler.addOverridingQSimModule(new RollingHorizonDrtOperationModule(prebookedPlans, drtCfg, + 86400, 86400, 10, false, 1, RollingHorizonDrtOperationModule.OfflineSolverType.REGRET_INSERTION)); + controler.addOverridingModule(new LinearStopDurationModule(drtCfg)); + } + controler.run(); + + { + Population expected = PopulationUtils.createPopulation(ConfigUtils.createConfig()); + PopulationUtils.readPopulation(expected, utils.getInputDirectory() + "output_plans.xml.gz"); + + Population actual = PopulationUtils.createPopulation(ConfigUtils.createConfig()); + PopulationUtils.readPopulation(actual, utils.getOutputDirectory() + "output_plans.xml.gz"); + + boolean result = PopulationUtils.comparePopulations(expected, actual); + Assert.assertTrue(result); + } + + { + String expected = utils.getInputDirectory() + "output_events.xml.gz"; + String actual = utils.getOutputDirectory() + "output_events.xml.gz"; + EventsFileComparator.Result result = EventsUtils.compareEventsFiles(expected, actual); + Assert.assertEquals(EventsFileComparator.Result.FILES_ARE_EQUAL, result); + } + } +} diff --git a/contribs/drt-extensions/test/input/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimizer/RunOfflineOptimizationTest/mielecTest/output_events.xml.gz b/contribs/drt-extensions/test/input/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimizer/RunOfflineOptimizationTest/mielecTest/output_events.xml.gz new file mode 100644 index 00000000000..404212cf1ed Binary files /dev/null and b/contribs/drt-extensions/test/input/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimizer/RunOfflineOptimizationTest/mielecTest/output_events.xml.gz differ diff --git a/contribs/drt-extensions/test/input/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimizer/RunOfflineOptimizationTest/mielecTest/output_plans.xml.gz b/contribs/drt-extensions/test/input/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimizer/RunOfflineOptimizationTest/mielecTest/output_plans.xml.gz new file mode 100644 index 00000000000..720dd6cd998 Binary files /dev/null and b/contribs/drt-extensions/test/input/org/matsim/contrib/drt/extension/preplanned/optimizer/offline_optimizer/RunOfflineOptimizationTest/mielecTest/output_plans.xml.gz differ diff --git a/examples/scenarios/mielec/vehicles-10-cap-4-offline.xml b/examples/scenarios/mielec/vehicles-10-cap-4-offline.xml new file mode 100644 index 00000000000..2a19ea24deb --- /dev/null +++ b/examples/scenarios/mielec/vehicles-10-cap-4-offline.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file