使用Optaplanner解决VRPTWPD

2022-09-02 11:04:44

我是optaplanner的新手,我希望用它来解决VRPTW的提货和交付(VRPTWPD)问题。

我首先从示例存储库中获取 VRPTW 代码。我正在尝试添加它以解决我的问题。但是,我无法返回符合优先级/车辆约束的解决方案(取件必须在交付之前完成,并且两者必须由同一辆车完成)。

我一直在返回一个解决方案,其中硬分数是我对这种解决方案的期望(即,我可以将所有违规行为加在一个小样本问题中,并看到硬分数与我为这些违规行为分配的惩罚相匹配)。

我尝试的第一种方法是遵循Geoffrey De Smet在这里概述的步骤 - https://stackoverflow.com/a/19087210/351400

每个都有一个变量,用于描述它是取件(PU)还是交货(DO)。它还具有一个名为的变量,该变量指示正在提取或交付的包裹。CustomercustomerTypeparcelId

我向命名的 添加了一个阴影变量。这是一个哈希集,其中包含驾驶员在访问给定的时随身携带的所有包裹Id。CustomerparcelIdsOnboardCustomer

我的不断更新看起来像这样:VariableListenerparcelIdsOnboard

public void afterEntityAdded(ScoreDirector scoreDirector, Customer customer) {
    if (customer instanceof TimeWindowedCustomer) {
        updateParcelsOnboard(scoreDirector, (TimeWindowedCustomer) customer);
    }
}

public void afterVariableChanged(ScoreDirector scoreDirector, Customer customer) {
    if (customer instanceof TimeWindowedCustomer) {
        updateParcelsOnboard(scoreDirector, (TimeWindowedCustomer) customer);
    }
}

protected void updateParcelsOnboard(ScoreDirector scoreDirector, TimeWindowedCustomer sourceCustomer) {
    Standstill previousStandstill = sourceCustomer.getPreviousStandstill();
    Set<Integer> parcelIdsOnboard = (previousStandstill instanceof TimeWindowedCustomer)
            ? new HashSet<Integer>(((TimeWindowedCustomer) previousStandstill).getParcelIdsOnboard()) : new HashSet<Integer>();

    TimeWindowedCustomer shadowCustomer = sourceCustomer;
    while (shadowCustomer != null) {
        updateParcelIdsOnboard(parcelIdsOnboard, shadowCustomer);
        scoreDirector.beforeVariableChanged(shadowCustomer, "parcelIdsOnboard");
        shadowCustomer.setParcelIdsOnboard(parcelIdsOnboard);
        scoreDirector.afterVariableChanged(shadowCustomer, "parcelIdsOnboard");
        shadowCustomer = shadowCustomer.getNextCustomer();
    }
}

private void updateParcelIdsOnboard(Set<Integer> parcelIdsOnboard, TimeWindowedCustomer customer) {
    if (customer.getCustomerType() == Customer.PICKUP) {
        parcelIdsOnboard.add(customer.getParcelId());
    } else if (customer.getCustomerType() == Customer.DELIVERY) {
        parcelIdsOnboard.remove(customer.getParcelId());
    } else {
        // TODO: throw an assertion
    }
}

然后,我添加了以口水规则:

rule "pickupBeforeDropoff"
    when
        TimeWindowedCustomer((customerType == Customer.DELIVERY) && !(parcelIdsOnboard.contains(parcelId)));
    then
        System.out.println("precedence violated");
        scoreHolder.addHardConstraintMatch(kcontext, -1000);
end

对于我的示例问题,我总共创建了6个对象(3个皮卡和3个交付)。我的车队规模是12辆车。Customer

当我运行这个时,我一直得到一个-3000的硬分,这与我看到两辆车正在使用的输出相匹配。一辆车做所有的皮卡,一辆车做所有的交付。

我使用的第二种方法是为每个对象提供对其对应对象的引用(例如,包裹1的 PICKUP 引用包裹 1 的交付,反之亦然)。CustomerCustomerCustomerCustomer

然后,我实施了以下规则,以强制宗地位于同一车辆中(注意:未完全实现优先约束)。

rule "pudoInSameVehicle"
    when
        TimeWindowedCustomer(vehicle != null && counterpartCustomer.getVehicle() != null && (vehicle != counterpartCustomer.getVehicle()));
    then
        scoreHolder.addHardConstraintMatch(kcontext, -1000);
end

对于相同的示例问题,这始终给出 -3000 分,并且与上述解决方案相同。

我尝试过在模式下运行这两个规则。使用的规则不会触发任何异常。但是,该规则会触发以下异常(未在模式下触发)。FULL_ASSERTparcelIdsOnboard"pudoInSameVehicle"FAST_ASSERT

The corrupted scoreDirector has no ConstraintMatch(s) which are in excess.
The corrupted scoreDirector has 1 ConstraintMatch(s) which are missing:

我不确定为什么这是腐败的,任何建议将不胜感激。

有趣的是,这两种方法都产生了相同的(不正确的)解决方案。我希望有人会对下一步该尝试的内容提出一些建议。谢谢!

更新:

在深入研究了在FULL_ASSERT模式下触发的断言之后,我意识到问题在于PICK和Delivery s的依赖性。也就是说,如果您采取的举动消除了交货的硬处罚,则还必须删除与取件相关的处罚 。为了保持它们同步,我更新了我和我的对象,以在两个对象上触发分数计算回调。以下是更新它后的方法,以触发刚刚移动的对应项的分数计算。CustomerCustomerCustomerVehicleUpdatingVariableListenerArrivalTimeUpdatingVariableListenerCustomerupdateVehicleCustomerCustomer

protected void updateVehicle(ScoreDirector scoreDirector, TimeWindowedCustomer sourceCustomer) {
    Standstill previousStandstill = sourceCustomer.getPreviousStandstill();
    Integer departureTime = (previousStandstill instanceof TimeWindowedCustomer)
            ? ((TimeWindowedCustomer) previousStandstill).getDepartureTime() : null;

    TimeWindowedCustomer shadowCustomer = sourceCustomer;
    Integer arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
    while (shadowCustomer != null && ObjectUtils.notEqual(shadowCustomer.getArrivalTime(), arrivalTime)) {
        scoreDirector.beforeVariableChanged(shadowCustomer, "arrivalTime");
        scoreDirector.beforeVariableChanged(((TimeWindowedCustomer) shadowCustomer).getCounterpartCustomer(), "arrivalTime");
        shadowCustomer.setArrivalTime(arrivalTime);
        scoreDirector.afterVariableChanged(shadowCustomer, "arrivalTime");
        scoreDirector.afterVariableChanged(((TimeWindowedCustomer) shadowCustomer).getCounterpartCustomer(), "arrivalTime");
        departureTime = shadowCustomer.getDepartureTime();
        shadowCustomer = shadowCustomer.getNextCustomer();
        arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
    }
}

这解决了我在第二种方法中遇到的分数损坏问题,并且在一个小样本问题上,产生了一个满足所有硬约束的解决方案(即解决方案的硬分数为0)。

接下来,我试图运行一个更大的问题(约380个客户),但解决方案返回的硬分数非常差。我尝试搜索解决方案1分钟,5分钟和15分钟。分数似乎随着运行时的线性提高而线性提高。但是,在15分钟时,解决方案仍然非常糟糕,似乎需要运行至少一个小时才能产生可行的解决方案。我需要这个最多在5-10分钟内运行。

我了解了过滤器选择。我的理解是,您可以运行一个函数来检查您将要进行的移动是否会导致破坏内置的硬约束,如果会,则跳过此移动。

这意味着您不必重新运行分数计算或探索您知道不会有成效的分支。例如,在我的问题中,我不希望您能够将a移动到a,除非其对应物被分配给该车辆或根本没有分配车辆。CustomerVehicle

这是我实现的过滤器来检查这一点。它只适用于ChangeMoves,但我怀疑我需要它来为SwapMoves实现类似的功能。

public class PrecedenceFilterChangeMove implements SelectionFilter<ChangeMove> { 

    @Override
    public boolean accept(ScoreDirector scoreDirector, ChangeMove selection) {
        TimeWindowedCustomer customer = (TimeWindowedCustomer)selection.getEntity();
        if (customer.getCustomerType() == Customer.DELIVERY) {
            if (customer.getCounterpartCustomer().getVehicle() == null) {
                return true;
            }
            return customer.getVehicle() == customer.getCounterpartCustomer().getVehicle();
        }
        return true;
    }
}

添加此过滤器立即导致得分更差。这让我认为我错误地实现了该函数,尽管我不清楚为什么它是不正确的。

更新 2:

一位同事指出了我的PresidenceFilterChangeMove的问题。正确的版本如下。我还包含了 PrecedenceFilterSwapMove 实现。总之,这些使我能够找到解决问题的方法,即在大约10分钟内没有违反硬约束。我认为我还可以进行其他一些优化,以进一步减少这种情况。

如果这些变化是富有成效的,我将发布另一个更新。我仍然很想听听optaplanner社区中的某个人关于我的方法,以及他们是否认为有更好的方法来模拟这个问题!

优先级过滤器更改移动

@Override
public boolean accept(ScoreDirector scoreDirector, ChangeMove selection) {
    TimeWindowedCustomer customer = (TimeWindowedCustomer)selection.getEntity();
    if (customer.getCustomerType() == Customer.DELIVERY) {
        if (customer.getCounterpartCustomer().getVehicle() == null) {
            return true;
        }
        return selection.getToPlanningValue() == customer.getCounterpartCustomer().getVehicle();
    } 
    return true;
}

优先级过滤器交换移动

@Override
public boolean accept(ScoreDirector scoreDirector, SwapMove selection) {
    TimeWindowedCustomer leftCustomer = (TimeWindowedCustomer)selection.getLeftEntity();
    TimeWindowedCustomer rightCustomer = (TimeWindowedCustomer)selection.getRightEntity();
    if (rightCustomer.getCustomerType() == Customer.DELIVERY || leftCustomer.getCustomerType() == Customer.DELIVERY) {      
        return rightCustomer.getVehicle() == leftCustomer.getCounterpartCustomer().getVehicle() ||
                leftCustomer.getVehicle() == rightCustomer.getCounterpartCustomer().getVehicle();
    }
    return true;
}

答案 1

这里有混合的提货和交付VRP实验代码,这是有效的。我们还没有一个完善的开箱即用的例子,但我们正处于长期路线图上。


答案 2

推荐