简体   繁体   English

使用 OptaPlanner 解决带时间窗的车辆路线问题

[英]Using OptaPlanner to solve a Vehicle Routing Problem with Time Windows

Hello OptaPlanner community.您好 OptaPlanner 社区。 I am developing a Rest API to plan the routes of a fleet of vehicles.我正在开发一个 Rest API 来规划车队的路线。 Looking for a tool that would help me I found Optaplanner, and I have seen that it has several success stories.在寻找对我有帮助的工具时,我找到了 Optaplanner,并且我看到它有几个成功案例。 In a first stage I made a planning taking into account the fastest distance and the capacity of the vehicle to cover all its visits.在第一阶段,我制定了一个计划,考虑到最快的距离和车辆覆盖所有访问的能力。 And I got the results I expected.我得到了我预期的结果。 Now I'm planning for time windows of visits and deposits, but I'm not successful yet.现在我正在计划访问和存款的时间窗口,但我还没有成功。

Requirements要求

R1- I have a fleet of vehicles. R1-我有车队。 Each vehicle has a capacity and its deposit and this deposit has a window of time.每辆车都有容量和押金,押金有一个时间窗口。 From the example of OptaPlanner for VRP I have only made a variation on the capacity that I handle as a float.从用于 VRP 的 OptaPlanner 示例中,我仅对作为浮动处理的容量进行了更改。 As I understand it, all the vehicles in the OptaPlanner example are moved for a single depot.据我了解,OptaPlanner 示例中的所有车辆都被移动到一个仓库。 In my case, each vehicle has its own depot, each vehicle has its own fixed depot, and it is possible that several vehicles have the same depot.就我而言,每辆车都有自己的车位,每辆车都有自己的固定车位,并且可能有几辆车有同一个车位。

R2- I have the visits (delivery services). R2- 我有访问(送货服务)。 Each visit has a demand and a window of time.每次访问都有一个需求和一个时间窗口。 From the example of OptaPlanner for VRP I have only made one modification regarding the demand that I handle it as a type "float".从用于 VRP 的 OptaPlanner 示例中,我仅对将其作为“浮动”类型处理的需求进行了一项修改。

In this process of adding this variant with TW to my routing problem I have some doubts and problems since I have not obtained a viable solution to my problem by applying TW:在将这个带有 TW 的变体添加到我的路由问题的过程中,我有一些疑问和问题,因为我还没有通过应用 TW 获得可行的解决方案:

1- I understand that I do not need to make modifications to the OptaPlanner example so that each vehicle cannot transport more items than its capacity. 1- 我知道我不需要对 OptaPlanner 示例进行修改,以便每辆车不能运输超过其容量的物品。 I only need to adjust the constrint provider so that the calculation is on float.我只需要调整约束提供程序,以便计算是浮动的。 I would like to know if I am right ?我想知道我是否正确? and on the other hand How I can manage the capacities and demands with dimensions?, in OptaPlanner it is a number but I need to manage it as volume and weight.另一方面,我如何通过尺寸管理容量和需求?,在 OptaPlanner 中,它是一个数字,但我需要将其作为体积和重量进行管理。

In the OptaPlanner domain I modified the variables "capacity" from vehicle and "demand" from visit, both to type "float".在 OptaPlanner 域中,我修改了来自车辆的变量“容量”和来自访问的“需求”,两者都输入了“浮动”。

Constraint vehicleCapacity(ConstraintFactory constraintFactory) {
    return constraintFactory.from(PlanningVisit.class)
            .groupBy(PlanningVisit::getVehicle, sumBigDecimal(PlanningVisit::getDemand))
            .filter((vehicle, demand) -> demand.compareTo(vehicle.getCapacity()) > 0)
            .penalizeLong(
                    "vehicle capacity",
                    HardSoftLongScore.ONE_HARD,
                    (vehicle, demand) -> demand.subtract(vehicle.getCapacity()).longValue());
}

