ChainedSwapMove 期望链接额外的规划变量(当它来自 CountableValueRange<Long> 时)

ChainedSwapMove expects extra planning variable to be chained (when it comes from a CountableValueRange<Long>)

我正在使用 Optaplanner 7.38。0.Final

我实现了一个相对简单的模型,但我 运行 遇到了一些问题。

我有一个名为 'Visit' 的规划实体,其规划变量 previousStandstill 遵循与车辆路线示例几乎相同的模式,锚变量是 'Motorcycle' class 所以基本上优化问题是为给定的 Motorcycle-Employee 找到访问链(在第一次创建解决方案时固定),它最小化所有员工的所有路线的时间并为所有访问提供服务。

问题是我想不时打破链条返回仓库,虽然没有必要创建另一个 class 来打破这个链条,但我包含了一个计划变量需要重新启动路由时的纪元秒时间戳(时间戳在下面代码中相对较小的 运行ge 和 startOfTrip 之间)。

Optaplanner 能够使用给定的 xml 配置创建求解器,如果我在分数计算器上创建一个断点,我可以检查变量并看到构造启发式能够创建有效的访问链甚至设置时间戳变量的值。

但不知何故,几毫秒后,当在第 43 行调用 org.optaplanner.core.impl.heuristic.selector.move.generic.chained.ChainedSwapMove 构造函数时,求解器命中 NullPointerException(在上面给定版本的原始源代码上)

    public ChainedSwapMove(List<GenuineVariableDescriptor<Solution_>> variableDescriptorList,
            List<SingletonInverseVariableSupply> inverseVariableSupplyList, Object leftEntity, Object rightEntity) {
        super(variableDescriptorList, leftEntity, rightEntity);
        oldLeftTrailingEntityList = new ArrayList<>(inverseVariableSupplyList.size());
        oldRightTrailingEntityList = new ArrayList<>(inverseVariableSupplyList.size());
        for (SingletonInverseVariableSupply inverseVariableSupply : inverseVariableSupplyList) {
            oldLeftTrailingEntityList.add(inverseVariableSupply.getInverseSingleton(leftEntity));
            oldRightTrailingEntityList.add(inverseVariableSupply.getInverseSingleton(rightEntity));
        }
    }

似乎变量 inverseVariableSupplyList 包含一个空引用(它在分析包含常规非链接 PlanningVariable 的 variableDescriptorList 时创建此空引用)

package X;

import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.api.domain.variable.AnchorShadowVariable;
import org.optaplanner.core.api.domain.variable.InverseRelationShadowVariable;
import org.optaplanner.core.api.domain.variable.PlanningVariable;
import org.optaplanner.core.api.domain.variable.PlanningVariableGraphType;

import javax.persistence.Transient;
import java.io.Serializable;
import java.time.LocalDateTime;

@PlanningEntity
public class OptimizingVisit implements OptimizingStandstill , Serializable {
    private static final long serialVersionUID = 9163651541108883957L;
    private ContinuousBranchTripSolution solution;
    private Order order;

    private Long startOfTrip;
    private Long start;
    private Long arrivalTime;
    private Long end;
    private Long travelDuration;
    private Long travelDistance;

    private OptimizingStandstill previousStandstill;

    private OptimizingVisit nextStandstill; //shadow variable
    private OptimizingDriver optimizingDriver;

    public OptimizingVisit() {
    }

    public OptimizingVisit(Order order, ContinuousBranchTripSolution solution) {
        this.order = order;
        this.solution = solution;
    }

    public Order getOrder() {
        return order;
    }

    public void setOrder(Order order) {
        this.order = order;
    }

    @AnchorShadowVariable(sourceVariableName = "previousStandstill")
    public OptimizingDriver getOptimizingDriver() {
        return optimizingDriver;
    }

    public void setOptimizingDriver(OptimizingDriver optimizingDriver) {
        this.optimizingDriver = optimizingDriver;
    }

