Search code examples
javaoptaplanner

Variablelistener corruption with anchorshadowedvariable in optaplanner


I am trying to solve a planning problem which look like the Time window vehicule routing problem.

So I am looking for a Solution containing a list of tasks. Tasks can be assigneed to one of two workers.

My Task class looks like that (I didn't cut/paste all the getters and setters here) :

@PlanningEntity(difficultyComparatorClass = TaskDifficultyComparator.class)
public class Task implements Location {
    private String location;
    private LocalTime arrivalTime;
    private Location previousLocation;
    private Task nextTask;
    private StartLocal startLocal;


    private Duration taskDuration = Duration.ofMinutes(20);
    private LocalTime readyTime;
    private LocalTime dueTime;

    @PlanningVariable(valueRangeProviderRefs = {"tasks", "startLocal"},
            graphType = PlanningVariableGraphType.CHAINED)
    public Location getPreviousLocation() {
        return previousLocation;
    }
    public void setPreviousLocation(Location previousLocation) {
        this.previousLocation = previousLocation;
    }

    @Override
    public String {return location;}

    @Override
    public void setLocation(String location) {this.location = location;}

    public int getTimeToGoTo(Location location) {
        ....
    }

    @AnchorShadowVariable(sourceVariableName = "previousLocation")
    public StartLocal getStartLocal() {
        return startLocal;
    }
    public void setStartLocal(StartLocal startLocal) {
        this.startLocal = startLocal;
    }

    @CustomShadowVariable(variableListenerClass = ArrivalTimeVariableListener.class,
            sources = {@CustomShadowVariable.Source(variableName = "previousLocation")})
    public LocalTime getArrivalTime() {
        return arrivalTime;
    }
    public void setArrivalTime(LocalTime arrivalTime) {
        this.arrivalTime = arrivalTime;
    }
}

And my variable listener class is :

public class ArrivalTimeVariableListener implements VariableListener<Task> {
    @Override
    public void beforeEntityAdded(ScoreDirector scoreDirector, Task task) {}

    @Override
    public void afterEntityAdded(ScoreDirector scoreDirector, Task task) {
        updateArrivalTime(scoreDirector, task);
    }

    @Override
    public void beforeVariableChanged(ScoreDirector scoreDirector, Task task) {        }

    @Override
    public void afterVariableChanged(ScoreDirector scoreDirector, Task task) {
        updateArrivalTime(scoreDirector, task);
    }

    @Override
    public void beforeEntityRemoved(ScoreDirector scoreDirector, Task task) {        }

    @Override
    public void afterEntityRemoved(ScoreDirector scoreDirector, Task task) {        }

    private void updateArrivalTime(ScoreDirector scoreDirector, Task task){
        LocalTime arrivalTimeInThisLocation = calculateArrivalTime(task);
        Task shadowTask = task;
        while (shadowTask != null) {
            scoreDirector.beforeVariableChanged(shadowTask, "arrivalTime");
            shadowTask.setArrivalTime(arrivalTimeInThisLocation);
            scoreDirector.afterVariableChanged(shadowTask, "arrivalTime");
            shadowTask = shadowTask.getNextTask();
            if(shadowTask!=null)arrivalTimeInThisLocation = calculateArrivalTime(shadowTask);
        }
    }

    @VisibleForTesting
    static public LocalTime calculateArrivalTime(Task task){
        LocalTime whenArriveAtPreviousLocation = task.getPreviousLocation().getArrivalTime();
        int secondsToGo = task.getTimeToGoTo(task.getPreviousLocation());
        long secondsToCompleteLastTask = 0;
        if(task.getPreviousLocation() instanceof Task){
            secondsToCompleteLastTask = ((Task) task.getPreviousLocation()).getTaskDuration().getSeconds();
        }
        LocalTime whenArriveInThisLocationASAP = whenArriveAtPreviousLocation.
                plusSeconds(secondsToCompleteLastTask).
                plusSeconds(secondsToGo);

        if(whenArriveInThisLocationASAP.isAfter(task.getReadyTime())){                
            return whenArriveInThisLocationASAP;
        }
        return task.getReadyTime();
    }
}

When I try to solve my problem I have the following error :

VariableListener corruption: the entity (Task s shadow variable (Task.arrivalTime)'s corrupted value (09:32) changed to uncorrupted value (09:33) after all VariableListeners were triggered without changes to the genuine variables. Probably the VariableListener class for that shadow variable (Task.arrivalTime) forgot to update it when one of its sources changed after completedAction (Initial score calculated).

I know that this issue is a duplicate of this one. But I read (very very) carrefully answer of this SOF question, and I can't understand what I am doing wrong.

I well understood, role of the variable listener in my use case is to update the "arrivalTime" of the tasks following the tasks we just change. Isn't it ?


Solution

  • Ok, I found what I did wrong.

    What I did wrong is hard to see in my question. The problem was that I initialize my solution's shadows variable to a non null value. I had the following code in my unit test :

    Task task1 = new Task();
    task1.setPreviousTask(task0);
    task0.setNextTask(task1);
    
    //...
    solution.computeAllArrivalTimes();
    

    This is not a good idea to initialize shadows variable and Planning variables. If I set them to null, then every things works like a charm.

    I just need to add some null check in "calculateArrivalTime" to allow this method to be called.