Search code examples
javaoptaplanner

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


I am using Optaplanner 7.38.0.Final

I have implemented a relatively simple model and I ran into some issues.

I have a planning entity called 'Visit' with a planning variable previousStandstill that follows pretty much the same pattern of the vehicle routing example, the anchor variable is 'Motorcycle' class so basically the optimization problem is to find the chain of visits for a given Motorcycle-Employee (fixed when solution is created the first time) that minimizes the time of all the routes for all the employees and serve all the visits.

The issue is that I want to break the chain from time to time to go back to the depot, I though that was unnecessary to create another class to make this breaks and I include a planning variable with a epoch second timestamp when the route restart is needed (the timestamps are between a relatively small range, startOfTrip on the code below).

Optaplanner is able to create the solver with the given xml configuration and if I create a break point on the score calculator I can inspect the vars and see that the construction heuristic is able to create a valid Visit chain and even set values of the timestamp variable.

But somehow after a few milliseconds the solver hits a NullPointerException when the org.optaplanner.core.impl.heuristic.selector.move.generic.chained.ChainedSwapMove constructor is called on the line 43 ( on the original source code of the given version above)

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

it seems that the variable inverseVariableSupplyList contains a null reference ( it creates this null reference when it analyze the variableDescriptorList containing the regular non chained PlanningVariable )

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


Solution

  • We have recently fixed PLANNER-1961, the symptops of which bear remarkable similarity to your issue here. Please check out OptaPlanner 7.39.0.Final (when it's out) or later, chances are your problem will go away.