    public Employee getDriver(){
        return this.getOptimizingDriver().getDriver();
    }

    @PlanningVariable( valueRangeProviderRefs = "startTimeCandidates" )
    public Long getStartOfTrip() {
        return startOfTrip;
    }

    public void setStartOfTrip(Long startOfTrip) {
        this.startOfTrip = startOfTrip;
    }

    public Long getTravelDuration() {
        return travelDuration;
    }

    public void setTravelDuration(Long travelDuration) {
        this.travelDuration = travelDuration;
    }

    public Long getTravelDistance() {
        return travelDistance;
    }

    public void setTravelDistance(Long travelDistance) {
        this.travelDistance = travelDistance;
    }

    @PlanningVariable( graphType = PlanningVariableGraphType.CHAINED , valueRangeProviderRefs = { "visitsRange" , "driversRange" } )
    public OptimizingStandstill getPreviousStandstill() {
        return previousStandstill;
    }

    public void setPreviousStandstill(OptimizingStandstill previousStandstill) {
        this.previousStandstill = previousStandstill;
    }

    @Override
    public OptimizingVisit getNextStandstill() {
        return nextStandstill;
    }

    @Override
    public void setNextStandstill(OptimizingVisit nextStandstill) {
        this.nextStandstill = nextStandstill;
    }

    @Override
    public Hexagon getHexagon() {
        return this.getOrder().getShippingAddress().getHexagon();
    }

    public TimeRange getTimeRange() {
        return new TimeRange( this.start , this.end );
    }

    /*Helper Methods*/
    public long getRecursiveStart(){
        if( this.getStartOfTrip() != null ) return this.getStartOfTrip() + 5*60;
        if( this.start != null ) return this.start;
        this.start = this.getPreviousStandstill().getRecursiveEnd();
        return this.start;
    }

    public long getRecursiveArrivalTime(){
        if( this.arrivalTime != null ) return this.arrivalTime;
        this.arrivalTime = this.getRecursiveStart() + solution.getDistanceBetweenHexagons( this.getPreviousStandstill().getHexagon() , this.getHexagon() ).getDuration();
        return this.arrivalTime;
    }

    @Override
    public long getRecursiveEnd(){
        if( this.end != null ) return this.end;
        this.end = this.getRecursiveArrivalTime() + TripsOptimizer.standByDuration;
        return this.end;
    }

    public boolean isEndOfTrip(){
        return this.getNextStandstill() == null || ( ( OptimizingVisit ) this.getNextStandstill()).getStartOfTrip() != null;
    }

    public long endOfTrip(){
        return this.getRecursiveEnd() + TripsOptimizer.standByDuration + solution.getDistanceBetweenHexagons( this.getHexagon() , this.getOptimizingDriver().getHexagon() ).getDuration();
    }

    @Override
    public void cleanTimes() {
        this.start = null;
        this.arrivalTime = null;
        this.end = null;
    }

    public long overlapWith( OptimizingVisit optimizingVisit ){
        if( this.getRecursiveStart() > optimizingVisit.getRecursiveEnd() ) return 0;
        if( this.getRecursiveEnd() < optimizingVisit.getRecursiveStart() ) return 0;

        OptimizingVisit firstEvent;
        OptimizingVisit lastEvent;
        if( this.getRecursiveStart() < optimizingVisit.getRecursiveStart() ){
            firstEvent = this;
            lastEvent = optimizingVisit;
        }else{
            firstEvent = optimizingVisit;
            lastEvent = this;
        }

        if( lastEvent.getRecursiveEnd() < firstEvent.getRecursiveEnd() ) return lastEvent.getRecursiveEnd() - lastEvent.getRecursiveStart();

        return  firstEvent.getRecursiveEnd() - lastEvent.getRecursiveStart();
    }