2- In the OptaPlanner example I understand that the TW is a long that multiplies by a thousand, but I do not know if this long expresses an hour or date, or if it is just the hour converted into minutes and multiplied by a thousand. 2- 在 OptaPlanner 示例中,我知道 TW 是乘以一千的长,但我不知道这个长是表示小时还是日期,还是只是将小时转换为分钟并乘以一千。 What I am doing is converting the TW to minutes and multiplied by a thousand, for example if it is 8am, the ready time is a log equal to '480000'.我正在做的是将 TW 转换为分钟并乘以一千,例如,如果是上午 8 点,则就绪时间是等于“480000”的日志。 In the case of the service duration, I do not multiply it by 1000, I always treat it as 10 minutes.在服务时长的情况下,我没有乘以1000,我总是把它当作10分钟。 Am I doing the conversion correctly?我是否正确进行了转换? , is this the long that OptaPlanner expects? ,这是OptaPlanner期望的长度吗?

3- I understand that using the example of OptaPlanner for time windows I can solve this requirement (R2), without making variations, however for some reason that I can not find is not giving me back a feasible solution. 3- 我知道使用 OptaPlanner 的时间窗口示例我可以解决这个要求 (R2),而无需进行任何更改,但是由于某种原因,我找不到并没有给我一个可行的解决方案。 It returns me for example: time spent (5000), best score (-3313987hard/-4156887soft).它返回我例如:花费的时间(5000),最好的分数(-3313987hard/-4156887soft)。

I have thought that the error could be in the conversion of the dates of the time window or maybe some hard constraint that I lack, because the arrival times of the visits do not adapt to the time windows defined for visits nor for deposits.我认为错误可能在于时间窗口日期的转换,或者可能是我缺乏的一些硬约束,因为访问的到达时间不适应为访问或存款定义的时间窗口。

For example: I have 4 visits with time windows, 2 in the morning (visit 2 and visit 4) and 2 in the afternoon (visit 1 and visit 3).例如:我有 4 次时间窗口访问,上午 2 次(访问 2 和访问 4),下午 2 次(访问 1 和访问 3)。 I have two vehicles, vehicle 1 leaves a depot 1 that has a time window in a morning schedule and the other vehicle leaves a depot 2 that has a time window in an afternoon schedule.我有两辆车,一辆车离开了一个有早上时间表的时间窗口的仓库 1,另一辆车离开了一个有下午时间表的时间窗口的仓库 2。 So I expect vehicle 1 to conduct the visits that have a time window in the morning and vehicle 2 to conduct the visits that have a time window in the afternoon: [vehicle 1: {visit 2, visit 4}, vehicle 2: {visit 1, visit 3}]所以我期望车辆1进行上午有时间窗口的访问,车辆2进行下午有时间窗口的访问:[车辆1:{访问2,访问4},车辆2:{访问1、访问3}]

I must be doing something very wrong, but I can't find where, not only does it not meet the TW of the deposit, but the arrival times of each visit exceed the defined TW.一定是我做错了什么,但是我找不到在哪里,不仅不符合存款的TW,而且每次访问的到达时间都超过了定义的TW。 I don't understand why I get such big arrival times, that even exceed the defined limit for 1 day (all arrival times are over 1440000 = 1400min = 24 = 12am), that is, they arrived after this time.我不明白为什么我得到这么大的到达时间,甚至超过了 1 天的定义限制(所有到达时间都超过 1440000 = 1400 分钟 = 24 = 12 点),也就是说,他们在这个时间之后到达。

This is the result I have obtained: score (-3313987hard/-4156887soft)这是我得到的结果:分数(-3313987hard/-4156887soft)

Route 1 referring to the route followed by vehicle 1 Vehicle 1路线 1 指车辆 1 所遵循的路线 车辆 1

Depot 1 with TW (8:00 a 13:00)
    ready_time: 480000
    due_time: 780000


Visit 2 (8:30 a 12:30)
    ready_time: 510000
    due_time: 750000
    service_duraration 10 = 10

    arrival_time: 1823943
    departure_time: 1833943

Visit 4 (9:30 a 12:30)
    ready_time: 570000
    due_time: 750000
    service_duraration 10

    arrival_time: 1739284
    departure_time: 1739294

Visit 3 (14:40 a 15:30)
    ready_time: 880000
    due_time: 930000
    service_duraration 10

    arrival_time: 1150398
    departure_time: 1150408

Route 2 referring to the route followed by vehicle 2 Vehicle 2路线 2 指车辆 2 所遵循的路线 车辆 2

Depot 2 with TW (12:00 a 17:00)
    ready_time: 720000
    due_time: 1020000

