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

时间:2020-10-05 22:59:22

标签: optaplanner

Hello OptaPlanner社区。 我正在开发Rest API,以计划车队的路线。在寻找一种对我有帮助的工具时,我找到了Optaplanner,并且我发现它有多个成功案例。在第一阶段中,我考虑了最快的距离和车辆的载客量来制定计划。我得到了预期的结果。现在,我正在计划访问和存款的时间窗口,但是我还没有成功。

要求

R1-我有一支车队。每个车辆都具有容量及其存放处,并且该存放处具有时间范围。从用于VRP的OptaPlanner的示例中,我仅对我作为浮动对象处理的容量进行了更改。据我了解,OptaPlanner示例中的所有车辆都移动到一个仓库。在我的情况下,每辆车都有自己的仓库,每辆车都有自己的固定仓库,并且可能有几辆车有相同的仓库。

R2-我有探视服务(送货服务)。每次访问都有需求和时间窗口。从用于VRP的OptaPlanner的示例中,我仅对我将其处理为“浮动”类型的需求进行了一次修改。

在将带有TW的变体添加到我的路由问题的过程中,我有一些疑问和问题,因为我没有通过应用TW来获得解决问题的可行方法:

1-我了解我不需要修改OptaPlanner示例,因此每辆车运输的物品不能超过其容量。我只需要调整约束提供者,以使计算处于浮动状态。我想知道我是否正确吗?另一方面,如何管理尺寸的容量和需求?在OptaPlanner中,它是一个数字,但我需要按体积和重量来管理它。

在OptaPlanner域中,我将车辆的变量“ capacity”和访问的变量“ demand”修改为“ float”。

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-在OptaPlanner示例中,我了解到TW是一个乘以一千的长整数,但是我不知道该长整数是否表示小时或日期,还是仅将小时换算成分钟并乘以一千。 我正在做的是将TW转换为分钟数并乘以一千,例如,如果是上午8点,则准备时间为对数,等于“ 480000”。 对于服务持续时间,我不会将其乘以1000,而是始终将其视为10分钟。我转换正确吗? ,这是OptaPlanner期望的时间长吗?

3-我知道使用OptaPlanner的时间窗口示例可以解决此要求(R2),而无需进行任何更改,但是由于某种原因,我无法找到并没有给我带来可行的解决方案。例如,它返回了我:花费的时间(5000),最佳成绩(-3313987hard / -4156887soft)。

我曾经认为错误可能是时间窗口日期的转换,或者我可能缺少一些硬约束,因为访问的到达时间不适合为访问或存款定义的时间窗口

例如: 我有4个带时间窗的访问,早上2次(访问2,访问4),下午2次(访问1,访问3)。 我有两辆车,车辆1离开仓库1,该仓库在早晨的时间表中有一个时间窗口,而另一辆汽车离开仓库2,其在下午的时间表中有一个时间窗口。 因此,我希望车辆1进行早上有时间窗口的访问,车辆2进行下午有时间窗口的访问:[车辆1:{访问2,访问4},车辆2:{访问1,访问3}]

我一定做错了什么,但我找不到哪里,不仅不符合押金的TW,而且每次造访的到达时间都超出了定义的TW。我不明白为什么我会有这么大的到达时间,甚至超过了规定的限制1天(所有到达时间都超过1440000 = 1400min = 24 = 12am),也就是说,他们是在这个时间之后到达的。

这是我获得的结果:得分(-3313987hard / -4156887soft)

路线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

路线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

这是我的代码,它可以为您提供更好的上下文。 这是我的VariableListerner,用于更新阴影变量“到达时间”。我没有做任何修改,但是每次访问返回给我的到达时间都不符合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();
     }
}

此服务是我从数据库中存储的数据中构建域实体的地方(查找)。我在求解器中使用了这个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;
}

然后我创建求解器,开始优化并最终保存最佳结果:

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");
}

欢迎提供有关错误所在位置的任何帮助。我是从Optaplanner开始的,请发表任何意见。非常感谢!

对不起书法,英语不是我的语言。

1 个答案:

答案 0 :(得分:0)

非常感谢Geoffrey,我运用了您的建议并找到了问题的根源。感谢您的帮助!

我将对发生的事情发表评论,以防它对某人有用: 碰巧我使用的是OptaWeb示例的距离计算,该示例为此目的使用了GrahHopper,默认情况下它返回最小距离,因此计算需要时间。通过引入时间窗口,我打破了分数:

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

我的得分很低,因为我没有对所有变量都使用相同的转换,TW:准备时间和出发时间以分钟表示,乘以1000,而距离以毫秒为单位。

示例:

  • 就绪时间:480000(8:00 * 60 * 1000)
  • 到期时间:780000(13:00 * 60 * 1000)

距离回到我身边

  • 距离:641263

因此我的分数被打破了。

我所做的是将所有时间变量都转换为毫秒:

“ HH:MM”,HH * 3,600,000和MM * 60,000

示例:

  • 就绪时间:2880万
  • 到期时间:4680万
  • 服务时间:60000(每次访问10分钟)

现在准备好了!您访问的每辆车的到达时间均已调整为定义的时间窗口。