    public long getTimePenalization(){
        if( this.order == null ) return 0;

        long estimatedArrivalTime = this.getRecursiveArrivalTime();

        TimeRange orderTimeRange = this.getOrder().getTimeRange();

        if( estimatedArrivalTime > orderTimeRange.getEnd() ){
            double secondsOfDifference = estimatedArrivalTime - orderTimeRange.getEnd();

            return (long) Math.pow( secondsOfDifference , this.getOrder().isExpress() ? 2 : 1.5 );
        }

        if( estimatedArrivalTime > orderTimeRange.getStart() ) return 0;

        return (long) Math.pow( orderTimeRange.getStart() - estimatedArrivalTime , 2 );
    }

    @Transient
    public double getCarryOnCash() {
        if( this.order == null ) return 0;
        double r = 0;
        if( this.order.isOnAdvanceMode() ){
            for ( TransactionMatrix tm : this.order.getTransactionMatrix() ) {
                if( !PaymentMethodType.CASH.equals( tm.getPaymentMethodType() ) ) continue;
                r += tm.getAdvance();
            }
        }else{
            for ( TransactionMatrix tm : this.order.getTransactionMatrix() ) {
                if( !PaymentMethodType.CASH.equals( tm.getPaymentMethodType() ) ) continue;
                r += tm.getAmount();
            }
        }

        return r;
    }

    public long getEarlyOrLateSeconds(){
        TimeRange orderTimeRange = this.getOrder().getTimeRange();
        long arrivalTime = this.getRecursiveArrivalTime();
        long r = 0;
        if( arrivalTime < orderTimeRange.getStart() ) r += orderTimeRange.getStart() - arrivalTime;
        if( arrivalTime > orderTimeRange.getEnd() ) r += arrivalTime - orderTimeRange.getEnd();
        return r;
    }

    public long getContinuousOptimizationScore( ContinuousBranchTripSolution solution ) {
        return 0;
        /*if( !( this.getPreviousStandstill() instanceof OptimizingTrip ) ){
            return this.getTimePenalization();
        }
        double r = 0;
        OptimizingTrip trip = (OptimizingTrip) this.getPreviousStandstill();

        for ( DriverShift shift : solution.getDriverShifts() ){
            if( this.getOptimizingDriver().getDriver().computedIdHashCode() != shift.getDriver().computedIdHashCode() ) continue;
            long seconds = Math.max( 0 , trip.getEnd() - shift.getAvailableUntilAsEpochSeconds() );
            r += Math.pow( seconds * 2 , 2 );
        }

        r += 0.25d * Math.max( this.getCarryOnCash() - this.getOptimizingDriver().getDriver().getTrustLevel() , 0 );

        if ( trip.getStart() > solution.getStart() ) {
            r -= 0.5d * ( trip.getEnd() - solution.getStart() );
        }

        r += this.getTimePenalization();

        return (long) r;*/
    }

    @Override
    public String toString() {
        return String.format("OptimizingVisit{  %s  ,  %s  ,  %s  ,  %s , %s min early , %s min late  }",
                this.order.getNumber(),
                this.getOrder().getLowerBoundDelivery(),
                this.getPreviousStandstill() == null ? "" : LocalDateTime.ofEpochSecond( this.getRecursiveArrivalTime() , 0 , BitemporalModel.ZONE_OFFSET ),
                this.getOrder().getUpperBoundDelivery(),
                this.getPreviousStandstill() == null ? "" : Math.max( 0 , ( this.getOrder().getTimeRange().getStart() - this.getRecursiveArrivalTime() ) ) / 60,
                this.getPreviousStandstill() == null ? "" : Math.max( 0 , ( this.getRecursiveArrivalTime() - this.getOrder().getTimeRange().getEnd() ) ) / 60
        );
    }
}

我们最近修复了 PLANNER-1961,其症状与您此处的问题非常相似。请查看 OptaPlanner 7.39。0.Final(当它出来时)或更高版本,您的问题很可能会消失。