Visit 1 (14:00 a 16:30)
    ready_time: 840000
    due_time: 990000
    service_duraration 10 = 10

    arrival_time: 2523243
    departure_time: 2523253

This is my code, it can give you a better context.这是我的代码,它可以为您提供更好的上下文。 This is my VariableListerner for updating the shadow variable 'arrival time'.这是我的 VariableListerner,用于更新影子变量“到达时间”。 I have not made any modifications, however the arrival times returned to me for each visit do not comply with the TW.我没有做任何修改,但是每次访问返回给我的到达时间不符合 TW。

public class ArrivalTimeUpdatingVariableListener implements VariableListener<PlanningVisit> {
    ...

    protected void updateArrivalTime(ScoreDirector scoreDirector, TimeWindowedVisit sourceCustomer) {

       Standstill previousStandstill = sourceCustomer.getPreviousStandstill();
       Long departureTime = previousStandstill == null ? null
               : (previousStandstill instanceof TimeWindowedVisit)
               ? ((TimeWindowedVisit) previousStandstill).getDepartureTime()
               : ((TimeWindowedDepot) ((PlanningVehicle) 
                                 previousStandstill).getDepot()).getReadyTime();
       TimeWindowedVisit shadowCustomer = sourceCustomer;
       Long arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
       while (shadowCustomer != null && !Objects.equals(shadowCustomer.getArrivalTime(), 
           arrivalTime)) {
               scoreDirector.beforeVariableChanged(shadowCustomer, "arrivalTime");
               shadowCustomer.setArrivalTime(arrivalTime);
               scoreDirector.afterVariableChanged(shadowCustomer, "arrivalTime");
               departureTime = shadowCustomer.getDepartureTime();
               shadowCustomer = shadowCustomer.getNextVisit();
               arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
             }        
        }

    private Long calculateArrivalTime(TimeWindowedVisit customer, Long previousDepartureTime) {
       if (customer == null || customer.getPreviousStandstill() == null) {
              return null;
       }
       if (customer.getPreviousStandstill() instanceof PlanningVehicle) {
           // PreviousStandstill is the Vehicle, so we leave from the Depot at the best suitable time
           return Math.max(customer.getReadyTime(),
                     previousDepartureTime + customer.distanceFromPreviousStandstill());
       }
       return previousDepartureTime + customer.distanceFromPreviousStandstill();
     }
}

And this service is where I build the domain entities from the data stored in the database (find).这个服务是我从存储在数据库中的数据构建域实体的地方(查找)。 This TimeWindowedVehicleRoutingSolution is the one I use in the solver.这个 TimeWindowedVehicleRoutingSolution 是我在求解器中使用的。

   public TimeWindowedVehicleRoutingSolution find(UUID jobId) {
        //load VRP from DB
        RoutingProblem byJobId = routingProblemRepository.findVRP(jobId);
        Set<Vehicle> vehicles = byJobId.getVehicles();
        Set<Visit> visits = byJobId.getVisits();

        //building solution
        List<PlanningDepot> planningDepots = new ArrayList<>();
        List<PlanningVehicle> planningVehicles = new ArrayList<>();
        List<PlanningVisit> planningVisits = new ArrayList<>();


        vehicles.forEach(vehicle -> {
            //submit to planner location of the deposit, add to matrix for calculating distance
            PlanningLocation planningLocation = 
                        optimizer.submitToPlanner(vehicle.getDepot().getLocation());

            //Depot, Vehicle and Visit are my persistence JPA entities, they are not the OptaPlanner 
             domain entities.
            //The OptaPlanner domain entities are: PlanningVehicle, PlanningDepot and PlanningVisit
            //I build the entities of the optaplanner domain from my persistence entities

            Depot depot = vehicle.getDepot();
            TimeWindowedDepot timeWindowedDepot = new TimeWindowedDepot();
            TimeWindowedDepot timeWindowedDepot = new TimeWindowedDepot(depot.getId(), 
                     planningLocation, depot.getStart(), depot.getEnd());

        PlanningVehicle planningVehicle = new PlanningVehicle();
        planningVehicle.setId(vehicle.getId());
        planningVehicle.setCapacity(vehicle.getCapacity());
        // each vehicle has its deposit
        planningVehicle.setDepot(timeWindowedDepot);

        planningVehicles.add(planningVehicle);
       });

       visits.forEach(visit -> {
           //submit to planner location of the visit, add to matrix for calculating distance
            PlanningLocation planningLocation = optimizer.submitToPlanner(visit.getLocation());

            TimeWindowedVisit timeWindowedVisit = new TimeWindowedVisit();
            TimeWindowedVisit timeWindowedVisit = new TimeWindowedVisit(visit.getId(),     
                  planningLocation, visit.getLoad(),visit.getStart(), visit.getEnd(), 
                  visit.getServiceDuration());

            planningVisits.add(timeWindowedVisit);
      });

    //create TWVRP
    TimeWindowedVehicleRoutingSolution solution = new TimeWindowedVehicleRoutingSolution();
    solution.setId(jobId);
    solution.setDepotList(planningDepots);
    solution.setVisitList(planningVisits);
    solution.setVehicleList(planningVehicles);

    return solution;
}

