diff --git a/contribs/application/src/main/java/org/matsim/application/analysis/traffic/traveltime/SampleValidationRoutes.java b/contribs/application/src/main/java/org/matsim/application/analysis/traffic/traveltime/SampleValidationRoutes.java index 0e6c0f2af59..aa3a1fc4ba0 100644 --- a/contribs/application/src/main/java/org/matsim/application/analysis/traffic/traveltime/SampleValidationRoutes.java +++ b/contribs/application/src/main/java/org/matsim/application/analysis/traffic/traveltime/SampleValidationRoutes.java @@ -19,10 +19,7 @@ import org.matsim.api.core.v01.network.Node; import org.matsim.application.CommandSpec; import org.matsim.application.MATSimAppCommand; -import org.matsim.application.options.CrsOptions; -import org.matsim.application.options.InputOptions; -import org.matsim.application.options.OutputOptions; -import org.matsim.application.options.ShpOptions; +import org.matsim.application.options.*; import org.matsim.application.prepare.network.SampleNetwork; import org.matsim.core.network.NetworkUtils; import org.matsim.core.router.costcalculators.OnlyTimeDependentTravelDisutility; @@ -32,9 +29,11 @@ import org.matsim.core.trafficmonitoring.FreeSpeedTravelTime; import org.matsim.core.utils.geometry.CoordUtils; import org.matsim.core.utils.geometry.transformations.GeotoolsTransformation; +import org.matsim.core.utils.io.IOUtils; import picocli.CommandLine; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -86,6 +85,8 @@ public class SampleValidationRoutes implements MATSimAppCommand { @CommandLine.Option(names = "--mode", description = "Mode to validate", defaultValue = TransportMode.car) private String mode; + @CommandLine.Option(names = "--input-od", description = "Use input fromNode,toNode instead of sampling", required = false) + private String inputOD; public static void main(String[] args) { new SampleValidationRoutes().execute(args); @@ -142,9 +143,15 @@ public Integer call() throws Exception { OnlyTimeDependentTravelDisutility util = new OnlyTimeDependentTravelDisutility(tt); LeastCostPathCalculator router = new SpeedyALTFactory().createPathCalculator(network, util, tt); - List routes = sampleRoutes(network, router, rnd); - log.info("Sampled {} routes in range {}", routes.size(), distRange); + List routes; + if (inputOD != null) { + log.info("Using input OD file {}", inputOD); + routes = queryRoutes(network, router); + } else { + routes = sampleRoutes(network, router, rnd); + log.info("Sampled {} routes in range {}", routes.size(), distRange); + } try (CSVPrinter csv = new CSVPrinter(Files.newBufferedWriter(output.getPath()), CSVFormat.DEFAULT)) { csv.printRecord("from_node", "to_node", "beeline_dist", "dist", "travel_time", "geometry"); @@ -228,6 +235,7 @@ private List sampleRoutes(Network network, LeastCostPathCalculator router ShpOptions.Index index = shp.isDefined() ? shp.createIndex(crs, "_") : null; Predicate exclude = excludeRoads != null && !excludeRoads.isBlank() ? new Predicate<>() { final Pattern p = Pattern.compile(excludeRoads, Pattern.CASE_INSENSITIVE); + @Override public boolean test(Link link) { return p.matcher(NetworkUtils.getHighwayType(link)).find(); @@ -282,6 +290,59 @@ public boolean test(Link link) { return result; } + /** + * Use given od pairs as input for validation. + */ + private List queryRoutes(Network network, LeastCostPathCalculator router) { + + List result = new ArrayList<>(); + String crs = ProjectionUtils.getCRS(network); + + if (this.crs.getInputCRS() != null) + crs = this.crs.getInputCRS(); + + if (crs == null) { + throw new IllegalArgumentException("Input CRS could not be detected. Please specify with --input-crs [EPSG:xxx]"); + } + + GeotoolsTransformation ct = new GeotoolsTransformation(crs, "EPSG:4326"); + + try (CSVParser parser = CSVParser.parse(IOUtils.getBufferedReader(inputOD), CSVFormat.DEFAULT.builder().setHeader().setSkipHeaderRecord(true). + setDelimiter(CsvOptions.detectDelimiter(inputOD)).build())) { + + List header = parser.getHeaderNames(); + if (!header.contains("from_node")) + throw new IllegalArgumentException("Missing 'from_node' column in input file"); + if (!header.contains("to_node")) + throw new IllegalArgumentException("Missing 'to_node' column in input file"); + + for (CSVRecord r : parser) { + Node fromNode = network.getNodes().get(Id.createNodeId(r.get("from_node"))); + Node toNode = network.getNodes().get(Id.createNodeId(r.get("to_node"))); + + if (fromNode == null) + throw new IllegalArgumentException("Node " + r.get("from_node") + " not found"); + if (toNode == null) + throw new IllegalArgumentException("Node " + r.get("to_node") + " not found"); + + LeastCostPathCalculator.Path path = router.calcLeastCostPath(fromNode, toNode, 0, null, null); + result.add(new Route( + fromNode.getId(), + toNode.getId(), + ct.transform(fromNode.getCoord()), + ct.transform(toNode.getCoord()), + path.travelTime, + path.links.stream().mapToDouble(Link::getLength).sum() + )); + } + + } catch (IOException e) { + throw new UncheckedIOException(e); + } + + return result; + } + /** * Key as pair of from and to node. */ diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/eshifts/scheduler/EShiftTaskScheduler.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/eshifts/scheduler/EShiftTaskScheduler.java index a87c54a1be8..9c5a7172282 100644 --- a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/eshifts/scheduler/EShiftTaskScheduler.java +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/eshifts/scheduler/EShiftTaskScheduler.java @@ -62,17 +62,17 @@ public class EShiftTaskScheduler implements ShiftTaskScheduler { private final Network network; private final ChargingInfrastructure chargingInfrastructure; - public EShiftTaskScheduler(Network network, TravelTime travelTime, TravelDisutility travelDisutility, - MobsimTimer timer, ShiftDrtTaskFactory taskFactory, ShiftsParams shiftsParams, - ChargingInfrastructure chargingInfrastructure, OperationFacilities operationFacilities, Fleet fleet) { - this.travelTime = travelTime; - this.timer = timer; - this.taskFactory = taskFactory; - this.network = network; - this.shiftsParams = shiftsParams; - this.router = new SpeedyALTFactory().createPathCalculator(network, travelDisutility, travelTime); - this.chargingInfrastructure = chargingInfrastructure; - } + public EShiftTaskScheduler(Network network, TravelTime travelTime, TravelDisutility travelDisutility, + MobsimTimer timer, ShiftDrtTaskFactory taskFactory, ShiftsParams shiftsParams, + ChargingInfrastructure chargingInfrastructure, OperationFacilities operationFacilities, Fleet fleet) { + this.travelTime = travelTime; + this.timer = timer; + this.taskFactory = taskFactory; + this.network = network; + this.shiftsParams = shiftsParams; + this.router = new SpeedyALTFactory().createPathCalculator(network, travelDisutility, travelTime); + this.chargingInfrastructure = chargingInfrastructure; + } public void relocateForBreak(ShiftDvrpVehicle vehicle, OperationFacility breakFacility, DrtShift shift) { final Schedule schedule = vehicle.getSchedule(); @@ -81,7 +81,7 @@ public void relocateForBreak(ShiftDvrpVehicle vehicle, OperationFacility breakFa final Link toLink = network.getLinks().get(breakFacility.getLinkId()); if (currentTask instanceof DriveTask && currentTask.getTaskType().equals(EmptyVehicleRelocator.RELOCATE_VEHICLE_TASK_TYPE) - && currentTask.equals(schedule.getTasks().get(schedule.getTaskCount()-2))) { + && currentTask.equals(schedule.getTasks().get(schedule.getTaskCount() - 2))) { //try to divert/cancel relocation LinkTimePair start = ((OnlineDriveTaskTracker) currentTask.getTaskTracker()).getDiversionPoint(); VrpPathWithTravelData path; @@ -112,13 +112,13 @@ public void relocateForBreak(ShiftDvrpVehicle vehicle, OperationFacility breakFa } else { final Task task = schedule.getTasks().get(schedule.getTaskCount() - 1); final Link lastLink = ((StayTask) task).getLink(); - double departureTime = task.getBeginTime(); + double departureTime = task.getBeginTime(); - // @Nico Did I change something here? - if (schedule.getCurrentTask() == task) { - departureTime = Math.max(task.getBeginTime(), timer.getTimeOfDay()); - } - if (lastLink.getId() != breakFacility.getLinkId()) { + // @Nico Did I change something here? + if (schedule.getCurrentTask() == task) { + departureTime = Math.max(task.getBeginTime(), timer.getTimeOfDay()); + } + if (lastLink.getId() != breakFacility.getLinkId()) { VrpPathWithTravelData path = VrpPaths.calcAndCreatePath(lastLink, toLink, departureTime, router, @@ -154,7 +154,7 @@ public void relocateForBreak(ShiftDvrpVehicle vehicle, OperationFacility breakFa double endTime = startTime + shift.getBreak().orElseThrow().getDuration(); double latestDetourArrival = timer.getTimeOfDay(); - relocateForBreakImpl(vehicle, startTime, endTime, latestDetourArrival, toLink, shift, breakFacility); + relocateForBreakImpl(vehicle, startTime, endTime, latestDetourArrival, toLink, shift, breakFacility); } } } @@ -165,24 +165,24 @@ private void relocateForBreakImpl(ShiftDvrpVehicle vehicle, double startTime, do Schedule schedule = vehicle.getSchedule(); // append SHIFT_BREAK task - DrtShiftBreak shiftBreak = shift.getBreak().orElseThrow(); + DrtShiftBreak shiftBreak = shift.getBreak().orElseThrow(); - ShiftBreakTask dropoffStopTask; - ElectricVehicle ev = ((EvDvrpVehicle) vehicle).getElectricVehicle(); - Optional charger = charge(breakFacility, ev); - if (charger.isPresent()) { + ShiftBreakTask dropoffStopTask; + ElectricVehicle ev = ((EvDvrpVehicle) vehicle).getElectricVehicle(); + Optional charger = charge(breakFacility, ev); + if (charger.isPresent()) { final Charger chargerImpl = charger.get(); - final double waitTime = ChargingEstimations - .estimateMaxWaitTimeForNextVehicle(chargerImpl); + final double waitTime = ChargingEstimations + .estimateMaxWaitTimeForNextVehicle(chargerImpl); - if (ev.getBattery().getCharge() / ev.getBattery().getCapacity() > shiftsParams.chargeDuringBreakThreshold || - waitTime > 0) { + if (ev.getBattery().getCharge() / ev.getBattery().getCapacity() > shiftsParams.chargeDuringBreakThreshold || + waitTime > 0) { dropoffStopTask = taskFactory.createShiftBreakTask(vehicle, startTime, endTime, link, shiftBreak, breakFacility); } else { - double energyCharge = ((BatteryCharging) ev.getChargingPower()).calcEnergyCharged(chargerImpl.getSpecification(), endTime - startTime); - double totalEnergy = -energyCharge; + double energyCharge = ((BatteryCharging) ev.getChargingPower()).calcEnergyCharged(chargerImpl.getSpecification(), endTime - startTime); + double totalEnergy = -energyCharge; ((ChargingWithAssignmentLogic) chargerImpl.getLogic()).assignVehicle(ev); dropoffStopTask = ((ShiftEDrtTaskFactoryImpl) taskFactory).createChargingShiftBreakTask(vehicle, startTime, endTime, link, shiftBreak, chargerImpl, totalEnergy, breakFacility); @@ -199,32 +199,32 @@ private void relocateForBreakImpl(ShiftDvrpVehicle vehicle, double startTime, do final double latestTimeConstraintArrival = shiftBreak.getLatestBreakEndTime() - shiftBreak.getDuration(); - shiftBreak.schedule(Math.min(latestDetourArrival, latestTimeConstraintArrival)); + shiftBreak.schedule(Math.min(latestDetourArrival, latestTimeConstraintArrival)); } private Optional charge(OperationFacility breakFacility, ElectricVehicle electricVehicle) { if (chargingInfrastructure != null) { - List> chargerIds = breakFacility.getChargers(); - if(!chargerIds.isEmpty()) { - Optional selectedCharger = chargerIds - .stream() - .map(id -> chargingInfrastructure.getChargers().get(id)) - .filter(charger -> shiftsParams.breakChargerType.equals(charger.getChargerType())) - .min((c1, c2) -> { - final double waitTime = ChargingEstimations - .estimateMaxWaitTimeForNextVehicle(c1); - final double waitTime2 = ChargingEstimations - .estimateMaxWaitTimeForNextVehicle(c2); - return Double.compare(waitTime, waitTime2); - }); - if(selectedCharger.isPresent()) { - if (selectedCharger.get().getLogic().getChargingStrategy().isChargingCompleted(electricVehicle)) { - return Optional.empty(); - } - } - return selectedCharger; - } - } + List> chargerIds = breakFacility.getChargers(); + if (!chargerIds.isEmpty()) { + Optional selectedCharger = chargerIds + .stream() + .map(id -> chargingInfrastructure.getChargers().get(id)) + .filter(charger -> shiftsParams.breakChargerType.equals(charger.getChargerType())) + .min((c1, c2) -> { + final double waitTime = ChargingEstimations + .estimateMaxWaitTimeForNextVehicle(c1); + final double waitTime2 = ChargingEstimations + .estimateMaxWaitTimeForNextVehicle(c2); + return Double.compare(waitTime, waitTime2); + }); + if (selectedCharger.isPresent()) { + if (selectedCharger.get().getLogic().getChargingStrategy().isChargingCompleted(electricVehicle)) { + return Optional.empty(); + } + } + return selectedCharger; + } + } return Optional.empty(); } @@ -234,7 +234,7 @@ public void relocateForShiftChange(DvrpVehicle vehicle, Link link, DrtShift shif final Task currentTask = schedule.getCurrentTask(); if (currentTask instanceof DriveTask && currentTask.getTaskType().equals(EmptyVehicleRelocator.RELOCATE_VEHICLE_TASK_TYPE) - && currentTask.equals(schedule.getTasks().get(schedule.getTaskCount()-2))) { + && currentTask.equals(schedule.getTasks().get(schedule.getTaskCount() - 2))) { //try to divert/cancel relocation LinkTimePair start = ((OnlineDriveTaskTracker) currentTask.getTaskTracker()).getDiversionPoint(); VrpPathWithTravelData path; @@ -317,22 +317,22 @@ private void appendShiftChange(DvrpVehicle vehicle, DrtShift shift, OperationFac // append SHIFT_CHANGEOVER task - ElectricVehicle ev = ((EvDvrpVehicle) vehicle).getElectricVehicle(); - Optional charger = charge(breakFacility, ev); - if (charger.isPresent()) { - Charger chargingImpl = charger.get(); + ElectricVehicle ev = ((EvDvrpVehicle) vehicle).getElectricVehicle(); + Optional charger = charge(breakFacility, ev); + if (charger.isPresent()) { + Charger chargingImpl = charger.get(); - final double waitTime = ChargingEstimations - .estimateMaxWaitTimeForNextVehicle(chargingImpl); + final double waitTime = ChargingEstimations + .estimateMaxWaitTimeForNextVehicle(chargingImpl); - if (ev.getBattery().getCharge() / ev.getBattery().getCapacity() < shiftsParams.chargeDuringBreakThreshold + if (ev.getBattery().getCharge() / ev.getBattery().getCapacity() < shiftsParams.chargeDuringBreakThreshold || ((ChargingWithAssignmentLogic) chargingImpl.getLogic()).getAssignedVehicles().contains(ev) - || waitTime >0) { + || waitTime > 0) { dropoffStopTask = taskFactory.createShiftChangeoverTask(vehicle, startTime, endTime, link, shift, breakFacility); } else { - double energyCharge = ((BatteryCharging) ev.getChargingPower()).calcEnergyCharged(chargingImpl.getSpecification(), endTime - startTime); - double totalEnergy = -energyCharge; + double energyCharge = ((BatteryCharging) ev.getChargingPower()).calcEnergyCharged(chargingImpl.getSpecification(), endTime - startTime); + double totalEnergy = -energyCharge; ((ChargingWithAssignmentLogic) chargingImpl.getLogic()).assignVehicle(ev); dropoffStopTask = ((ShiftEDrtTaskFactoryImpl) taskFactory).createChargingShiftChangeoverTask(vehicle, startTime, endTime, link, chargingImpl, totalEnergy, shift, breakFacility); @@ -353,9 +353,11 @@ public void startShift(ShiftDvrpVehicle vehicle, double now, DrtShift shift) { if (stayTask instanceof WaitForShiftTask) { ((WaitForShiftTask) stayTask).getFacility().deregisterVehicle(vehicle.getId()); stayTask.setEndTime(now); - if(Schedules.getLastTask(schedule).equals(stayTask)) { + if (Schedules.getLastTask(schedule).equals(stayTask)) { //nothing planned yet. schedule.addTask(taskFactory.createStayTask(vehicle, now, shift.getEndTime(), stayTask.getLink())); + } else { + Schedules.getNextTask(schedule).setBeginTime(now); } } else { throw new IllegalStateException("Vehicle cannot start shift during task:" + stayTask.getTaskType().name()); @@ -370,9 +372,9 @@ public boolean updateShiftChange(ShiftDvrpVehicle vehicle, Link link, DrtShift s VrpPathWithTravelData path = VrpPaths.calcAndCreatePath(start.link, link, Math.max(start.time, timer.getTimeOfDay()), router, travelTime); //if (path.getArrivalTime() <= shift.getEndTime()) { - updateShiftChangeImpl(vehicle, path, shift, facility, lastTask); - return true; - // } + updateShiftChangeImpl(vehicle, path, shift, facility, lastTask); + return true; + // } } return false; } @@ -381,29 +383,28 @@ public boolean updateShiftChange(ShiftDvrpVehicle vehicle, Link link, DrtShift s public void planAssignedShift(ShiftDvrpVehicle vehicle, double timeStep, DrtShift shift) { Schedule schedule = vehicle.getSchedule(); StayTask stayTask = (StayTask) schedule.getCurrentTask(); - if (stayTask instanceof WaitForShiftTask waitForShiftTask) { - if(waitForShiftTask instanceof EDrtWaitForShiftTask eDrtWaitForShiftTask) { - if(eDrtWaitForShiftTask.getChargingTask() != null) { - Task nextTask = Schedules.getNextTask(vehicle.getSchedule()); - if(nextTask instanceof WaitForShiftTask) { - // set +1 to ensure this update happens after next shift start check - nextTask.setEndTime(Math.max(timeStep + 1, shift.getStartTime())); - //append stay task if required - if(Schedules.getLastTask(schedule).equals(nextTask)) { - schedule.addTask(taskFactory.createStayTask(vehicle, nextTask.getEndTime(), shift.getEndTime(), ((WaitForShiftTask) nextTask).getLink())); - } - } else { - throw new RuntimeException(); - } - } else { - stayTask.setEndTime(Math.max(timeStep +1 , shift.getStartTime())); + if (stayTask instanceof EDrtWaitForShiftTask eDrtWaitForShiftTask) { + if (eDrtWaitForShiftTask.getChargingTask() != null) { + Task nextTask = Schedules.getNextTask(vehicle.getSchedule()); + if (nextTask instanceof WaitForShiftTask) { + // set +1 to ensure this update happens after next shift start check + nextTask.setEndTime(Math.max(timeStep + 1, shift.getStartTime())); //append stay task if required - if(Schedules.getLastTask(schedule).equals(stayTask)) { - schedule.addTask(taskFactory.createStayTask(vehicle, stayTask.getEndTime(), shift.getEndTime(), stayTask.getLink())); + if (Schedules.getLastTask(schedule).equals(nextTask)) { + schedule.addTask(taskFactory.createStayTask(vehicle, nextTask.getEndTime(), shift.getEndTime(), ((WaitForShiftTask) nextTask).getLink())); } + } else { + throw new RuntimeException(); + } + } else { + stayTask.setEndTime(Math.max(timeStep + 1, shift.getStartTime())); + //append stay task if required + if (Schedules.getLastTask(schedule).equals(stayTask)) { + schedule.addTask(taskFactory.createStayTask(vehicle, stayTask.getEndTime(), shift.getEndTime(), stayTask.getLink())); } } } + } @@ -412,10 +413,10 @@ public void cancelAssignedShift(ShiftDvrpVehicle vehicle, double timeStep, DrtSh Schedule schedule = vehicle.getSchedule(); StayTask stayTask = (StayTask) schedule.getCurrentTask(); if (stayTask instanceof WaitForShiftTask waitForShiftTask) { - if(waitForShiftTask instanceof EDrtWaitForShiftTask eDrtWaitForShiftTask) { - if(eDrtWaitForShiftTask.getChargingTask() != null) { + if (waitForShiftTask instanceof EDrtWaitForShiftTask eDrtWaitForShiftTask) { + if (eDrtWaitForShiftTask.getChargingTask() != null) { Task nextTask = Schedules.getNextTask(vehicle.getSchedule()); - if(nextTask instanceof WaitForShiftTask) { + if (nextTask instanceof WaitForShiftTask) { nextTask.setEndTime(vehicle.getServiceEndTime()); } else { throw new RuntimeException(); @@ -433,15 +434,15 @@ private void updateShiftChangeImpl(DvrpVehicle vehicle, VrpPathWithTravelData vr DrtShift shift, OperationFacility facility, Task lastTask) { Schedule schedule = vehicle.getSchedule(); - Optional oldChangeOver = ShiftSchedules.getNextShiftChangeover(schedule); - if(oldChangeOver.isPresent() && oldChangeOver.get() instanceof EDrtShiftChangeoverTaskImpl) { - if(((EDrtShiftChangeoverTaskImpl) oldChangeOver.get()).getChargingTask() != null) { - ElectricVehicle ev = ((EvDvrpVehicle) vehicle).getElectricVehicle(); - ((EDrtShiftChangeoverTaskImpl) oldChangeOver.get()).getChargingTask().getChargingLogic().unassignVehicle(ev); - } - } + Optional oldChangeOver = ShiftSchedules.getNextShiftChangeover(schedule); + if (oldChangeOver.isPresent() && oldChangeOver.get() instanceof EDrtShiftChangeoverTaskImpl) { + if (((EDrtShiftChangeoverTaskImpl) oldChangeOver.get()).getChargingTask() != null) { + ElectricVehicle ev = ((EvDvrpVehicle) vehicle).getElectricVehicle(); + ((EDrtShiftChangeoverTaskImpl) oldChangeOver.get()).getChargingTask().getChargingLogic().unassignVehicle(ev); + } + } - List copy = new ArrayList<>(schedule.getTasks().subList(lastTask.getTaskIdx() + 1, schedule.getTasks().size())); + List copy = new ArrayList<>(schedule.getTasks().subList(lastTask.getTaskIdx() + 1, schedule.getTasks().size())); for (Task task : copy) { schedule.removeTask(task); } @@ -449,7 +450,7 @@ private void updateShiftChangeImpl(DvrpVehicle vehicle, VrpPathWithTravelData vr ((OnlineDriveTaskTracker) lastTask.getTaskTracker()).divertPath(vrpPath); } else { //add drive to break location - lastTask.setEndTime(vrpPath.getDepartureTime()); + lastTask.setEndTime(vrpPath.getDepartureTime()); schedule.addTask(taskFactory.createDriveTask(vehicle, vrpPath, RELOCATE_VEHICLE_SHIFT_CHANGEOVER_TASK_TYPE)); // add RELOCATE } diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/dispatcher/DrtShiftDispatcherImpl.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/dispatcher/DrtShiftDispatcherImpl.java index 6e082f6f7d5..b021d811b3b 100644 --- a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/dispatcher/DrtShiftDispatcherImpl.java +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/dispatcher/DrtShiftDispatcherImpl.java @@ -218,31 +218,46 @@ private void assignShifts(double timeStep) { for (DrtShift shift : assignableShifts) { ShiftDvrpVehicle vehicle = null; - for (ShiftEntry active : activeShifts) { - if (active.shift().getEndTime() > shift.getStartTime()) { - break; + if(shift.getDesignatedVehicleId().isPresent()) { + DvrpVehicle designatedVehicle = fleet.getVehicles().get(shift.getDesignatedVehicleId().get()); + Verify.verify(designatedVehicle.getSchedule().getStatus() == Schedule.ScheduleStatus.STARTED); + Verify.verify(designatedVehicle instanceof ShiftDvrpVehicle); + if(!((ShiftDvrpVehicle) designatedVehicle).getShifts().isEmpty()) { + continue; } if(shift.getOperationFacilityId().isPresent()) { - //we have to check that the vehicle ends the previous shift at the same facility where - //the new shift is to start. - if(active.shift().getOperationFacilityId().isPresent()) { - if(!active.shift().getOperationFacilityId().get().equals(shift.getOperationFacilityId().get())) { - continue; - } - } else { - Optional nextShiftChangeover = ShiftSchedules.getNextShiftChangeover(active.vehicle().getSchedule()); - if(nextShiftChangeover.isPresent()) { - Verify.verify(nextShiftChangeover.get().getShift().equals(active.shift())); - if(!nextShiftChangeover.get().getFacility().getId().equals(shift.getOperationFacilityId().get())) { - // there is already a shift changeover scheduled elsewhere + Verify.verify(idleVehiclesQueues.get(shift.getOperationFacilityId().get()).contains(designatedVehicle)); + } + vehicle = (ShiftDvrpVehicle) designatedVehicle; + } + + if(vehicle == null) { + for (ShiftEntry active : activeShifts) { + if (active.shift().getEndTime() > shift.getStartTime()) { + break; + } + if (shift.getOperationFacilityId().isPresent()) { + //we have to check that the vehicle ends the previous shift at the same facility where + //the new shift is to start. + if (active.shift().getOperationFacilityId().isPresent()) { + if (!active.shift().getOperationFacilityId().get().equals(shift.getOperationFacilityId().get())) { continue; } + } else { + Optional nextShiftChangeover = ShiftSchedules.getNextShiftChangeover(active.vehicle().getSchedule()); + if (nextShiftChangeover.isPresent()) { + Verify.verify(nextShiftChangeover.get().getShift().equals(active.shift())); + if (!nextShiftChangeover.get().getFacility().getId().equals(shift.getOperationFacilityId().get())) { + // there is already a shift changeover scheduled elsewhere + continue; + } + } } } - } - if (assignShiftToVehicleLogic.canAssignVehicleToShift(active.vehicle(), shift)) { - vehicle = active.vehicle(); - break; + if (assignShiftToVehicleLogic.canAssignVehicleToShift(active.vehicle(), shift)) { + vehicle = active.vehicle(); + break; + } } } diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/io/DrtShiftsReader.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/io/DrtShiftsReader.java index f707a8c8108..928fd2360cd 100644 --- a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/io/DrtShiftsReader.java +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/io/DrtShiftsReader.java @@ -8,6 +8,7 @@ import org.matsim.contrib.drt.extension.operations.shifts.shift.DrtShiftBreakSpecificationImpl; import org.matsim.contrib.drt.extension.operations.shifts.shift.DrtShiftSpecificationImpl; import org.matsim.contrib.drt.extension.operations.shifts.shift.DrtShiftsSpecification; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; import org.matsim.core.utils.io.MatsimXmlParser; import org.xml.sax.Attributes; @@ -27,6 +28,7 @@ public class DrtShiftsReader extends MatsimXmlParser { public static final String START_TIME = "start"; public static final String END_TIME = "end"; public static final String OPERATION_FACILITY_ID = "operationFacilityId"; + public static final String DESIGNATED_VEHICLE_ID = "designatedVehicleId"; public static final String EARLIEST_BREAK_START_TIME = "earliestStart"; public static final String LATEST_BREAK_END_TIME = "latestEnd"; @@ -55,8 +57,12 @@ public void startTag(final String name, final Attributes atts, final Stack, DrtShiftSpecification> shifts) throws atts.add(createTuple(END_TIME, shift.getEndTime())); shift.getOperationFacilityId().ifPresent(operationFacilityId -> atts.add(createTuple(OPERATION_FACILITY_ID, operationFacilityId.toString()))); + shift.getDesignatedVehicleId().ifPresent(designatedVehicleId -> + atts.add(createTuple(DESIGNATED_VEHICLE_ID, designatedVehicleId.toString()))); this.writeStartTag(SHIFT_NAME, atts); //Write break, if present diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/run/ShiftDrtModeOptimizerQSimModule.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/run/ShiftDrtModeOptimizerQSimModule.java index a65de06b2c4..64180f24491 100644 --- a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/run/ShiftDrtModeOptimizerQSimModule.java +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/run/ShiftDrtModeOptimizerQSimModule.java @@ -92,7 +92,9 @@ public DrtShifts get() { breakSpec.getLatestBreakEndTime(), breakSpec.getDuration()); } - return new DrtShiftImpl(spec.getId(), spec.getStartTime(), spec.getEndTime(), spec.getOperationFacilityId().orElse(null), shiftBreak); + return new DrtShiftImpl(spec.getId(), spec.getStartTime(), spec.getEndTime(), + spec.getOperationFacilityId().orElse(null), spec.getDesignatedVehicleId().orElse(null), + shiftBreak); }) .collect(ImmutableMap.toImmutableMap(DrtShift::getId, s -> s)); return () -> shifts; diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/scheduler/ShiftTaskSchedulerImpl.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/scheduler/ShiftTaskSchedulerImpl.java index 94b5c3d0fed..c2fcc1ac4ee 100644 --- a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/scheduler/ShiftTaskSchedulerImpl.java +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/scheduler/ShiftTaskSchedulerImpl.java @@ -269,6 +269,8 @@ public void startShift(ShiftDvrpVehicle vehicle, double now, DrtShift shift) { if(Schedules.getLastTask(schedule).equals(stayTask)) { //nothing planned yet. schedule.addTask(taskFactory.createStayTask(vehicle, now, shift.getEndTime(), stayTask.getLink())); + } else { + Schedules.getNextTask(schedule).setBeginTime(now); } } else { throw new IllegalStateException("Vehicle cannot start shift during task:" + stayTask.getTaskType().name()); @@ -296,7 +298,8 @@ public void planAssignedShift(ShiftDvrpVehicle vehicle, double timeStep, DrtShif Schedule schedule = vehicle.getSchedule(); StayTask stayTask = (StayTask) schedule.getCurrentTask(); if (stayTask instanceof WaitForShiftTask) { - stayTask.setEndTime(Math.max(timeStep, shift.getStartTime())); + // set +1 to ensure this update happens after next shift start check + stayTask.setEndTime(Math.max(timeStep + 1, shift.getStartTime())); } } diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShift.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShift.java index d641677cd38..24442914ba3 100644 --- a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShift.java +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShift.java @@ -3,6 +3,7 @@ import org.matsim.api.core.v01.Id; import org.matsim.api.core.v01.Identifiable; import org.matsim.contrib.drt.extension.operations.operationFacilities.OperationFacility; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; import java.util.Optional; @@ -15,8 +16,6 @@ public interface DrtShift extends Identifiable { double getEndTime(); - Optional getBreak(); - boolean isStarted(); boolean isEnded(); @@ -26,4 +25,8 @@ public interface DrtShift extends Identifiable { void end(); Optional> getOperationFacilityId(); + + Optional getBreak(); + + Optional> getDesignatedVehicleId(); } diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShiftImpl.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShiftImpl.java index 79373816104..1f929cdd574 100644 --- a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShiftImpl.java +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShiftImpl.java @@ -2,6 +2,7 @@ import org.matsim.api.core.v01.Id; import org.matsim.contrib.drt.extension.operations.operationFacilities.OperationFacility; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; import java.util.Optional; @@ -15,6 +16,7 @@ public class DrtShiftImpl implements DrtShift { private final double start; private final double end; private final Id operationFacilityId; + private final Id designatedVehicleId; private final DrtShiftBreak shiftBreak; @@ -22,12 +24,13 @@ public class DrtShiftImpl implements DrtShift { private boolean ended = false; public DrtShiftImpl(Id id, double start, double end, Id operationFacilityId, - DrtShiftBreak shiftBreak) { + Id designatedVehicleId, DrtShiftBreak shiftBreak) { this.id = id; this.start = start; this.end = end; this.operationFacilityId = operationFacilityId; - this.shiftBreak = shiftBreak; + this.designatedVehicleId = designatedVehicleId; + this.shiftBreak = shiftBreak; } @Override @@ -45,6 +48,11 @@ public Optional getBreak() { return Optional.ofNullable(shiftBreak); } + @Override + public Optional> getDesignatedVehicleId() { + return Optional.ofNullable(designatedVehicleId); + } + @Override public boolean isStarted() { return started; diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShiftSpecification.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShiftSpecification.java index 18f877c45e3..e32ad8a8416 100644 --- a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShiftSpecification.java +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShiftSpecification.java @@ -3,6 +3,7 @@ import org.matsim.api.core.v01.Id; import org.matsim.api.core.v01.Identifiable; import org.matsim.contrib.drt.extension.operations.operationFacilities.OperationFacility; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; import java.util.Optional; @@ -18,4 +19,6 @@ public interface DrtShiftSpecification extends Identifiable { Optional getBreak(); Optional> getOperationFacilityId(); + + Optional> getDesignatedVehicleId(); } diff --git a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShiftSpecificationImpl.java b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShiftSpecificationImpl.java index fd5a36f2501..6e0bc88b0dc 100644 --- a/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShiftSpecificationImpl.java +++ b/contribs/drt-extensions/src/main/java/org/matsim/contrib/drt/extension/operations/shifts/shift/DrtShiftSpecificationImpl.java @@ -2,6 +2,7 @@ import org.matsim.api.core.v01.Id; import org.matsim.contrib.drt.extension.operations.operationFacilities.OperationFacility; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; import java.util.Optional; @@ -15,6 +16,7 @@ public class DrtShiftSpecificationImpl implements DrtShiftSpecification { private final double end; private final DrtShiftBreakSpecification shiftBreak; private final Id operationFacilityId; + private final Id designatedVehicleId; private DrtShiftSpecificationImpl(Builder builder) { this.id = builder.id; @@ -22,6 +24,7 @@ private DrtShiftSpecificationImpl(Builder builder) { this.end = builder.end; this.shiftBreak = builder.shiftBreak; this.operationFacilityId = builder.operationFacilityId; + this.designatedVehicleId = builder.designatedVehicleId; } @Override @@ -44,6 +47,11 @@ public Optional> getOperationFacilityId() { return Optional.ofNullable(operationFacilityId); } + @Override + public Optional> getDesignatedVehicleId() { + return Optional.ofNullable(designatedVehicleId); + } + @Override public Id getId() { return id; @@ -69,6 +77,7 @@ public static final class Builder { private double end; private DrtShiftBreakSpecification shiftBreak; private Id operationFacilityId; + public Id designatedVehicleId; private Builder() { } @@ -97,6 +106,10 @@ public Builder operationFacility(Id operationFacilityId) { this.operationFacilityId = operationFacilityId; return this; } + public Builder designatedVehicle(Id designatedVehicleId) { + this.designatedVehicleId = designatedVehicleId; + return this; + } public DrtShiftSpecificationImpl build() { return new DrtShiftSpecificationImpl(this); diff --git a/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/operations/shifts/ShiftsIOTest.java b/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/operations/shifts/ShiftsIOTest.java index ff5eb57ecdc..ae00d55bd16 100644 --- a/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/operations/shifts/ShiftsIOTest.java +++ b/contribs/drt-extensions/src/test/java/org/matsim/contrib/drt/extension/operations/shifts/ShiftsIOTest.java @@ -15,6 +15,7 @@ import org.matsim.contrib.drt.extension.operations.shifts.shift.DrtShiftSpecification; import org.matsim.contrib.drt.extension.operations.shifts.shift.DrtShiftsSpecification; import org.matsim.contrib.drt.extension.operations.shifts.shift.DrtShiftsSpecificationImpl; +import org.matsim.contrib.dvrp.fleet.DvrpVehicle; import org.matsim.testcases.MatsimTestUtils; /** @@ -35,6 +36,9 @@ public class ShiftsIOTest { private final Id oid1 = Id.create("op1", OperationFacility.class); private final Id oid2 = Id.create("op2", OperationFacility.class); + private final Id vehId1 = Id.create("drt1", DvrpVehicle.class); + private final Id vehId2 = Id.create("drt2", DvrpVehicle.class); + @Test void testBasicReaderWriter() { @@ -69,6 +73,7 @@ private void checkContent(DrtShiftsSpecification shiftsSpecification) { assertEquals(45000., shiftSpecification1.getEndTime(), 0); assertTrue(shiftSpecification1.getOperationFacilityId().isPresent()); assertEquals(oid1, shiftSpecification1.getOperationFacilityId().get()); + assertEquals(vehId1, shiftSpecification1.getDesignatedVehicleId().get()); assertEquals(1800., shiftSpecification1.getBreak().orElseThrow().getDuration(), 0); assertEquals(28800., shiftSpecification1.getBreak().orElseThrow().getEarliestBreakStartTime(), 0); assertEquals(32400., shiftSpecification1.getBreak().orElseThrow().getLatestBreakEndTime(), 0); @@ -80,6 +85,7 @@ private void checkContent(DrtShiftsSpecification shiftsSpecification) { assertEquals(49000., shiftSpecification2.getEndTime(), 0); assertTrue(shiftSpecification2.getOperationFacilityId().isPresent()); assertEquals(oid2, shiftSpecification2.getOperationFacilityId().get()); + assertEquals(vehId2, shiftSpecification2.getDesignatedVehicleId().get()); assertEquals(3600., shiftSpecification2.getBreak().orElseThrow().getDuration(), 0); assertEquals(29200., shiftSpecification2.getBreak().orElseThrow().getEarliestBreakStartTime(), 0); assertEquals(32800., shiftSpecification2.getBreak().orElseThrow().getLatestBreakEndTime(), 0); @@ -91,6 +97,7 @@ private void checkContent(DrtShiftsSpecification shiftsSpecification) { assertEquals(53000., shiftSpecification3.getEndTime(), 0); assertFalse(shiftSpecification3.getOperationFacilityId().isPresent()); assertEquals(Optional.empty(), shiftSpecification3.getOperationFacilityId()); + assertEquals(Optional.empty(), shiftSpecification3.getDesignatedVehicleId()); assertTrue(shiftSpecification3.getBreak().isEmpty()); } } diff --git a/contribs/drt-extensions/test/input/org/matsim/contrib/drt/extension/operations/shifts/testShifts.xml b/contribs/drt-extensions/test/input/org/matsim/contrib/drt/extension/operations/shifts/testShifts.xml index 2cb87bd3c9f..972a28788af 100644 --- a/contribs/drt-extensions/test/input/org/matsim/contrib/drt/extension/operations/shifts/testShifts.xml +++ b/contribs/drt-extensions/test/input/org/matsim/contrib/drt/extension/operations/shifts/testShifts.xml @@ -1,8 +1,8 @@ - + - + diff --git a/contribs/drt/src/main/java/org/matsim/contrib/drt/optimizer/insertion/DefaultUnplannedRequestInserter.java b/contribs/drt/src/main/java/org/matsim/contrib/drt/optimizer/insertion/DefaultUnplannedRequestInserter.java index 0f8443f0141..1854e93ffd1 100644 --- a/contribs/drt/src/main/java/org/matsim/contrib/drt/optimizer/insertion/DefaultUnplannedRequestInserter.java +++ b/contribs/drt/src/main/java/org/matsim/contrib/drt/optimizer/insertion/DefaultUnplannedRequestInserter.java @@ -54,6 +54,7 @@ public class DefaultUnplannedRequestInserter implements UnplannedRequestInserter { private static final Logger log = LogManager.getLogger(DefaultUnplannedRequestInserter.class); public static final String NO_INSERTION_FOUND_CAUSE = "no_insertion_found"; + public static final String OFFER_REJECTED_CAUSE = "offer_rejected"; private final String mode; private final Fleet fleet; @@ -125,17 +126,7 @@ private void scheduleUnplannedRequest(DrtRequest req, Map, Vehic Optional best = insertionSearch.findBestInsertion(req, Collections.unmodifiableCollection(vehicleEntries.values())); if (best.isEmpty()) { - if (!insertionRetryQueue.tryAddFailedRequest(req, now)) { - eventsManager.processEvent( - new PassengerRequestRejectedEvent(now, mode, req.getId(), req.getPassengerIds(), - NO_INSERTION_FOUND_CAUSE)); - log.debug("No insertion found for drt request " - + req - + " with passenger ids=" - + req.getPassengerIds().stream().map(Object::toString).collect(Collectors.joining(",")) - + " fromLinkId=" - + req.getFromLink().getId()); - } + retryOrReject(req, now, NO_INSERTION_FOUND_CAUSE); } else { InsertionWithDetourData insertion = best.get(); @@ -144,26 +135,44 @@ private void scheduleUnplannedRequest(DrtRequest req, Map, Vehic insertion.detourTimeInfo.pickupDetourInfo.departureTime, insertion.detourTimeInfo.dropoffDetourInfo.arrivalTime); - var vehicle = insertion.insertion.vehicleEntry.vehicle; - var pickupDropoffTaskPair = insertionScheduler.scheduleRequest(acceptedRequest.get(), insertion); + if(acceptedRequest.isPresent()) { + var vehicle = insertion.insertion.vehicleEntry.vehicle; + var pickupDropoffTaskPair = insertionScheduler.scheduleRequest(acceptedRequest.get(), insertion); - VehicleEntry newVehicleEntry = vehicleEntryFactory.create(vehicle, now); - if (newVehicleEntry != null) { - vehicleEntries.put(vehicle.getId(), newVehicleEntry); - } else { - vehicleEntries.remove(vehicle.getId()); - } + VehicleEntry newVehicleEntry = vehicleEntryFactory.create(vehicle, now); + if (newVehicleEntry != null) { + vehicleEntries.put(vehicle.getId(), newVehicleEntry); + } else { + vehicleEntries.remove(vehicle.getId()); + } + + double expectedPickupTime = pickupDropoffTaskPair.pickupTask.getBeginTime(); + expectedPickupTime = Math.max(expectedPickupTime, acceptedRequest.get().getEarliestStartTime()); + expectedPickupTime += stopDurationProvider.calcPickupDuration(vehicle, req); - double expectedPickupTime = pickupDropoffTaskPair.pickupTask.getBeginTime(); - expectedPickupTime = Math.max(expectedPickupTime, acceptedRequest.get().getEarliestStartTime()); - expectedPickupTime += stopDurationProvider.calcPickupDuration(vehicle, req); + double expectedDropoffTime = pickupDropoffTaskPair.dropoffTask.getBeginTime(); + expectedDropoffTime += stopDurationProvider.calcDropoffDuration(vehicle, req); - double expectedDropoffTime = pickupDropoffTaskPair.dropoffTask.getBeginTime(); - expectedDropoffTime += stopDurationProvider.calcDropoffDuration(vehicle, req); + eventsManager.processEvent( + new PassengerRequestScheduledEvent(now, mode, req.getId(), req.getPassengerIds(), vehicle.getId(), + expectedPickupTime, expectedDropoffTime)); + } else { + retryOrReject(req, now, OFFER_REJECTED_CAUSE); + } + } + } + private void retryOrReject(DrtRequest req, double now, String cause) { + if (!insertionRetryQueue.tryAddFailedRequest(req, now)) { eventsManager.processEvent( - new PassengerRequestScheduledEvent(now, mode, req.getId(), req.getPassengerIds(), vehicle.getId(), - expectedPickupTime, expectedDropoffTime)); + new PassengerRequestRejectedEvent(now, mode, req.getId(), req.getPassengerIds(), + cause)); + log.debug("No insertion found for drt request " + + req + + " with passenger ids=" + + req.getPassengerIds().stream().map(Object::toString).collect(Collectors.joining(",")) + + " fromLinkId=" + + req.getFromLink().getId()); } } } diff --git a/contribs/drt/src/main/java/org/matsim/contrib/drt/scheduler/DefaultRequestInsertionScheduler.java b/contribs/drt/src/main/java/org/matsim/contrib/drt/scheduler/DefaultRequestInsertionScheduler.java index 72082906255..bab43ffc742 100644 --- a/contribs/drt/src/main/java/org/matsim/contrib/drt/scheduler/DefaultRequestInsertionScheduler.java +++ b/contribs/drt/src/main/java/org/matsim/contrib/drt/scheduler/DefaultRequestInsertionScheduler.java @@ -24,6 +24,7 @@ import java.util.List; +import org.matsim.api.core.v01.Id; import org.matsim.api.core.v01.network.Link; import org.matsim.contrib.drt.optimizer.VehicleEntry; import org.matsim.contrib.drt.optimizer.Waypoint; @@ -37,6 +38,7 @@ import org.matsim.contrib.drt.stops.StopTimeCalculator; import org.matsim.contrib.dvrp.fleet.DvrpVehicle; import org.matsim.contrib.dvrp.fleet.Fleet; +import org.matsim.contrib.dvrp.path.DivertedVrpPath; import org.matsim.contrib.dvrp.path.VrpPathWithTravelData; import org.matsim.contrib.dvrp.path.VrpPaths; import org.matsim.contrib.dvrp.schedule.DriveTask; @@ -65,9 +67,9 @@ public class DefaultRequestInsertionScheduler implements RequestInsertionSchedul private final StopTimeCalculator stopTimeCalculator; private final boolean scheduleWaitBeforeDrive; - public DefaultRequestInsertionScheduler(Fleet fleet, MobsimTimer timer, - TravelTime travelTime, ScheduleTimingUpdater scheduleTimingUpdater, DrtTaskFactory taskFactory, - StopTimeCalculator stopTimeCalculator, boolean scheduleWaitBeforeDrive) { + public DefaultRequestInsertionScheduler(Fleet fleet, MobsimTimer timer, TravelTime travelTime, + ScheduleTimingUpdater scheduleTimingUpdater, DrtTaskFactory taskFactory, + StopTimeCalculator stopTimeCalculator, boolean scheduleWaitBeforeDrive) { this.timer = timer; this.travelTime = travelTime; this.scheduleTimingUpdater = scheduleTimingUpdater; @@ -143,7 +145,7 @@ private void verifyConstraints(InsertionWithDetourData insertion) { } private void verifyStructure(Schedule schedule) { - boolean previousDrive = false; + DriveTask previousDrive = null; int startIndex = schedule.getStatus().equals(ScheduleStatus.STARTED) ? schedule.getCurrentTask().getTaskIdx() : 0; @@ -151,11 +153,18 @@ private void verifyStructure(Schedule schedule) { for (int index = startIndex; index < schedule.getTaskCount(); index++) { Task task = schedule.getTasks().get(index); - if (task instanceof DriveTask) { - Verify.verify(!previousDrive); - previousDrive = true; + if (task instanceof DriveTask driveTask) { + if(previousDrive != null) { + Verify.verify(previousDrive.getPath() instanceof DivertedVrpPath, + "The first of two subsequent drive tasks has to be a diverted path."); + Id firstEnd = previousDrive.getPath().getToLink().getId(); + Id secondStart = driveTask.getPath().getFromLink().getId(); + Verify.verify(firstEnd.equals(secondStart), + String.format("Subsequent drive tasks are not connected link %s !=> %s", firstEnd.toString(), secondStart.toString())); + } + previousDrive = driveTask; } else { - previousDrive = false; + previousDrive = null; } } } @@ -343,7 +352,7 @@ private DrtStopTask insertPickup(AcceptedDrtRequest request, InsertionWithDetour } private DrtStopTask insertDropoff(AcceptedDrtRequest request, InsertionWithDetourData insertionWithDetourData, - DrtStopTask pickupTask) { + DrtStopTask pickupTask) { final double now = timer.getTimeOfDay(); var insertion = insertionWithDetourData.insertion; VehicleEntry vehicleEntry = insertion.vehicleEntry; diff --git a/contribs/drt/src/test/java/org/matsim/contrib/drt/run/examples/RunDrtExampleIT.java b/contribs/drt/src/test/java/org/matsim/contrib/drt/run/examples/RunDrtExampleIT.java index 0b2adb16db2..7b62adcace0 100644 --- a/contribs/drt/src/test/java/org/matsim/contrib/drt/run/examples/RunDrtExampleIT.java +++ b/contribs/drt/src/test/java/org/matsim/contrib/drt/run/examples/RunDrtExampleIT.java @@ -26,9 +26,7 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -37,6 +35,10 @@ import org.matsim.contrib.drt.optimizer.DrtRequestInsertionRetryParams; import org.matsim.contrib.drt.optimizer.insertion.repeatedselective.RepeatedSelectiveInsertionSearchParams; import org.matsim.contrib.drt.optimizer.insertion.selective.SelectiveInsertionSearchParams; +import org.matsim.contrib.drt.passenger.AcceptedDrtRequest; +import org.matsim.contrib.drt.passenger.DefaultOfferAcceptor; +import org.matsim.contrib.drt.passenger.DrtOfferAcceptor; +import org.matsim.contrib.drt.passenger.DrtRequest; import org.matsim.contrib.drt.prebooking.PrebookingParams; import org.matsim.contrib.drt.prebooking.logic.ProbabilityBasedPrebookingLogic; import org.matsim.contrib.drt.run.DrtConfigGroup; @@ -53,6 +55,7 @@ import org.matsim.contrib.dvrp.passenger.PassengerRequestScheduledEvent; import org.matsim.contrib.dvrp.passenger.PassengerRequestScheduledEventHandler; import org.matsim.contrib.dvrp.run.AbstractDvrpModeModule; +import org.matsim.contrib.dvrp.run.AbstractDvrpModeQSimModule; import org.matsim.contrib.dvrp.run.DvrpConfigGroup; import org.matsim.contrib.zone.skims.DvrpTravelTimeMatrixParams; import org.matsim.core.config.Config; @@ -60,6 +63,7 @@ import org.matsim.core.controler.AbstractModule; import org.matsim.core.controler.Controler; import org.matsim.core.controler.OutputDirectoryHierarchy.OverwriteFileSetting; +import org.matsim.core.gbl.MatsimRandom; import org.matsim.core.utils.io.IOUtils; import org.matsim.examples.ExamplesUtils; import org.matsim.testcases.MatsimTestUtils; @@ -392,6 +396,39 @@ void testRunDrtWithPrebooking() { verifyDrtCustomerStatsCloseToExpectedStats(utils.getOutputDirectory(), expectedStats); } + + @Test + void testRunDrtOfferRejectionExample() { + Id.resetCaches(); + URL configUrl = IOUtils.extendUrl(ExamplesUtils.getTestScenarioURL("mielec"), + "mielec_stop_based_drt_config.xml"); + Config config = ConfigUtils.loadConfig(configUrl, new MultiModeDrtConfigGroup(), new DvrpConfigGroup(), + new OTFVisConfigGroup()); + + config.controller().setOverwriteFileSetting(OverwriteFileSetting.deleteDirectoryIfExists); + config.controller().setOutputDirectory(utils.getOutputDirectory()); + + Controler controller = DrtControlerCreator.createControler(config, false); + controller.addOverridingQSimModule(new AbstractDvrpModeQSimModule("drt") { + @Override + protected void configureQSim() { + bindModal(DrtOfferAcceptor.class).toProvider(modalProvider(getter -> new ProbabilisticOfferAcceptor())); + } + }); + controller.run(); + + + var expectedStats = Stats.newBuilder() + .rejectionRate(0.46) + .rejections(174.0) + .waitAverage(222.66) + .inVehicleTravelTimeMean(369.74) + .totalTravelTimeMean(592.4) + .build(); + + verifyDrtCustomerStatsCloseToExpectedStats(utils.getOutputDirectory(), expectedStats); + } + /** * Early warning system: if customer stats vary more than the defined percentage above or below the expected values * then the following unit tests will fail. This is meant to serve as a red flag. @@ -536,4 +573,20 @@ public void install() { }); } } + + private static class ProbabilisticOfferAcceptor implements DrtOfferAcceptor { + + private final DefaultOfferAcceptor delegate = new DefaultOfferAcceptor(); + + private final Random random = new Random(123); + + @Override + public Optional acceptDrtOffer(DrtRequest request, double departureTime, double arrivalTime) { + if (random.nextBoolean()) { + return Optional.empty(); + } else { + return delegate.acceptDrtOffer(request, departureTime, arrivalTime); + } + } + } } diff --git a/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/ChainedPtFareCalculator.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/ChainedPtFareCalculator.java new file mode 100644 index 00000000000..1e95a8b909f --- /dev/null +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/ChainedPtFareCalculator.java @@ -0,0 +1,28 @@ +package org.matsim.contrib.vsp.pt.fare; + +import com.google.inject.Inject; +import org.matsim.api.core.v01.Coord; + +import java.util.Optional; +import java.util.Set; + +/** + * This class is a {@link PtFareCalculator} that chains multiple {@link PtFareCalculator}s together. As soon as one of the chained calculators + * returns a fare, this calculator returns that fare. In {@link PtFareModule} all available {@link PtFareCalculator}s are bound to this class. The + * order in which the calculators are bound is determined by the priority of the {@link PtFareParams} they are created from. + */ +public class ChainedPtFareCalculator implements PtFareCalculator { + @Inject + private Set fareCalculators; + + @Override + public Optional calculateFare(Coord from, Coord to) { + for (PtFareCalculator fareCalculator : fareCalculators) { + Optional fareResult = fareCalculator.calculateFare(from, to); + if (fareResult.isPresent()) { + return fareResult; + } + } + return Optional.empty(); + } +} diff --git a/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/ChainedPtFareHandler.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/ChainedPtFareHandler.java new file mode 100644 index 00000000000..fef87f65ba3 --- /dev/null +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/ChainedPtFareHandler.java @@ -0,0 +1,68 @@ +package org.matsim.contrib.vsp.pt.fare; + +import com.google.inject.Inject; +import org.matsim.api.core.v01.Coord; +import org.matsim.api.core.v01.Id; +import org.matsim.api.core.v01.events.ActivityStartEvent; +import org.matsim.api.core.v01.events.PersonMoneyEvent; +import org.matsim.api.core.v01.population.Person; +import org.matsim.core.api.experimental.events.AgentWaitingForPtEvent; +import org.matsim.core.api.experimental.events.EventsManager; +import org.matsim.core.router.StageActivityTypeIdentifier; +import org.matsim.pt.PtConstants; + +import java.util.HashMap; +import java.util.Map; + +public class ChainedPtFareHandler implements PtFareHandler { + @Inject + private EventsManager events; + + private final Map, Coord> personDepartureCoordMap = new HashMap<>(); + private final Map, Coord> personArrivalCoordMap = new HashMap<>(); + + @Inject + private ChainedPtFareCalculator fareCalculator; + + @Override + public void handleEvent(ActivityStartEvent event) { + if (event.getActType().equals(PtConstants.TRANSIT_ACTIVITY_TYPE)) { + personDepartureCoordMap.computeIfAbsent(event.getPersonId(), c -> event.getCoord()); // The departure place is fixed to the place of + // first pt interaction an agent has in the whole leg + personArrivalCoordMap.put(event.getPersonId(), event.getCoord()); // The arrival stop will keep updating until the agent start a real + // activity (i.e. finish the leg) + } + + if (StageActivityTypeIdentifier.isStageActivity(event.getActType())) { + return; + } + + Id personId = event.getPersonId(); + if (!personDepartureCoordMap.containsKey(personId)) { + return; + } + + Coord from = personDepartureCoordMap.get(personId); + Coord to = personArrivalCoordMap.get(personId); + + PtFareCalculator.FareResult fare = fareCalculator.calculateFare(from, to).orElseThrow(); + + // charge fare to the person + events.processEvent(new PersonMoneyEvent(event.getTime(), event.getPersonId(), -fare.fare(), PtFareConfigGroup.PT_FARE, + fare.transactionPartner(), event.getPersonId().toString())); + + personDepartureCoordMap.remove(personId); + personArrivalCoordMap.remove(personId); + } + + @Override + public void handleEvent(AgentWaitingForPtEvent event) { + //TODO + } + + @Override + public void reset(int iteration) { + personArrivalCoordMap.clear(); + personDepartureCoordMap.clear(); + } +} diff --git a/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/DistanceBasedPtFareCalculator.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/DistanceBasedPtFareCalculator.java new file mode 100644 index 00000000000..74c1d13b9b5 --- /dev/null +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/DistanceBasedPtFareCalculator.java @@ -0,0 +1,79 @@ +package org.matsim.contrib.vsp.pt.fare; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.locationtech.jts.geom.Geometry; +import org.matsim.api.core.v01.Coord; +import org.matsim.application.options.ShpOptions; +import org.matsim.core.utils.geometry.CoordUtils; +import org.matsim.core.utils.geometry.geotools.MGC; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.SortedMap; + +/** + * This class calculates the fare for a public transport trip based on the distance between the origin and destination. If a shape file is + * provided, the + * fare will be calculated only if the trip is within the shape. + */ +public class DistanceBasedPtFareCalculator implements PtFareCalculator { + private static final Logger log = LogManager.getLogger(DistanceBasedPtFareCalculator.class); + + private final double minFare; + private final SortedMap distanceClassFareParams; + private ShpOptions shp = null; + private final String transactionPartner; + + private final Map inShapeCache = new HashMap<>(); + + public DistanceBasedPtFareCalculator(DistanceBasedPtFareParams params) { + this.minFare = params.getMinFare(); + this.distanceClassFareParams = params.getDistanceClassFareParams(); + this.transactionPartner = params.getTransactionPartner(); + + if (params.getFareZoneShp() != null) { + log.info("For DistanceBasedPtFareCalculator '{}' a fare zone shape file was provided. During the computation, the fare will be " + + "calculated only if the trip is within the shape.", params.getDescription()); + this.shp = new ShpOptions(params.getFareZoneShp(), null, null); + } else { + log.info("For DistanceBasedPtFareCalculator '{}' no fare zone shape file was provided. The fare will be calculated for all trips.", + params.getDescription()); + } + } + + @Override + public Optional calculateFare(Coord from, Coord to) { + if (!shapeCheck(from, to)) { + return Optional.empty(); + } + + double distance = CoordUtils.calcEuclideanDistance(from, to); + + double fare = computeFare(distance, minFare, distanceClassFareParams); + return Optional.of(new FareResult(fare, transactionPartner)); + } + + private boolean shapeCheck(Coord from, Coord to) { + if (shp == null) { + return true; + } + return inShapeCache.computeIfAbsent(from, this::inShape) && inShapeCache.computeIfAbsent(to, this::inShape); + } + + private boolean inShape(Coord coord) { + return shp.readFeatures().stream().anyMatch(f -> MGC.coord2Point(coord).within((Geometry) f.getDefaultGeometry())); + } + + public static double computeFare(double distance, double minFare, + SortedMap distanceClassFareParams) { + for (DistanceBasedPtFareParams.DistanceClassLinearFareFunctionParams distanceClassFareParam : distanceClassFareParams.values()) { + if (distance <= distanceClassFareParam.getMaxDistance()) { + return Math.max(minFare, distance * distanceClassFareParam.getFareSlope() + distanceClassFareParam.getFareIntercept()); + } + } + log.error("No fare found for distance of " + distance + " meters."); + throw new RuntimeException("No fare found for distance of " + distance + " meters."); + } +} diff --git a/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/DistanceBasedPtFareParams.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/DistanceBasedPtFareParams.java new file mode 100644 index 00000000000..6a968d80dda --- /dev/null +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/DistanceBasedPtFareParams.java @@ -0,0 +1,211 @@ +package org.matsim.contrib.vsp.pt.fare; + +import jakarta.validation.constraints.PositiveOrZero; +import org.apache.commons.math.stat.regression.SimpleRegression; +import org.matsim.core.config.ReflectiveConfigGroup; + +import java.util.*; + +/** + * @author Chengqi Lu (luchengqi7) + * The parameters for the distance-based PT trip fare calculation. + * The default values are set based on the fitting results of the trip and fare data collected on September 2021 + * The values are based on the standard unit of meter (m) and Euro (EUR) + */ +public class DistanceBasedPtFareParams extends PtFareParams { + public static final DistanceBasedPtFareParams GERMAN_WIDE_FARE = germanWideFare(); + + public static final String SET_NAME = "ptFareCalculationDistanceBased"; + public static final String MIN_FARE = "minFare"; + + @PositiveOrZero + private double minFare = 2.0; + + public DistanceBasedPtFareParams() { + super(SET_NAME); + } + + @Override + public Map getComments() { + Map map = super.getComments(); + map.put(MIN_FARE, "Minimum fare for a PT trip (e.g. Kurzstrecke/short distance ticket in cities, ticket for 1 zone in rural areas). " + + "Default is 2.0EUR."); + return map; + } + + @StringGetter(MIN_FARE) + public double getMinFare() { + return minFare; + } + + @StringSetter(MIN_FARE) + public void setMinFare(double minFare) { + this.minFare = minFare; + } + + // in Deutschlandtarif, the linear function for the prices above 100km seem to have a different steepness + // hence the following difference in data points + // prices taken from https://deutschlandtarifverbund.de/wp-content/uploads/2024/07/20231201_TBDT_J_10_Preisliste_V07.pdf + // TODO: This fare will change. We might need a way to select the fare of a specific year + private static DistanceBasedPtFareParams germanWideFare() { + final double MIN_FARE = 1.70; + + SimpleRegression under100kmTrip = new SimpleRegression(); + under100kmTrip.addData(1, MIN_FARE); + under100kmTrip.addData(2, 1.90); + under100kmTrip.addData(3, 2.00); + under100kmTrip.addData(4, 2.10); + under100kmTrip.addData(5, 2.20); + under100kmTrip.addData(6, 3.20); + under100kmTrip.addData(7, 3.70); + under100kmTrip.addData(8, 3.80); + under100kmTrip.addData(9, 3.90); + under100kmTrip.addData(10, 4.10); + under100kmTrip.addData(11, 5.00); + under100kmTrip.addData(12, 5.40); + under100kmTrip.addData(13, 5.60); + under100kmTrip.addData(14, 5.80); + under100kmTrip.addData(15, 5.90); + under100kmTrip.addData(16, 6.40); + under100kmTrip.addData(17, 6.50); + under100kmTrip.addData(18, 6.60); + under100kmTrip.addData(19, 6.70); + under100kmTrip.addData(20, 6.90); + under100kmTrip.addData(30, 9.90); + under100kmTrip.addData(40, 13.70); + under100kmTrip.addData(50, 16.30); + under100kmTrip.addData(60, 18.10); + under100kmTrip.addData(70, 20.10); + under100kmTrip.addData(80, 23.20); + under100kmTrip.addData(90, 26.20); + under100kmTrip.addData(100, 28.10); + + SimpleRegression longDistanceTrip = new SimpleRegression(); + longDistanceTrip.addData(100, 28.10); + longDistanceTrip.addData(200, 47.20); + longDistanceTrip.addData(300, 59.70); + longDistanceTrip.addData(400, 71.70); + longDistanceTrip.addData(500, 83.00); + longDistanceTrip.addData(600, 94.60); + longDistanceTrip.addData(700, 106.30); + longDistanceTrip.addData(800, 118.20); + longDistanceTrip.addData(900, 130.10); + longDistanceTrip.addData(1000, 141.00); + longDistanceTrip.addData(1100, 148.60); + longDistanceTrip.addData(1200, 158.10); + longDistanceTrip.addData(1300, 169.20); + longDistanceTrip.addData(1400, 179.80); + longDistanceTrip.addData(1500, 190.10); + longDistanceTrip.addData(1600, 201.50); + longDistanceTrip.addData(1700, 212.80); + longDistanceTrip.addData(1800, 223.30); + longDistanceTrip.addData(1900, 233.90); + longDistanceTrip.addData(2000, 244.00); + + var params = new DistanceBasedPtFareParams(); + + DistanceClassLinearFareFunctionParams distanceClass100kmFareParams = params.getOrCreateDistanceClassFareParams(100_000.); + distanceClass100kmFareParams.setFareSlope(under100kmTrip.getSlope()); + distanceClass100kmFareParams.setFareIntercept(under100kmTrip.getIntercept()); + + DistanceClassLinearFareFunctionParams distanceClassLongFareParams = params.getOrCreateDistanceClassFareParams(Double.POSITIVE_INFINITY); + distanceClassLongFareParams.setFareSlope(longDistanceTrip.getSlope()); + distanceClassLongFareParams.setFareIntercept(longDistanceTrip.getIntercept()); + + params.setTransactionPartner("Deutschlandtarif"); + params.setMinFare(MIN_FARE); + + return params; + } + + public SortedMap getDistanceClassFareParams() { + @SuppressWarnings("unchecked") + final Collection distanceClassFareParams = + (Collection) getParameterSets(DistanceClassLinearFareFunctionParams.SET_NAME); + final SortedMap map = new TreeMap<>(); + + for (DistanceClassLinearFareFunctionParams pars : distanceClassFareParams) { + if (this.isLocked()) { + pars.setLocked(); + } + map.put(pars.getMaxDistance(), pars); + } + if (this.isLocked()) { + return Collections.unmodifiableSortedMap(map); + } else { + return map; + } + } + + public DistanceClassLinearFareFunctionParams getOrCreateDistanceClassFareParams(double maxDistance) { + DistanceClassLinearFareFunctionParams distanceClassFareParams = this.getDistanceClassFareParams().get(maxDistance); + if (distanceClassFareParams == null) { + distanceClassFareParams = new DistanceClassLinearFareFunctionParams(maxDistance); + addParameterSet(distanceClassFareParams); + } + return distanceClassFareParams; + } + + public static class DistanceClassLinearFareFunctionParams extends ReflectiveConfigGroup { + + public static final String SET_NAME = "distanceClassLinearFare"; + public static final String FARE_SLOPE = "fareSlope"; + public static final String FARE_INTERCEPT = "fareIntercept"; + public static final String MAX_DISTANCE = "maxDistance"; + + @PositiveOrZero + private double fareSlope; + @PositiveOrZero + private double fareIntercept; + @PositiveOrZero + private double maxDistance; + + public DistanceClassLinearFareFunctionParams(double maxDistance) { + super(SET_NAME); + this.maxDistance = maxDistance; + } + + @StringGetter(FARE_SLOPE) + public double getFareSlope() { + return fareSlope; + } + + @StringSetter(FARE_SLOPE) + public void setFareSlope(double fareSlope) { + this.fareSlope = fareSlope; + } + + @StringGetter(FARE_INTERCEPT) + public double getFareIntercept() { + return fareIntercept; + } + + + @StringSetter(FARE_INTERCEPT) + public void setFareIntercept(double fareIntercept) { + this.fareIntercept = fareIntercept; + } + + @StringGetter(MAX_DISTANCE) + public double getMaxDistance() { + return maxDistance; + } + + @StringSetter(MAX_DISTANCE) + public void setMaxDistance(double maxDistance) { + this.maxDistance = maxDistance; + } + + @Override + public Map getComments() { + Map map = super.getComments(); + map.put(FARE_SLOPE, "Linear function fare = slope * distance + intercept: the value of the slope factor in currency units / m" + + "(not km!)."); + map.put(FARE_INTERCEPT, "Linear function fare = slope * distance + intercept: the value of the intercept in currency units."); + map.put(MAX_DISTANCE, "The given linear function is applied to trips up to this distance threshold in meters. If set to a finite value" + + ", the linear function for the next distance class will be tried out. If no fare is defined with " + MAX_DISTANCE + " greater than" + + "pt trip length, an error is thrown."); + return map; + } + } +} diff --git a/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/FareZoneBasedPtFareCalculator.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/FareZoneBasedPtFareCalculator.java new file mode 100644 index 00000000000..1066bb4b014 --- /dev/null +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/FareZoneBasedPtFareCalculator.java @@ -0,0 +1,49 @@ +package org.matsim.contrib.vsp.pt.fare; + +import org.geotools.api.feature.simple.SimpleFeature; +import org.locationtech.jts.geom.Geometry; +import org.matsim.api.core.v01.Coord; +import org.matsim.application.options.ShpOptions; +import org.matsim.core.utils.geometry.geotools.MGC; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * This class calculates the fare for a public transport trip based on the fare zone the origin and destination are in. + */ +public class FareZoneBasedPtFareCalculator implements PtFareCalculator { + private final ShpOptions shp; + private final String transactionPartner; + private final Map> zoneByCoordCache = new HashMap<>(); + + public static final String FARE = "fare"; + + public FareZoneBasedPtFareCalculator(FareZoneBasedPtFareParams params) { + this.shp = new ShpOptions(params.getFareZoneShp(), null, null); + transactionPartner = params.getTransactionPartner(); + } + + @Override + public Optional calculateFare(Coord from, Coord to) { + Optional departureZone = zoneByCoordCache.computeIfAbsent(from, this::determineFareZone); + Optional arrivalZone = zoneByCoordCache.computeIfAbsent(to, this::determineFareZone); + + //if one of the zones is empty, it is not included in the shape file, so this calculator cannot compute the fare + if (departureZone.isEmpty() || arrivalZone.isEmpty()) { + return Optional.empty(); + } + + if (!departureZone.get().getID().equals(arrivalZone.get().getID())) { + return Optional.empty(); + } + + Double fare = (Double) departureZone.get().getAttribute(FARE); + return Optional.of(new FareResult(fare, transactionPartner)); + } + + Optional determineFareZone(Coord coord) { + return shp.readFeatures().stream().filter(f -> MGC.coord2Point(coord).within((Geometry) f.getDefaultGeometry())).findFirst(); + } +} diff --git a/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/FareZoneBasedPtFareParams.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/FareZoneBasedPtFareParams.java new file mode 100644 index 00000000000..391c609684e --- /dev/null +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/FareZoneBasedPtFareParams.java @@ -0,0 +1,9 @@ +package org.matsim.contrib.vsp.pt.fare; + +public class FareZoneBasedPtFareParams extends PtFareParams { + public static final String SET_NAME = "ptFareCalculationFareZoneBased"; + + public FareZoneBasedPtFareParams() { + super(SET_NAME); + } +} diff --git a/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareCalculator.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareCalculator.java new file mode 100644 index 00000000000..597f105c0dc --- /dev/null +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareCalculator.java @@ -0,0 +1,12 @@ +package org.matsim.contrib.vsp.pt.fare; + +import org.matsim.api.core.v01.Coord; + +import java.util.Optional; + +public interface PtFareCalculator { + Optional calculateFare(Coord from, Coord to); + + record FareResult(Double fare, String transactionPartner) { + } +} diff --git a/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareConfigGroup.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareConfigGroup.java new file mode 100644 index 00000000000..9001b2ce2e9 --- /dev/null +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareConfigGroup.java @@ -0,0 +1,80 @@ +package org.matsim.contrib.vsp.pt.fare; + +import jakarta.validation.constraints.PositiveOrZero; +import org.matsim.core.config.Config; +import org.matsim.core.config.ReflectiveConfigGroup; + +import java.util.Map; +import java.util.stream.Stream; + +public class PtFareConfigGroup extends ReflectiveConfigGroup { + public static final String PT_FARE = "pt fare"; + public static final String MODULE_NAME = "ptFare"; + public static final String APPLY_UPPER_BOUND = "applyUpperBound"; + public static final String UPPER_BOUND_FACTOR = "upperBoundFactor"; + + public static final String UPPER_BOUND_FACTOR_CMT = "When upper bound is applied, upperBound = upperBoundFactor * max Fare of the day. " + + "This value is decided by the ratio between average daily cost of a ticket subscription and the single " + + "trip ticket of the same trip. Usually this value should be somewhere between 1.0 and 2.0"; + public static final String APPLY_UPPER_BOUND_CMT = "Enable the upper bound for daily PT fare to count for ticket subscription. Input value: " + + "true" + + " or false"; + + private boolean applyUpperBound = true; + @PositiveOrZero + private double upperBoundFactor = 1.5; + + public PtFareConfigGroup() { + super(MODULE_NAME); + } + + @Override + public Map getComments() { + Map map = super.getComments(); + map.put(APPLY_UPPER_BOUND, APPLY_UPPER_BOUND_CMT); + map.put(UPPER_BOUND_FACTOR, UPPER_BOUND_FACTOR_CMT); + return map; + } + + @StringGetter(APPLY_UPPER_BOUND) + public boolean getApplyUpperBound() { + return applyUpperBound; + } + + @StringSetter(APPLY_UPPER_BOUND) + public void setApplyUpperBound(boolean applyUpperBound) { + this.applyUpperBound = applyUpperBound; + } + + + @StringGetter(UPPER_BOUND_FACTOR) + public double getUpperBoundFactor() { + return upperBoundFactor; + } + + @StringSetter(UPPER_BOUND_FACTOR) + public void setUpperBoundFactor(double upperBoundFactor) { + this.upperBoundFactor = upperBoundFactor; + } + + @Override + protected void checkConsistency(Config config) { + super.checkConsistency(config); + + var distanceBasedParameterSets = getParameterSets(DistanceBasedPtFareParams.SET_NAME); + var fareZoneBasedParameterSets = getParameterSets(FareZoneBasedPtFareParams.SET_NAME); + + if (distanceBasedParameterSets.isEmpty() && fareZoneBasedParameterSets.isEmpty()) { + throw new IllegalArgumentException("No parameter sets found for pt fare calculation. Please add at least one parameter set."); + } + + long distinctOrders = Stream.concat(distanceBasedParameterSets.stream(), fareZoneBasedParameterSets.stream()) + .map(PtFareParams.class::cast) + .map(PtFareParams::getOrder) + .distinct().count(); + + if (distinctOrders != distanceBasedParameterSets.size() + fareZoneBasedParameterSets.size()) { + throw new IllegalArgumentException("Duplicate order values found in parameter sets. Please make sure that order values are unique."); + } + } +} diff --git a/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareHandler.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareHandler.java new file mode 100644 index 00000000000..0bd169e0b60 --- /dev/null +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareHandler.java @@ -0,0 +1,8 @@ +package org.matsim.contrib.vsp.pt.fare; + +import org.matsim.api.core.v01.events.handler.ActivityStartEventHandler; +import org.matsim.core.api.experimental.events.handler.AgentWaitingForPtEventHandler; + +public interface PtFareHandler extends ActivityStartEventHandler, AgentWaitingForPtEventHandler { + //other methods for informed mode choice +} diff --git a/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareModule.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareModule.java new file mode 100644 index 00000000000..d3b2f39d02a --- /dev/null +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareModule.java @@ -0,0 +1,48 @@ +package org.matsim.contrib.vsp.pt.fare; + +import com.google.inject.multibindings.Multibinder; +import org.matsim.api.core.v01.TransportMode; +import org.matsim.core.config.ConfigGroup; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.controler.AbstractModule; + +import java.util.Collection; +import java.util.Comparator; +import java.util.stream.Stream; + +public class PtFareModule extends AbstractModule { + + @Override + public void install() { + getConfig().scoring().getModes().get(TransportMode.pt).setDailyMonetaryConstant(0); + getConfig().scoring().getModes().get(TransportMode.pt).setMarginalUtilityOfDistance(0); + Multibinder ptFareCalculator = Multibinder.newSetBinder(binder(), PtFareCalculator.class); + + PtFareConfigGroup ptFareConfigGroup = ConfigUtils.addOrGetModule(this.getConfig(), PtFareConfigGroup.class); + Collection fareZoneBased = ptFareConfigGroup.getParameterSets(FareZoneBasedPtFareParams.SET_NAME); + Collection distanceBased = ptFareConfigGroup.getParameterSets(DistanceBasedPtFareParams.SET_NAME); + + Stream.concat(fareZoneBased.stream(), distanceBased.stream()) + .map(c -> (PtFareParams) c) + .sorted(Comparator.comparing(PtFareParams::getOrder)) + .forEach(p -> { + if (p instanceof FareZoneBasedPtFareParams fareZoneBasedPtFareParams) { + ptFareCalculator.addBinding().toInstance(new FareZoneBasedPtFareCalculator(fareZoneBasedPtFareParams)); + } else if (p instanceof DistanceBasedPtFareParams distanceBasedPtFareParams) { + ptFareCalculator.addBinding().toInstance(new DistanceBasedPtFareCalculator(distanceBasedPtFareParams)); + } else { + throw new RuntimeException("Unknown PtFareParams: " + p.getClass()); + } + }); + + bind(ChainedPtFareCalculator.class); + bind(PtFareHandler.class).to(ChainedPtFareHandler.class); + addEventHandlerBinding().to(PtFareHandler.class); + + if (ptFareConfigGroup.getApplyUpperBound()) { + PtFareUpperBoundHandler ptFareUpperBoundHandler = new PtFareUpperBoundHandler(ptFareConfigGroup.getUpperBoundFactor()); + addEventHandlerBinding().toInstance(ptFareUpperBoundHandler); + addControlerListenerBinding().toInstance(ptFareUpperBoundHandler); + } + } +} diff --git a/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareParams.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareParams.java new file mode 100644 index 00000000000..2ab1c501d4f --- /dev/null +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareParams.java @@ -0,0 +1,71 @@ +package org.matsim.contrib.vsp.pt.fare; + +import org.matsim.core.config.ReflectiveConfigGroup; + +import java.util.Map; + +public abstract class PtFareParams extends ReflectiveConfigGroup { + public static final String FARE_ZONE_SHP = "fareZoneShp"; + public static final String ORDER = "order"; + public static final String TRANSACTION_PARTNER = "transactionPartner"; + public static final String DESCRIPTION = "description"; + + private int order; + private String fareZoneShp; + private String transactionPartner; + private String description; + + public PtFareParams(String name) { + super(name); + } + + @Override + public Map getComments() { + var map = super.getComments(); + map.put(FARE_ZONE_SHP, "Shp file with fare zone(s). This parameter is only used for PtFareCalculationModel 'fareZoneBased'."); + map.put(ORDER, "Order of this fare calculation in the list of fare calculations. Lower values mean to be evaluated first."); + map.put(TRANSACTION_PARTNER, "The transaction partner for the fare calculation. This is used in the PersonMoneyEvent."); + map.put(DESCRIPTION, "Description of the fare zone."); + return map; + } + + @StringGetter(FARE_ZONE_SHP) + public String getFareZoneShp() { + return fareZoneShp; + } + + @StringSetter(FARE_ZONE_SHP) + public void setFareZoneShp(String fareZoneShp) { + this.fareZoneShp = fareZoneShp; + } + + @StringGetter(ORDER) + public int getOrder() { + return order; + } + + @StringSetter(ORDER) + public void setOrder(int order) { + this.order = order; + } + + @StringGetter(TRANSACTION_PARTNER) + public String getTransactionPartner() { + return transactionPartner; + } + + @StringSetter(TRANSACTION_PARTNER) + public void setTransactionPartner(String transactionPartner) { + this.transactionPartner = transactionPartner; + } + + @StringGetter(DESCRIPTION) + public String getDescription() { + return description; + } + + @StringSetter(DESCRIPTION) + public void setDescription(String description) { + this.description = description; + } +} diff --git a/contribs/vsp/src/main/java/playground/vsp/pt/fare/PtFareUpperBoundHandler.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareUpperBoundHandler.java similarity index 98% rename from contribs/vsp/src/main/java/playground/vsp/pt/fare/PtFareUpperBoundHandler.java rename to contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareUpperBoundHandler.java index 6638c12e7f3..5d05602df2d 100644 --- a/contribs/vsp/src/main/java/playground/vsp/pt/fare/PtFareUpperBoundHandler.java +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtFareUpperBoundHandler.java @@ -1,4 +1,4 @@ -package playground.vsp.pt.fare; +package org.matsim.contrib.vsp.pt.fare; import com.google.inject.Inject; import org.matsim.api.core.v01.Id; @@ -10,7 +10,6 @@ import org.matsim.core.config.groups.QSimConfigGroup; import org.matsim.core.controler.events.AfterMobsimEvent; import org.matsim.core.controler.listener.AfterMobsimListener; -import org.matsim.pt.PtConstants; import java.util.ArrayList; import java.util.HashMap; diff --git a/contribs/vsp/src/main/java/playground/vsp/pt/fare/PtTripWithDistanceBasedFareEstimator.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtTripWithDistanceBasedFareEstimator.java similarity index 86% rename from contribs/vsp/src/main/java/playground/vsp/pt/fare/PtTripWithDistanceBasedFareEstimator.java rename to contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtTripWithDistanceBasedFareEstimator.java index 5af45c407fe..11bbaae184d 100644 --- a/contribs/vsp/src/main/java/playground/vsp/pt/fare/PtTripWithDistanceBasedFareEstimator.java +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/pt/fare/PtTripWithDistanceBasedFareEstimator.java @@ -1,4 +1,4 @@ -package playground.vsp.pt.fare; +package org.matsim.contrib.vsp.pt.fare; import com.google.inject.Inject; import it.unimi.dsi.fastutil.doubles.DoubleArrayList; @@ -29,13 +29,22 @@ public class PtTripWithDistanceBasedFareEstimator extends PtTripEstimator { private final Map, TransitStopFacility> facilities; @Inject - public PtTripWithDistanceBasedFareEstimator(TransitSchedule transitSchedule, PtFareConfigGroup config, DistanceBasedPtFareParams ptFare, Scenario scenario) { + public PtTripWithDistanceBasedFareEstimator(TransitSchedule transitSchedule, PtFareConfigGroup config, + Scenario scenario) { super(transitSchedule); this.config = config; - this.ptFare = ptFare; + this.ptFare = extractPtFare(config); this.facilities = scenario.getTransitSchedule().getFacilities(); } + private static DistanceBasedPtFareParams extractPtFare(PtFareConfigGroup config) { + //TODO + return config.getParameterSets(DistanceBasedPtFareParams.SET_NAME).stream() + .map(DistanceBasedPtFareParams.class::cast) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No distance based fare parameters found")); + } + @Override public MinMaxEstimate estimate(EstimatorContext context, String mode, PlanModel plan, List trip, ModeAvailability option) { @@ -55,8 +64,9 @@ public MinMaxEstimate estimate(EstimatorContext context, String mode, PlanModel maxFareUtility = Math.max(fareUtility, maxFareUtility); max = estimate + maxFareUtility; - } else + } else { max = estimate + fareUtility; + } // Distance fareUtility is the highest possible price, therefore the minimum utility return MinMaxEstimate.of(estimate + fareUtility, max); @@ -78,8 +88,9 @@ public double estimate(EstimatorContext context, String mode, String[] modes, Pl List legs = plan.getLegs(mode, i); // Legs can be null if there is a predefined pt trip - if (legs == null) + if (legs == null) { continue; + } //assert legs != null : "Legs must be not null at this point"; @@ -121,8 +132,9 @@ private DoubleDoublePair estimateTrip(EstimatorContext context, List trip) TransitPassengerRoute route = (TransitPassengerRoute) leg.getRoute(); - if (access == null) + if (access == null) { access = facilities.get(route.getAccessStopId()); + } egress = facilities.get(route.getEgressStopId()); @@ -139,9 +151,8 @@ private DoubleDoublePair estimateTrip(EstimatorContext context, List trip) double dist = CoordUtils.calcEuclideanDistance(access.getCoord(), egress.getCoord()); - double fareUtility = -context.scoring.marginalUtilityOfMoney * DistanceBasedPtFareHandler.computeFare(dist, ptFare.getLongDistanceTripThreshold(), ptFare.getMinFare(), - ptFare.getNormalTripIntercept(), ptFare.getNormalTripSlope(), ptFare.getLongDistanceTripIntercept(), ptFare.getLongDistanceTripSlope()); - + double fareUtility = -context.scoring.marginalUtilityOfMoney * DistanceBasedPtFareCalculator.computeFare(dist, + ptFare.getMinFare(), ptFare.getDistanceClassFareParams()); estimate += context.scoring.marginalUtilityOfWaitingPt_s * totalWaitingTime; @@ -165,8 +176,9 @@ private double calcMinimumFare(PlanModel plan) { List legs = plan.getLegs(TransportMode.pt, i); - if (legs == null) + if (legs == null) { continue; + } // first access and last egress TransitStopFacility access = null; @@ -183,8 +195,9 @@ private double calcMinimumFare(PlanModel plan) { TransitPassengerRoute route = (TransitPassengerRoute) leg.getRoute(); hasPT = true; - if (access == null) + if (access == null) { access = facilities.get(route.getAccessStopId()); + } egress = facilities.get(route.getEgressStopId()); } @@ -206,15 +219,14 @@ private double calcMinimumFare(PlanModel plan) { // a single pt trip could never benefit from the upper bound if (n == 1) { - return DistanceBasedPtFareHandler.computeFare(minDist, ptFare.getLongDistanceTripThreshold(), ptFare.getMinFare(), - ptFare.getNormalTripIntercept(), ptFare.getNormalTripSlope(), ptFare.getLongDistanceTripIntercept(), ptFare.getLongDistanceTripSlope()); + return DistanceBasedPtFareCalculator.computeFare(minDist, ptFare.getMinFare(), ptFare.getDistanceClassFareParams()); } // the upper bound is the maximum single trip times a factor // therefore the minimum upper bound is the fare for the second-longest trip // the max costs are then assumed to be evenly distributed over all pt trips - return 1d / n * config.getUpperBoundFactor() * DistanceBasedPtFareHandler.computeFare(secondMinDist, ptFare.getLongDistanceTripThreshold(), ptFare.getMinFare(), - ptFare.getNormalTripIntercept(), ptFare.getNormalTripSlope(), ptFare.getLongDistanceTripIntercept(), ptFare.getLongDistanceTripSlope()); + return 1d / n * config.getUpperBoundFactor() * DistanceBasedPtFareCalculator.computeFare(secondMinDist, + ptFare.getMinFare(), ptFare.getDistanceClassFareParams()); } @Override diff --git a/contribs/vsp/src/main/java/org/matsim/contrib/vsp/scoring/RideScoringParamsFromCarParams.java b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/scoring/RideScoringParamsFromCarParams.java new file mode 100644 index 00000000000..2286692e4d6 --- /dev/null +++ b/contribs/vsp/src/main/java/org/matsim/contrib/vsp/scoring/RideScoringParamsFromCarParams.java @@ -0,0 +1,37 @@ +package org.matsim.contrib.vsp.scoring; + +import org.matsim.api.core.v01.TransportMode; +import org.matsim.core.config.groups.ScoringConfigGroup; + +public class RideScoringParamsFromCarParams { + + /** + * Sets ride scoring params based on car scoring params (including prices). This assumes that the ride passenger somehow has + * to compensate the driver for the additional driving effort to provide the ride. + * + * @param scoringConfigGroup config.scoring() + * @param alpha represents the share of an average ride trip that the car driver has to drive additionally to the car + * trip to provide the ride. Typical values are between 1 (i.e. driver drives additional 10km to provide a 10 km + * ride) and 2 (i.e. driver drives additional 20km to provide a 10 km ride). Alpha can be calibrated and must + * be alpha >= 0. + */ + public static void setRideScoringParamsBasedOnCarParams (ScoringConfigGroup scoringConfigGroup, double alpha) { + ScoringConfigGroup.ModeParams carParams = scoringConfigGroup.getOrCreateModeParams(TransportMode.car); + ScoringConfigGroup.ModeParams rideParams = scoringConfigGroup.getOrCreateModeParams(TransportMode.ride); + + // constant is a calibration parameter and should not be changed + + // ride has no fixed cost + rideParams.setDailyMonetaryConstant(0.0); + + // account for the driver's monetary distance rate. + rideParams.setMonetaryDistanceRate(alpha * carParams.getMonetaryDistanceRate()); + + // rider and driver have marginalUtilityOfDistance + rideParams.setMarginalUtilityOfDistance((alpha + 1.0) * carParams.getMarginalUtilityOfDistance()); + + // rider and driver have marginalUtilityOfTravelling, the driver additionally loses the opportunity to perform an activity + rideParams.setMarginalUtilityOfTraveling((alpha + 1.0) * carParams.getMarginalUtilityOfTraveling() + + alpha * -scoringConfigGroup.getPerforming_utils_hr()); + } +} diff --git a/contribs/vsp/src/main/java/playground/vsp/pt/fare/DistanceBasedPtFareHandler.java b/contribs/vsp/src/main/java/playground/vsp/pt/fare/DistanceBasedPtFareHandler.java deleted file mode 100644 index 79741e54919..00000000000 --- a/contribs/vsp/src/main/java/playground/vsp/pt/fare/DistanceBasedPtFareHandler.java +++ /dev/null @@ -1,81 +0,0 @@ -package playground.vsp.pt.fare; - -import com.google.inject.Inject; -import org.matsim.api.core.v01.Coord; -import org.matsim.api.core.v01.Id; -import org.matsim.api.core.v01.events.ActivityStartEvent; -import org.matsim.api.core.v01.events.PersonMoneyEvent; -import org.matsim.api.core.v01.events.handler.ActivityStartEventHandler; -import org.matsim.api.core.v01.population.Person; -import org.matsim.core.api.experimental.events.EventsManager; -import org.matsim.core.router.StageActivityTypeIdentifier; -import org.matsim.core.utils.geometry.CoordUtils; -import org.matsim.pt.PtConstants; - -import java.util.HashMap; -import java.util.Map; - -public class DistanceBasedPtFareHandler implements ActivityStartEventHandler { - @Inject - private EventsManager events; - - private final double minFare; - private final double shortTripIntercept; - private final double shortTripSlope; - private final double longTripIntercept; - private final double longTripSlope; - private final double longTripThreshold; - - private final Map, Coord> personDepartureCoordMap = new HashMap<>(); - private final Map, Coord> personArrivalCoordMap = new HashMap<>(); - - public DistanceBasedPtFareHandler(DistanceBasedPtFareParams params) { - this.minFare = params.getMinFare(); - this.shortTripIntercept = params.getNormalTripIntercept(); - this.shortTripSlope = params.getNormalTripSlope(); - this.longTripIntercept = params.getLongDistanceTripIntercept(); - this.longTripSlope = params.getLongDistanceTripSlope(); - this.longTripThreshold = params.getLongDistanceTripThreshold(); - } - - @Override - public void handleEvent(ActivityStartEvent event) { - if (event.getActType().equals(PtConstants.TRANSIT_ACTIVITY_TYPE)) { - personDepartureCoordMap.computeIfAbsent(event.getPersonId(), c -> event.getCoord()); // The departure place is fixed to the place of first pt interaction an agent has in the whole leg - personArrivalCoordMap.put(event.getPersonId(), event.getCoord()); // The arrival stop will keep updating until the agent start a real activity (i.e. finish the leg) - } - - if (!StageActivityTypeIdentifier.isStageActivity(event.getActType())) { - Id personId = event.getPersonId(); - if (personDepartureCoordMap.containsKey(personId)) { - double distance = CoordUtils.calcEuclideanDistance - (personDepartureCoordMap.get(personId), personArrivalCoordMap.get(personId)); - - double fare = computeFare(distance, longTripThreshold, minFare, shortTripIntercept, shortTripSlope, longTripIntercept, longTripSlope); - // charge fare to the person - events.processEvent( - new PersonMoneyEvent(event.getTime(), event.getPersonId(), -fare, - PtFareConfigGroup.PT_FARE, DistanceBasedPtFareParams.PT_FARE_DISTANCE_BASED, event.getPersonId().toString())); - - personDepartureCoordMap.remove(personId); - personArrivalCoordMap.remove(personId); - } - } - } - - public static double computeFare(double distance, double longTripThreshold, double minFare, - double shortTripIntercept, double shortTripSlope, - double longTripIntercept, double longTripSlope) { - if (distance <= longTripThreshold) { - return Math.max(minFare, shortTripIntercept + shortTripSlope * distance); - } else { - return Math.max(minFare, longTripIntercept + longTripSlope * distance); - } - } - - @Override - public void reset(int iteration) { - personArrivalCoordMap.clear(); - personDepartureCoordMap.clear(); - } -} diff --git a/contribs/vsp/src/main/java/playground/vsp/pt/fare/DistanceBasedPtFareParams.java b/contribs/vsp/src/main/java/playground/vsp/pt/fare/DistanceBasedPtFareParams.java deleted file mode 100644 index 442d956bf3c..00000000000 --- a/contribs/vsp/src/main/java/playground/vsp/pt/fare/DistanceBasedPtFareParams.java +++ /dev/null @@ -1,130 +0,0 @@ -package playground.vsp.pt.fare; - -import org.matsim.core.config.ReflectiveConfigGroup; - -import jakarta.validation.constraints.PositiveOrZero; - -import java.util.Map; - -/** - * @author Chengqi Lu (luchengqi7) - * The parameters for the distance-based PT trip fare calculation. - * The default values are set based on the fitting results of the trip and fare data collected on September 2021 - * The values are based on the standard unit of meter (m) and Euro (EUR) - */ -public class DistanceBasedPtFareParams extends ReflectiveConfigGroup { - public static final String PT_FARE_DISTANCE_BASED = "distance based pt fare"; - - public static final String SET_NAME = "ptFareCalculationDistanceBased"; - public static final String MIN_FARE = "minFare"; - public static final String NORMAL_TRIP_SLOPE = "normalTripSlope"; - public static final String NORMAL_TRIP_INTERCEPT = "normalTripIntercept"; - public static final String LONG_DISTANCE_TRIP_THRESHOLD = "longDistanceTripThreshold"; - public static final String LONG_DISTANCE_TRIP_SLOPE = "longDistanceTripSlope"; - public static final String LONG_DISTANCE_TRIP_INTERCEPT = "longDistanceTripIntercept"; - public static final String FARE_ZONE_SHP = "fareZoneShp"; - - @PositiveOrZero - private double minFare = 2.0; - @PositiveOrZero - private double normalTripIntercept = 1.6; - @PositiveOrZero - private double normalTripSlope = 0.00017; - @PositiveOrZero - private double longDistanceTripThreshold = 50000.0; - @PositiveOrZero - private double longDistanceTripIntercept = 30.0; - @PositiveOrZero - private double longDistanceTripSlope = 0.00025; - private String fareZoneShp; - - public DistanceBasedPtFareParams() { - super(SET_NAME); - } - - @Override - public Map getComments() { - Map map = super.getComments(); - map.put(MIN_FARE, "Minimum fare for a PT trip " + - "(e.g. Kurzstrecke/short distance ticket in cities, ticket for 1 zone in rural areas)"); - map.put(NORMAL_TRIP_SLOPE, "Linear model y = ax + b: the value of a, for normal trips (e.g. within the city or region)"); - map.put(NORMAL_TRIP_INTERCEPT, "Linear model y = ax + b: the value of b, for normal trips"); - map.put(LONG_DISTANCE_TRIP_SLOPE, "Linear model y = ax + b: the value of a, for long distance trips (e.g. intercity trips)"); - map.put(LONG_DISTANCE_TRIP_INTERCEPT, "Linear model y = ax + b: the value of b, for long trips"); - map.put(LONG_DISTANCE_TRIP_THRESHOLD, "Threshold of the long trips in meters. Below this value, " + - "the trips are considered as normal trips. Above this value, the trips are considered as " + - "inter-city trips"); - map.put(FARE_ZONE_SHP, "Shp file with fare zone(s). This parameter is only used for PtFareCalculationModel 'fareZoneBased'."); - return map; - } - - @StringGetter(MIN_FARE) - public double getMinFare() { - return minFare; - } - - @StringSetter(MIN_FARE) - public void setMinFare(double minFare) { - this.minFare = minFare; - } - - @StringGetter(NORMAL_TRIP_SLOPE) - public double getNormalTripSlope() { - return normalTripSlope; - } - - @StringSetter(NORMAL_TRIP_SLOPE) - public void setNormalTripSlope(double normalTripSlope) { - this.normalTripSlope = normalTripSlope; - } - - @StringGetter(NORMAL_TRIP_INTERCEPT) - public double getNormalTripIntercept() { - return normalTripIntercept; - } - - @StringSetter(NORMAL_TRIP_INTERCEPT) - public void setNormalTripIntercept(double normalTripIntercept) { - this.normalTripIntercept = normalTripIntercept; - } - - @StringGetter(LONG_DISTANCE_TRIP_SLOPE) - public double getLongDistanceTripSlope() { - return longDistanceTripSlope; - } - - @StringSetter(LONG_DISTANCE_TRIP_SLOPE) - public void setLongDistanceTripSlope(double longDistanceTripSlope) { - this.longDistanceTripSlope = longDistanceTripSlope; - } - - @StringGetter(LONG_DISTANCE_TRIP_INTERCEPT) - public double getLongDistanceTripIntercept() { - return longDistanceTripIntercept; - } - - @StringSetter(LONG_DISTANCE_TRIP_INTERCEPT) - public void setLongDistanceTripIntercept(double longDistanceTripIntercept) { - this.longDistanceTripIntercept = longDistanceTripIntercept; - } - - @StringGetter(LONG_DISTANCE_TRIP_THRESHOLD) - public double getLongDistanceTripThreshold() { - return longDistanceTripThreshold; - } - - @StringSetter(LONG_DISTANCE_TRIP_THRESHOLD) - public void setLongDistanceTripThreshold(double longDistanceTripThreshold) { - this.longDistanceTripThreshold = longDistanceTripThreshold; - } - - @StringGetter(FARE_ZONE_SHP) - public String getFareZoneShp() { - return fareZoneShp; - } - - @StringSetter(FARE_ZONE_SHP) - public void setFareZoneShp(String fareZoneShp) { - this.fareZoneShp = fareZoneShp; - } -} diff --git a/contribs/vsp/src/main/java/playground/vsp/pt/fare/FareZoneBasedPtFareHandler.java b/contribs/vsp/src/main/java/playground/vsp/pt/fare/FareZoneBasedPtFareHandler.java deleted file mode 100644 index 962a18fac57..00000000000 --- a/contribs/vsp/src/main/java/playground/vsp/pt/fare/FareZoneBasedPtFareHandler.java +++ /dev/null @@ -1,163 +0,0 @@ -package playground.vsp.pt.fare; - -import com.google.inject.Inject; -import org.apache.commons.math.stat.regression.SimpleRegression; -import org.geotools.api.feature.simple.SimpleFeature; -import org.locationtech.jts.geom.Geometry; -import org.matsim.api.core.v01.Coord; -import org.matsim.api.core.v01.Id; -import org.matsim.api.core.v01.events.ActivityStartEvent; -import org.matsim.api.core.v01.events.PersonMoneyEvent; -import org.matsim.api.core.v01.events.handler.ActivityStartEventHandler; -import org.matsim.api.core.v01.population.Person; -import org.matsim.application.options.ShpOptions; -import org.matsim.core.api.experimental.events.EventsManager; -import org.matsim.core.router.StageActivityTypeIdentifier; -import org.matsim.core.utils.geometry.CoordUtils; -import org.matsim.core.utils.geometry.geotools.MGC; -import org.matsim.pt.PtConstants; - -import java.util.AbstractMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class FareZoneBasedPtFareHandler implements ActivityStartEventHandler { - @Inject - private EventsManager events; - - public static final String FARE = "fare"; - public static final String PT_FARE_ZONE_BASED = "fare zone based pt fare"; - public static final String PT_GERMANWIDE_FARE_BASED = "german-wide fare based pt fare"; - - private final ShpOptions shp; - - private final Map, Coord> personDepartureCoordMap = new HashMap<>(); - private final Map, Coord> personArrivalCoordMap = new HashMap<>(); - - public FareZoneBasedPtFareHandler(DistanceBasedPtFareParams params) { - this.shp = new ShpOptions(params.getFareZoneShp(), null, null); - } - - @Override - public void handleEvent(ActivityStartEvent event) { - - if (event.getActType().equals(PtConstants.TRANSIT_ACTIVITY_TYPE)) { - personDepartureCoordMap.computeIfAbsent(event.getPersonId(), c -> event.getCoord()); // The departure place is fixed to the place of first pt interaction an agent has in the whole leg - personArrivalCoordMap.put(event.getPersonId(), event.getCoord()); // The arrival stop will keep updating until the agent start a real activity (i.e. finish the leg) - } - - if (!StageActivityTypeIdentifier.isStageActivity(event.getActType())) { - Id personId = event.getPersonId(); - if (personDepartureCoordMap.containsKey(personId)) { - double distance = CoordUtils.calcEuclideanDistance - (personDepartureCoordMap.get(personId), personArrivalCoordMap.get(personId)); - - SimpleFeature departureZone = determineFareZone(personDepartureCoordMap.get(personId), shp.readFeatures()); - SimpleFeature arrivalZone = determineFareZone(personArrivalCoordMap.get(personId), shp.readFeatures()); - - Map.Entry fareEntry = computeFare(distance, departureZone, arrivalZone); - // charge fare to the person - events.processEvent( - new PersonMoneyEvent(event.getTime(), event.getPersonId(), -fareEntry.getValue(), - PtFareConfigGroup.PT_FARE, fareEntry.getKey(), event.getPersonId().toString())); - - personDepartureCoordMap.remove(personId); - personArrivalCoordMap.remove(personId); - } - } - } - - public static Map.Entry computeFare(double distance, SimpleFeature departureZone, SimpleFeature arrivalZone) { - - if (departureZone != null && arrivalZone != null) { -// if both zones are not null -> departure and arrival point are inside of one of the tarifzonen - if (departureZone.getID().equals(arrivalZone.getID())) { - return new AbstractMap.SimpleEntry<>(PT_FARE_ZONE_BASED ,(double) departureZone.getAttribute(FARE)); - } - } -// in every other case return german wide fare / Deutschlandtarif - return getGermanWideFare(distance); - } - - private static Map.Entry getGermanWideFare(double distance) { - - SimpleRegression regression = new SimpleRegression(); - -// in Deutschlandtarif, the linear function for the prices above 100km seem to have a different steepness -// hence the following difference in data points -// prices taken from https://deutschlandtarifverbund.de/wp-content/uploads/2024/07/20231201_TBDT_J_10_Preisliste_V07.pdf - if (distance / 1000 <= 100.) { - regression.addData(1, 1.70); - regression.addData(2,1.90); - regression.addData(3,2.00); - regression.addData(4,2.10); - regression.addData(5,2.20); - regression.addData(6,3.20); - regression.addData(7,3.70); - regression.addData(8,3.80); - regression.addData(9,3.90); - regression.addData(10,4.10); - regression.addData(11,5.00); - regression.addData(12,5.40); - regression.addData(13,5.60); - regression.addData(14,5.80); - regression.addData(15,5.90); - regression.addData(16,6.40); - regression.addData(17,6.50); - regression.addData(18,6.60); - regression.addData(19,6.70); - regression.addData(20,6.90); - regression.addData(30,9.90); - regression.addData(40,13.70); - regression.addData(50,16.30); - regression.addData(60,18.10); - regression.addData(70,20.10); - regression.addData(80,23.20); - regression.addData(90,26.20); - regression.addData(100,28.10); - } else { - regression.addData(100,28.10); - regression.addData(200,47.20); - regression.addData(300,59.70); - regression.addData(400,71.70); - regression.addData(500,83.00); - regression.addData(600,94.60); - regression.addData(700,106.30); - regression.addData(800,118.20); - regression.addData(900,130.10); - regression.addData(1000,141.00); - regression.addData(1100,148.60); - regression.addData(1200,158.10); - regression.addData(1300,169.20); - regression.addData(1400,179.80); - regression.addData(1500,190.10); - regression.addData(1600,201.50); - regression.addData(1700,212.80); - regression.addData(1800,223.30); - regression.addData(1900,233.90); - regression.addData(2000,244.00); - } - return new AbstractMap.SimpleEntry<>(PT_GERMANWIDE_FARE_BASED, regression.getSlope() * distance / 1000 + regression.getIntercept()); - } - - static SimpleFeature determineFareZone(Coord coord, List features) { - SimpleFeature zone = null; - - for (SimpleFeature ft : features) { - Geometry geom = (Geometry) ft.getDefaultGeometry(); - - if (MGC.coord2Point(coord).within(geom)) { - zone = ft; - break; - } - } - return zone; - } - - @Override - public void reset(int iteration) { - personArrivalCoordMap.clear(); - personDepartureCoordMap.clear(); - } -} diff --git a/contribs/vsp/src/main/java/playground/vsp/pt/fare/PtFareConfigGroup.java b/contribs/vsp/src/main/java/playground/vsp/pt/fare/PtFareConfigGroup.java deleted file mode 100644 index 8abf76dcb37..00000000000 --- a/contribs/vsp/src/main/java/playground/vsp/pt/fare/PtFareConfigGroup.java +++ /dev/null @@ -1,75 +0,0 @@ -package playground.vsp.pt.fare; - -import org.matsim.core.config.ReflectiveConfigGroup; - -import jakarta.validation.constraints.PositiveOrZero; - -import java.util.Map; - -public class PtFareConfigGroup extends ReflectiveConfigGroup { - public static final String PT_FARE = "pt fare"; - public static final String MODULE_NAME = "ptFare"; - public static final String PT_FARE_CALCULATION = "ptFareCalculation"; - public static final String APPLY_UPPER_BOUND = "applyUpperBound"; - public static final String UPPER_BOUND_FACTOR = "upperBoundFactor"; - - public enum PtFareCalculationModels {distanceBased, fareZoneBased} // More to come (e.g. zone based, hybrid...) - - private static final String PT_FARE_CALCULATION_CMT = "PT fare calculation scheme. Current implementation: distanceBased (more to come...)"; - public static final String UPPER_BOUND_FACTOR_CMT = "When upper bound is applied, upperBound = upperBoundFactor * max Fare of the day. " + - "This value is decided by the ratio between average daily cost of a ticket subscription and the single " + - "trip ticket of the same trip. Usually this value should be somewhere between 1.0 and 2.0"; - public static final String APPLY_UPPER_BOUND_CMT = "Enable the upper bound for daily PT fare to count for ticket subscription. Input value: true or false"; - - private PtFareCalculationModels ptFareCalculation = PtFareCalculationModels.distanceBased; // Use distance based calculation by default - private boolean applyUpperBound = true; - @PositiveOrZero - private double upperBoundFactor = 1.5; - - public PtFareConfigGroup() { - super(MODULE_NAME); - } - - @Override - public Map getComments() { - Map map = super.getComments(); - map.put(PT_FARE_CALCULATION, PT_FARE_CALCULATION_CMT ); - map.put(APPLY_UPPER_BOUND, APPLY_UPPER_BOUND_CMT ); - map.put(UPPER_BOUND_FACTOR, UPPER_BOUND_FACTOR_CMT ); - return map; - } - - @StringGetter(PT_FARE_CALCULATION) - public PtFareCalculationModels getPtFareCalculation() { - return ptFareCalculation; - } - - @StringSetter(PT_FARE_CALCULATION) - public void setPtFareCalculationModel(PtFareCalculationModels ptFareCalculation) { - this.ptFareCalculation = ptFareCalculation; - } - - @StringGetter(APPLY_UPPER_BOUND) - public boolean getApplyUpperBound() { - return applyUpperBound; - } - - @StringSetter(APPLY_UPPER_BOUND) - public void setApplyUpperBound(boolean applyUpperBound) { - this.applyUpperBound = applyUpperBound; - } - - - @StringGetter(UPPER_BOUND_FACTOR) - public double getUpperBoundFactor() { - return upperBoundFactor; - } - - /** - * @param upperBoundFactor -- {@value #UPPER_BOUND_FACTOR_CMT} - */ - @StringSetter(UPPER_BOUND_FACTOR) - public void setUpperBoundFactor(double upperBoundFactor) { - this.upperBoundFactor = upperBoundFactor; - } -} diff --git a/contribs/vsp/src/main/java/playground/vsp/pt/fare/PtFareModule.java b/contribs/vsp/src/main/java/playground/vsp/pt/fare/PtFareModule.java deleted file mode 100644 index 4da41c16612..00000000000 --- a/contribs/vsp/src/main/java/playground/vsp/pt/fare/PtFareModule.java +++ /dev/null @@ -1,31 +0,0 @@ -package playground.vsp.pt.fare; - -import org.matsim.api.core.v01.TransportMode; -import org.matsim.core.config.ConfigUtils; -import org.matsim.core.controler.AbstractModule; - -public class PtFareModule extends AbstractModule { - - @Override - public void install() { - getConfig().scoring().getModes().get(TransportMode.pt).setDailyMonetaryConstant(0); - getConfig().scoring().getModes().get(TransportMode.pt).setMarginalUtilityOfDistance(0); - PtFareConfigGroup ptFareConfigGroup = ConfigUtils.addOrGetModule(this.getConfig(), PtFareConfigGroup.class); - DistanceBasedPtFareParams distanceBasedPtFareParams = ConfigUtils.addOrGetModule(this.getConfig(), DistanceBasedPtFareParams.class); - - if (ptFareConfigGroup.getPtFareCalculation() == PtFareConfigGroup.PtFareCalculationModels.distanceBased) { - addEventHandlerBinding().toInstance(new DistanceBasedPtFareHandler(distanceBasedPtFareParams)); - } else if (ptFareConfigGroup.getPtFareCalculation() == PtFareConfigGroup.PtFareCalculationModels.fareZoneBased) { - addEventHandlerBinding().toInstance(new FareZoneBasedPtFareHandler(distanceBasedPtFareParams)); - } else { - throw new RuntimeException("Please choose from the following fare Calculation method: [" + - PtFareConfigGroup.PtFareCalculationModels.distanceBased + ", " + PtFareConfigGroup.PtFareCalculationModels.fareZoneBased + "]"); - } - - if (ptFareConfigGroup.getApplyUpperBound()) { - PtFareUpperBoundHandler ptFareUpperBoundHandler = new PtFareUpperBoundHandler(ptFareConfigGroup.getUpperBoundFactor()); - addEventHandlerBinding().toInstance(ptFareUpperBoundHandler); - addControlerListenerBinding().toInstance(ptFareUpperBoundHandler); - } - } -} diff --git a/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/DistanceBasedPtFareCalculatorTest.java b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/DistanceBasedPtFareCalculatorTest.java new file mode 100644 index 00000000000..5a62cff6375 --- /dev/null +++ b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/DistanceBasedPtFareCalculatorTest.java @@ -0,0 +1,79 @@ +package org.matsim.contrib.vsp.pt.fare; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.matsim.api.core.v01.Coord; +import org.matsim.core.utils.geometry.CoordUtils; +import org.matsim.core.utils.io.IOUtils; +import org.matsim.examples.ExamplesUtils; + +import java.net.URL; +import java.util.Optional; + +class DistanceBasedPtFareCalculatorTest { + private static final String TRANSACTION_PARTNER = "TP"; + + @Test + void testNormalDistance() { + //100m -> 1.1 EUR + calculateAndCheck(null, new Coord(0, 0), new Coord(0, 100), 1.1); + } + + @Test + void testLongDistance() { + //3000m -> 4.5 EUR + calculateAndCheck(null, new Coord(0, 0), new Coord(0, 3000), 4.5); + } + + @Test + void testThreshold() { + //2000m -> 3.0 EUR + calculateAndCheck(null, new Coord(0, 0), new Coord(0, 2000), 3.0); + } + + @Test + void testNotInShapeFile() { + URL context = ExamplesUtils.getTestScenarioURL("kelheim"); + String shape = IOUtils.extendUrl(context, "ptTestArea/pt-area.shp").toString(); + Coord a = new Coord(726634.40, 5433508.07); + Coord b = new Coord(736634.40, 5533508.07); + DistanceBasedPtFareCalculator calculator = getCalculator(shape); + Assertions.assertEquals(Optional.empty(), calculator.calculateFare(a, b)); + } + + @Test + void testInShapeFile() { + URL context = ExamplesUtils.getTestScenarioURL("kelheim"); + String shape = IOUtils.extendUrl(context, "ptTestArea/pt-area.shp").toString(); + Coord a = new Coord(710300.624, 5422165.737); + Coord b = new Coord(714940.65, 5420707.78); + double distance = CoordUtils.calcEuclideanDistance(a, b); + calculateAndCheck(shape, a, b, distance * 0.0005 + 3.); + } + + private void calculateAndCheck(String shape, Coord a, Coord b, double fare) { + DistanceBasedPtFareCalculator distanceBasedPtFareCalculator = getCalculator(shape); + PtFareCalculator.FareResult fareResult = distanceBasedPtFareCalculator.calculateFare(a, b).orElseThrow(); + Assertions.assertEquals(new PtFareCalculator.FareResult(fare, TRANSACTION_PARTNER), fareResult); + } + + private DistanceBasedPtFareCalculator getCalculator(String shapeFile) { + var params = new DistanceBasedPtFareParams(); + params.setTransactionPartner(TRANSACTION_PARTNER); + //0-2000m: 1EUR + 1EUR/km + DistanceBasedPtFareParams.DistanceClassLinearFareFunctionParams distanceClass2kmFareParams = + params.getOrCreateDistanceClassFareParams(2000.0); + distanceClass2kmFareParams.setFareIntercept(1.0); + distanceClass2kmFareParams.setFareSlope(0.001); + + //2000m+: 3EUR + 0.5EUR/km + DistanceBasedPtFareParams.DistanceClassLinearFareFunctionParams distanceClassLongFareParams = + params.getOrCreateDistanceClassFareParams(Double.POSITIVE_INFINITY); + distanceClassLongFareParams.setFareIntercept(3.0); + distanceClassLongFareParams.setFareSlope(0.0005); + + params.setMinFare(1.0); + params.setFareZoneShp(shapeFile); + return new DistanceBasedPtFareCalculator(params); + } +} diff --git a/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/FareZoneBasedPtFareCalculatorTest.java b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/FareZoneBasedPtFareCalculatorTest.java new file mode 100644 index 00000000000..a23019c86f3 --- /dev/null +++ b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/FareZoneBasedPtFareCalculatorTest.java @@ -0,0 +1,57 @@ +package org.matsim.contrib.vsp.pt.fare; + +import org.junit.jupiter.api.Test; +import org.matsim.api.core.v01.Coord; +import org.matsim.core.utils.io.IOUtils; +import org.matsim.examples.ExamplesUtils; + +import java.net.URL; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class FareZoneBasedPtFareCalculatorTest { + private static final double COST_IN_SHP_FILE = 1.5; + private static final String TRANSACTION_PARTNER = "TP"; + + @Test + void testCalculateFareInShape() { + FareZoneBasedPtFareCalculator fareZoneBasedPtFareCalculator = new FareZoneBasedPtFareCalculator(getParams()); + + Coord inShape = new Coord(710300.624, 5422165.737); + Coord inShape2 = new Coord(714940.65, 5420707.78); + + assertEquals(Optional.of(new PtFareCalculator.FareResult(COST_IN_SHP_FILE, TRANSACTION_PARTNER)), + fareZoneBasedPtFareCalculator.calculateFare(inShape, + inShape2)); + assertEquals(Optional.of(new PtFareCalculator.FareResult(COST_IN_SHP_FILE, TRANSACTION_PARTNER)), + fareZoneBasedPtFareCalculator.calculateFare(inShape2, + inShape)); + } + + @Test + void testCalculateFareOutShape() { + FareZoneBasedPtFareCalculator fareZoneBasedPtFareCalculator = new FareZoneBasedPtFareCalculator(getParams()); + + Coord inShape = new Coord(710300.624, 5422165.737); + Coord inShape2 = new Coord(714940.65, 5420707.78); + Coord outShape = new Coord(726634.40, 5433508.07); + Coord outShape2 = new Coord(736634.40, 5533508.07); + + assertEquals(Optional.empty(), fareZoneBasedPtFareCalculator.calculateFare(inShape, outShape)); + assertEquals(Optional.empty(), fareZoneBasedPtFareCalculator.calculateFare(inShape, outShape2)); + assertEquals(Optional.empty(), fareZoneBasedPtFareCalculator.calculateFare(inShape2, outShape)); + assertEquals(Optional.empty(), fareZoneBasedPtFareCalculator.calculateFare(inShape2, outShape)); + assertEquals(Optional.empty(), fareZoneBasedPtFareCalculator.calculateFare(outShape, outShape2)); + } + + private FareZoneBasedPtFareParams getParams() { + URL context = ExamplesUtils.getTestScenarioURL("kelheim"); + + FareZoneBasedPtFareParams fareZoneBasedPtFareParams = new FareZoneBasedPtFareParams(); + fareZoneBasedPtFareParams.setFareZoneShp(IOUtils.extendUrl(context, "ptTestArea/pt-area.shp").toString()); + fareZoneBasedPtFareParams.setTransactionPartner(TRANSACTION_PARTNER); + return fareZoneBasedPtFareParams; + } + +} diff --git a/contribs/vsp/src/test/java/playground/vsp/pt/fare/FareZoneBasedPtFareHandlerTest.java b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/FareZoneBasedPtFareHandlerTest.java similarity index 55% rename from contribs/vsp/src/test/java/playground/vsp/pt/fare/FareZoneBasedPtFareHandlerTest.java rename to contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/FareZoneBasedPtFareHandlerTest.java index ec486ec2cee..687a2c4f522 100644 --- a/contribs/vsp/src/test/java/playground/vsp/pt/fare/FareZoneBasedPtFareHandlerTest.java +++ b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/FareZoneBasedPtFareHandlerTest.java @@ -1,12 +1,14 @@ -package playground.vsp.pt.fare; +package org.matsim.contrib.vsp.pt.fare; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import org.matsim.analysis.personMoney.PersonMoneyEventsAnalysisModule; import org.matsim.api.core.v01.Coord; import org.matsim.api.core.v01.Id; import org.matsim.api.core.v01.TransportMode; +import org.matsim.api.core.v01.events.PersonMoneyEvent; +import org.matsim.api.core.v01.events.handler.PersonMoneyEventHandler; import org.matsim.api.core.v01.population.*; import org.matsim.core.config.Config; import org.matsim.core.config.ConfigUtils; @@ -20,47 +22,74 @@ import org.matsim.examples.ExamplesUtils; import org.matsim.testcases.MatsimTestUtils; -import java.io.BufferedReader; -import java.io.IOException; import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import static org.matsim.application.ApplicationUtils.globFile; - public class FareZoneBasedPtFareHandlerTest { + private final static String FARE_ZONE_TRANSACTION_PARTNER = "fare zone transaction partner"; + private final static String DISTANCE_BASED_TRANSACTION_PARTNER = "distance based transaction partner"; + @RegisterExtension private MatsimTestUtils utils = new MatsimTestUtils(); @Test void testFareZoneBasedPtFareHandler() { + //Prepare + Config config = getConfig(); - URL context = ExamplesUtils.getTestScenarioURL("kelheim"); - Config config = ConfigUtils.loadConfig(IOUtils.extendUrl(context, "config.xml")); - config.controller().setOverwriteFileSetting(OutputDirectoryHierarchy.OverwriteFileSetting.deleteDirectoryIfExists); - config.controller().setOutputDirectory(utils.getOutputDirectory()); - config.controller().setLastIteration(0); - + //adapt pt fare PtFareConfigGroup ptFareConfigGroup = ConfigUtils.addOrGetModule(config, PtFareConfigGroup.class); - ptFareConfigGroup.setPtFareCalculationModel(PtFareConfigGroup.PtFareCalculationModels.fareZoneBased); - DistanceBasedPtFareParams fareParams = ConfigUtils.addOrGetModule(config, DistanceBasedPtFareParams.class); - fareParams.setFareZoneShp(IOUtils.extendUrl(context, "ptTestArea/pt-area.shp").toString()); + FareZoneBasedPtFareParams fareZoneBased = new FareZoneBasedPtFareParams(); + fareZoneBased.setDescription("simple fare zone based"); + fareZoneBased.setFareZoneShp(IOUtils.extendUrl(config.getContext(), "ptTestArea/pt-area.shp").toString()); + fareZoneBased.setOrder(1); + fareZoneBased.setTransactionPartner(FARE_ZONE_TRANSACTION_PARTNER); + DistanceBasedPtFareParams distanceBased = new DistanceBasedPtFareParams(); + DistanceBasedPtFareParams.DistanceClassLinearFareFunctionParams distanceClassFareParams = + distanceBased.getOrCreateDistanceClassFareParams(999_999_999.); + distanceClassFareParams.setFareSlope(0.00017); + distanceClassFareParams.setFareIntercept(1.6); + distanceBased.setOrder(2); + distanceBased.setTransactionPartner(DISTANCE_BASED_TRANSACTION_PARTNER); - ScoringConfigGroup scoring = ConfigUtils.addOrGetModule(config, ScoringConfigGroup.class); + ptFareConfigGroup.addParameterSet(fareZoneBased); + ptFareConfigGroup.addParameterSet(distanceBased); - ScoringConfigGroup.ActivityParams homeParams = new ScoringConfigGroup.ActivityParams("home"); - ScoringConfigGroup.ActivityParams workParams = new ScoringConfigGroup.ActivityParams("work"); - homeParams.setTypicalDuration(8 * 3600.); - workParams.setTypicalDuration(8 * 3600.); - scoring.addActivityParams(homeParams); - scoring.addActivityParams(workParams); + MutableScenario scenario = setUpScenario(config); + + //Run + var fareAnalysis = new FareAnalysis(); + + Controler controler = new Controler(scenario); + controler.addOverridingModule(new AbstractModule() { + @Override + public void install() { + install(new PtFareModule()); + addEventHandlerBinding().toInstance(fareAnalysis); + } + }); + controler.run(); + + //Check + List events = fareAnalysis.getEvents(); + Assertions.assertEquals(2, events.size()); + + final String FARE_TEST_PERSON = "fareTestPerson"; + //first event is the fare zone based event + Assertions.assertEquals(new PersonMoneyEvent(33264, Id.createPersonId(FARE_TEST_PERSON), -1.5, "pt fare", FARE_ZONE_TRANSACTION_PARTNER, + FARE_TEST_PERSON), events.get(0)); + //second event is the distance based event + Assertions.assertEquals(new PersonMoneyEvent(52056, Id.createPersonId(FARE_TEST_PERSON), -4.526183060514956, "pt fare", + DISTANCE_BASED_TRANSACTION_PARTNER, FARE_TEST_PERSON), events.get(1)); + } + + private @NotNull MutableScenario setUpScenario(Config config) { MutableScenario scenario = (MutableScenario) ScenarioUtils.loadScenario(config); Population population = ScenarioUtils.createScenario(ConfigUtils.createConfig()).getPopulation(); @@ -69,13 +98,13 @@ void testFareZoneBasedPtFareHandler() { Person person = fac.createPerson(Id.createPersonId("fareTestPerson")); Plan plan = fac.createPlan(); - Activity home = fac.createActivityFromCoord("home", new Coord(710300.624,5422165.737)); + Activity home = fac.createActivityFromCoord("home", new Coord(710300.624, 5422165.737)); // bus to Saal (Donau) work location departs at 09:14 home.setEndTime(9 * 3600.); - Activity work = fac.createActivityFromCoord("work", new Coord(714940.65,5420707.78)); + Activity work = fac.createActivityFromCoord("work", new Coord(714940.65, 5420707.78)); // rb17 to regensburg 2nd home location departs at 13:59 work.setEndTime(13 * 3600. + 45 * 60); - Activity home2 = fac.createActivityFromCoord("home", new Coord(726634.40,5433508.07)); + Activity home2 = fac.createActivityFromCoord("home", new Coord(726634.40, 5433508.07)); Leg leg = fac.createLeg(TransportMode.pt); @@ -88,37 +117,37 @@ void testFareZoneBasedPtFareHandler() { person.addPlan(plan); population.addPerson(person); scenario.setPopulation(population); + return scenario; + } - Controler controler = new Controler(scenario); - controler.addOverridingModule(new AbstractModule() { - @Override - public void install() { - install(new PtFareModule()); - install(new PersonMoneyEventsAnalysisModule()); - } - }); - controler.run(); + private @NotNull Config getConfig() { + URL context = ExamplesUtils.getTestScenarioURL("kelheim"); + Config config = ConfigUtils.loadConfig(IOUtils.extendUrl(context, "config.xml")); + config.controller().setOverwriteFileSetting(OutputDirectoryHierarchy.OverwriteFileSetting.deleteDirectoryIfExists); + config.controller().setOutputDirectory(utils.getOutputDirectory()); + config.controller().setLastIteration(0); - Assertions.assertTrue(Files.exists(Path.of(utils.getOutputDirectory()))); + ScoringConfigGroup scoring = ConfigUtils.addOrGetModule(config, ScoringConfigGroup.class); -// read personMoneyEvents.tsv and check if both fare entries do have the correct fare type - String filePath = globFile(Path.of(utils.getOutputDirectory()), "*output_personMoneyEvents.tsv*").toString(); - String line; - List events = new ArrayList<>(); + ScoringConfigGroup.ActivityParams homeParams = new ScoringConfigGroup.ActivityParams("home"); + ScoringConfigGroup.ActivityParams workParams = new ScoringConfigGroup.ActivityParams("work"); + homeParams.setTypicalDuration(8 * 3600.); + workParams.setTypicalDuration(8 * 3600.); + scoring.addActivityParams(homeParams); + scoring.addActivityParams(workParams); + return config; + } - try (BufferedReader br = IOUtils.getBufferedReader(filePath)) { -// skip header - br.readLine(); + private static class FareAnalysis implements PersonMoneyEventHandler { + private final List events = new ArrayList<>(); - while ((line = br.readLine()) != null) { - events.add(line.split(";")); - } - } catch (IOException e) { - e.printStackTrace(); + @Override + public void handleEvent(PersonMoneyEvent event) { + events.add(event); } - Assertions.assertEquals(2, events.size()); - Assertions.assertEquals(FareZoneBasedPtFareHandler.PT_FARE_ZONE_BASED, events.get(0)[4]); - Assertions.assertEquals(FareZoneBasedPtFareHandler.PT_GERMANWIDE_FARE_BASED, events.get(1)[4]); + public List getEvents() { + return events; + } } } diff --git a/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/PtFareConfigGroupTest.java b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/PtFareConfigGroupTest.java new file mode 100644 index 00000000000..ed32e9ac859 --- /dev/null +++ b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/PtFareConfigGroupTest.java @@ -0,0 +1,45 @@ +package org.matsim.contrib.vsp.pt.fare; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.matsim.core.config.Config; +import org.matsim.core.config.ConfigUtils; + +class PtFareConfigGroupTest { + @Test + void testNoFareParams_throws() { + Config config = ConfigUtils.createConfig(); + PtFareConfigGroup ptFareConfigGroup = ConfigUtils.addOrGetModule(config, PtFareConfigGroup.class); + Assertions.assertThrows(IllegalArgumentException.class, () -> ptFareConfigGroup.checkConsistency(config)); + } + + @Test + void testSamePriority_throws() { + Config config = ConfigUtils.createConfig(); + PtFareConfigGroup ptFareConfigGroup = ConfigUtils.addOrGetModule(config, PtFareConfigGroup.class); + FareZoneBasedPtFareParams fareZoneBased = new FareZoneBasedPtFareParams(); + fareZoneBased.setOrder(5); + ptFareConfigGroup.addParameterSet(fareZoneBased); + + DistanceBasedPtFareParams distanceBased = new DistanceBasedPtFareParams(); + distanceBased.setOrder(5); + ptFareConfigGroup.addParameterSet(distanceBased); + + Assertions.assertThrows(IllegalArgumentException.class, () -> ptFareConfigGroup.checkConsistency(config)); + } + + @Test + void test_ok() { + Config config = ConfigUtils.createConfig(); + PtFareConfigGroup ptFareConfigGroup = ConfigUtils.addOrGetModule(config, PtFareConfigGroup.class); + FareZoneBasedPtFareParams fareZoneBased = new FareZoneBasedPtFareParams(); + fareZoneBased.setOrder(5); + ptFareConfigGroup.addParameterSet(fareZoneBased); + + DistanceBasedPtFareParams distanceBased = new DistanceBasedPtFareParams(); + distanceBased.setOrder(10); + ptFareConfigGroup.addParameterSet(distanceBased); + + ptFareConfigGroup.checkConsistency(config); + } +} diff --git a/contribs/vsp/src/test/java/playground/vsp/pt/fare/PtTripFareEstimatorTest.java b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/PtTripFareEstimatorTest.java similarity index 79% rename from contribs/vsp/src/test/java/playground/vsp/pt/fare/PtTripFareEstimatorTest.java rename to contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/PtTripFareEstimatorTest.java index 6ddfef69d44..791931b89a2 100644 --- a/contribs/vsp/src/test/java/playground/vsp/pt/fare/PtTripFareEstimatorTest.java +++ b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/pt/fare/PtTripFareEstimatorTest.java @@ -1,4 +1,4 @@ -package playground.vsp.pt.fare; +package org.matsim.contrib.vsp.pt.fare; import com.google.inject.Inject; import com.google.inject.Injector; @@ -60,18 +60,23 @@ public void setUp() throws Exception { group = ConfigUtils.addOrGetModule(config, InformedModeChoiceConfigGroup.class); PtFareConfigGroup fare = ConfigUtils.addOrGetModule(config, PtFareConfigGroup.class); - DistanceBasedPtFareParams distanceFare = ConfigUtils.addOrGetModule(config, DistanceBasedPtFareParams.class); + DistanceBasedPtFareParams distanceFare = new DistanceBasedPtFareParams(); fare.setApplyUpperBound(true); fare.setUpperBoundFactor(1.5); distanceFare.setMinFare(0.1); - distanceFare.setNormalTripIntercept(0.5); - distanceFare.setNormalTripSlope(0.1); + DistanceBasedPtFareParams.DistanceClassLinearFareFunctionParams distanceClass20kmFareParams = + distanceFare.getOrCreateDistanceClassFareParams(20000.0); + distanceClass20kmFareParams.setFareIntercept(0.5); + distanceClass20kmFareParams.setFareSlope(0.1); - distanceFare.setLongDistanceTripThreshold(20000); - distanceFare.setLongDistanceTripIntercept(1); - distanceFare.setLongDistanceTripSlope(0.01); + DistanceBasedPtFareParams.DistanceClassLinearFareFunctionParams distanceClassLongFareParams = + distanceFare.getOrCreateDistanceClassFareParams(Double.POSITIVE_INFINITY); + distanceClassLongFareParams.setFareIntercept(1.0); + distanceClassLongFareParams.setFareSlope(0.01); + + fare.addParameterSet(distanceFare); controler = MATSimApplication.prepare(TestScenario.class, config); injector = controler.getInjector(); @@ -98,8 +103,9 @@ private List estimateAgent(Id personId) { List trip = model.getLegs(TransportMode.pt, i); - if (trip == null) + if (trip == null) { continue; + } MinMaxEstimate est = estimator.estimate(context, TransportMode.pt, model, trip, ModeAvailability.YES); @@ -116,9 +122,9 @@ void fare() { System.out.println(est); assertThat(est) - .allMatch(e -> e.getMin() < e.getMax(), "Min smaller max") - .first().extracting(MinMaxEstimate::getMin, InstanceOfAssertFactories.DOUBLE) - .isCloseTo(-379.4, Offset.offset(0.1)); + .allMatch(e -> e.getMin() < e.getMax(), "Min smaller max") + .first().extracting(MinMaxEstimate::getMin, InstanceOfAssertFactories.DOUBLE) + .isCloseTo(-379.4, Offset.offset(0.1)); } @@ -129,7 +135,7 @@ void all() { List est = estimateAgent(agent); assertThat(est) - .allMatch(e -> e.getMin() <= e.getMax(), "Min smaller max"); + .allMatch(e -> e.getMin() <= e.getMax(), "Min smaller max"); } } @@ -157,22 +163,22 @@ void planEstimate() { double estimate = estimator.estimate(context, TransportMode.pt, new String[]{"pt", "car", "pt", "pt", "pt"}, model, ModeAvailability.YES); assertThat(estimate) - .isLessThanOrEqualTo(maxSum) - .isGreaterThanOrEqualTo(minSum) - .isCloseTo(-2738.72, Offset.offset(0.1)); + .isLessThanOrEqualTo(maxSum) + .isGreaterThanOrEqualTo(minSum) + .isCloseTo(-2738.72, Offset.offset(0.1)); estimate = estimator.estimate(context, TransportMode.pt, new String[]{"pt", "car", "car", "car", "pt"}, model, ModeAvailability.YES); assertThat(estimate) - .isLessThanOrEqualTo(maxSum) - .isGreaterThanOrEqualTo(minSum) - .isCloseTo(-1222.91, Offset.offset(0.1)); + .isLessThanOrEqualTo(maxSum) + .isGreaterThanOrEqualTo(minSum) + .isCloseTo(-1222.91, Offset.offset(0.1)); // Essentially single trip estimate = estimator.estimate(context, TransportMode.pt, new String[]{"pt", "car", "car", "car", "car"}, model, ModeAvailability.YES); assertThat(estimate) - .isCloseTo(singleTrips.get(0).getMin(), Offset.offset(0.1)); + .isCloseTo(singleTrips.get(0).getMin(), Offset.offset(0.1)); } } diff --git a/contribs/vsp/src/test/java/org/matsim/contrib/vsp/scoring/RideScoringParamsFromCarParamsTest.java b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/scoring/RideScoringParamsFromCarParamsTest.java new file mode 100644 index 00000000000..2703f23443a --- /dev/null +++ b/contribs/vsp/src/test/java/org/matsim/contrib/vsp/scoring/RideScoringParamsFromCarParamsTest.java @@ -0,0 +1,34 @@ +package org.matsim.contrib.vsp.scoring; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.matsim.api.core.v01.TransportMode; +import org.matsim.core.config.Config; +import org.matsim.core.config.ConfigUtils; +import org.matsim.core.config.groups.ScoringConfigGroup; + +public class RideScoringParamsFromCarParamsTest { + + @Test + void testSetRideScoringParamsBasedOnCarParams() { + Config config = ConfigUtils.createConfig(); + ScoringConfigGroup scoringConfigGroup = config.scoring(); + + scoringConfigGroup.setPerforming_utils_hr(6.0); + ScoringConfigGroup.ModeParams carParams = scoringConfigGroup.getOrCreateModeParams(TransportMode.car); + carParams.setDailyMonetaryConstant(-10.0); + carParams.setMarginalUtilityOfDistance(-2.0); + carParams.setMarginalUtilityOfTraveling(-1.0); + carParams.setMonetaryDistanceRate(-0.3); + + double alpha = 2.0; + + RideScoringParamsFromCarParams.setRideScoringParamsBasedOnCarParams(scoringConfigGroup, alpha); + + ScoringConfigGroup.ModeParams rideParams = scoringConfigGroup.getOrCreateModeParams(TransportMode.ride); + Assertions.assertEquals(0.0, rideParams.getDailyMonetaryConstant()); + Assertions.assertEquals(-0.6, rideParams.getMonetaryDistanceRate()); + Assertions.assertEquals(-6.0, rideParams.getMarginalUtilityOfDistance()); + Assertions.assertEquals(alpha * -6.0 + (alpha + 1) * -1.0, rideParams.getMarginalUtilityOfTraveling()); + } +} diff --git a/contribs/vsp/src/test/java/playground/vsp/TestScenario.java b/contribs/vsp/src/test/java/playground/vsp/TestScenario.java index 24fde8c688a..973c9362786 100644 --- a/contribs/vsp/src/test/java/playground/vsp/TestScenario.java +++ b/contribs/vsp/src/test/java/playground/vsp/TestScenario.java @@ -14,7 +14,7 @@ import org.matsim.modechoice.estimators.DefaultLegScoreEstimator; import org.matsim.modechoice.estimators.FixedCostsEstimator; import org.matsim.testcases.MatsimTestUtils; -import playground.vsp.pt.fare.PtTripWithDistanceBasedFareEstimator; +import org.matsim.contrib.vsp.pt.fare.PtTripWithDistanceBasedFareEstimator; import javax.annotation.Nullable; import java.net.URL; diff --git a/matsim/pom.xml b/matsim/pom.xml index 3f2321ada6d..7f768fffe76 100644 --- a/matsim/pom.xml +++ b/matsim/pom.xml @@ -127,7 +127,7 @@ it.unimi.dsi fastutil-core - 8.5.13 + 8.5.14 org.geotools diff --git a/pom.xml b/pom.xml index 404796a50b1..be4f98f1127 100644 --- a/pom.xml +++ b/pom.xml @@ -31,7 +31,7 @@ 2.23.1 - 31.2 + 31.3 0.49.2 1.19.0 7.0.0 @@ -333,7 +333,7 @@ org.hamcrest hamcrest - 2.2 + 3.0 test