Then I create the solver, start the optimization and finally save the best:然后我创建求解器,开始优化,最后保存最好的:

public void solve(UUID jobId) {
    if (!planRepository.isResolved(jobId)) {
        logger.info("Starting solver");

        TimeWindowedVehicleRoutingSolution solution = null;
        TimeWindowedVehicleRoutingSolution timeWindowedVehicleRoutingSolution = find(jobId);
        
        try {
            SolverJob<TimeWindowedVehicleRoutingSolution, UUID> solverJob = 
                           solverManager.solve(jobId, timeWindowedVehicleRoutingSolution);

            solution = solverJob.getFinalBestSolution();
            save(jobId, solution);

        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    } else
        logger.info("This job already has a solution");
}

Any help on where the error is will be welcome.欢迎任何有关错误位置的帮助。 I am starting with Optaplanner, please any comments will be very helpful.我从 Optaplanner 开始,任何评论都会非常有帮助。 Thank you very much!非常感谢!

Sorry about the calligraphy, English is not my language.对不起书法,英语不是我的语言。

Thank you very much Geoffrey, I applied your suggestions and found the source of my problem.非常感谢 Geoffrey,我采纳了您的建议并找到了问题的根源。 Grateful for your help!感谢您的帮助!

I will comment on what happened, in case it is useful to someone: It happens that I was using for the calculation of the distance the example of OptaWeb, that uses GrahHopper for this end and by default it returns the minimum distance, reason why calculation is as far as time.我将评论发生的事情,以防它对某人有用:碰巧我正在使用 OptaWeb 的示例来计算距离,它为此使用 GrahHopper,默认情况下它返回最小距离,计算的原因就时间而言。 And by introducing time windows, I was breaking the score in:通过引入时间窗口,我打破了分数:

Math.max(customer.getReadyTime(),
previousDepartureTime + customer.distanceFromPreviousStandstill())

My score was broken because I did not use the same conversion for all variables, the TW: ready time and departure time was expressed in minutes and multiplied by a thousand, while the distance was in milliseconds.我的分数被打破了,因为我没有对所有变量使用相同的转换,TW:准备时间和出发时间以分钟表示并乘以一千,而距离以毫秒为单位。

Example:例子:

  • ready_time: 480000 (8:00 * 60 * 1000)准备时间:480000(8:00 * 60 * 1000)
  • due_time: 780000 (13:00 * 60 * 1000)到期时间:780000(13:00 * 60 * 1000)

As the distance returned to me:随着距离回到我身边:

  • distance: 641263距离:641263

And therefore my score was broken.因此我的分数被打破了。

What I did was to convert all my time variables to milliseconds:我所做的是将所有时间变量转换为毫秒:

"HH:MM", HH * 3 600 000 and MM * 60 000 "HH:MM", HH * 3 600 000 和 MM * 60 000

Example:例子:

  • ready_time: 28 800 000准备时间:28 800 000
  • due_time: 46 800 000到期时间:46 800 000
  • service_duration: 60 000 (10min per visit) service_duration:60 000(每次访问 10 分钟)

Now ready!现在准备好了! The arrival time of each vehicle to your visits is adjusted to the defined time windows.每辆车到达您访问的时间都会根据定义的时间窗口进行调整。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM