Skip to content
Blog

Simplify the Shadow Variable Listener Implementation

Learn how to simplify creating listeners for planning list variables with the new shadow variable @CascadingUpdateShadowVariable.

Imagine reducing your code by 90% while making your optimization models more efficient and easier to maintain. Sounds impossible? Not anymore. With Timefold’s new @CascadingUpdateShadowVariable, you can streamline complex planning tasks—like updating the arrival times in a vehicle route—into just a few lines of code. In this article, I’ll show you how this powerful new feature can simplify your codebase, enhance performance, and free you from the tedious task of writing custom listeners. We will demonstrate how to simplify the Vehicle Route Problem (VRP), turning 60 lines of code into 5.

The challenge: Managing Tail Chain updates in VRP

Timefold Solver has a useful feature called shadow variables. A shadow variable is a planning variable that can be deduced from the state of genuine planning variables, such as the arrival time of a given visit or the following visit of a vehicle route. The shadow variable @ShadowVariable provides an approach that allows implementing a custom listener to update the variable values.

Before Timefold Solver 1.13.0, you would update a chain of connected elements using the @ShadowVariable listener. This robust solution can be applied to any related use case. However, at Timefold, we aim to simplify your life by offering solutions that enhance your models' understandability and improve their performance. And now we’ve made it so that creating a separate listener to update source variables is sometimes unnecessary.

The upcoming sections will initially discuss the requirements for updating the tail chains. Following that, the different approaches for configuring a listener that updates a specified source shadow variable and triggers changes to the subsequent elements of a planning list variable will be explained.

# Tail chains need updating

Let’s consider the VRP problem definition. Given a set of vehicles and a list of locations to visit, each visit carries the expected arrival time to the destination. We estimate the arrival time based on the arrival time to the previous location and the travel time between locations.

In the previous image, the vehicle arrives at Location A at 08:00. After that, it travels to Location B, arriving at 09:00, and then travels to Location C. We have a series of related locations that need to be updated sequentially. That means if there are any changes in the arrival time at Location A, all subsequent locations must update their arrival times. In other words, a tail chain update is needed.

To illustrate the use case, let’s take a look at the Visit model class:

public class Visit {
    ...
    @InverseRelationShadowVariable(sourceVariableName = "visits")
    private Vehicle vehicle;
    @PreviousElementShadowVariable(sourceVariableName = "visits")
    private Visit previousVisit;
    @NextElementShadowVariable(sourceVariableName = "visits")
    private Visit nextVisit;
    ...
    private LocalDateTime arrivalTime;
}

The arrivalTime field contains the vehicle’s arrival time, which the constraint can utilize to impose penalties for missing deadlines. The next two sections explain how to use the @ShadowVariable and @CascadingUpdateShadowVariable listeners, respectively.

# The old way: implementing shadow variable listeners

The @ShadowVariable triggers a listener when one or more source shadow variables change. Let’s update the model Visit:

public class Visit {
    ...
    @InverseRelationShadowVariable(sourceVariableName = "visits")
    private Vehicle vehicle;
    @PreviousElementShadowVariable(sourceVariableName = "visits")
    private Visit previousVisit;
    @NextElementShadowVariable(sourceVariableName = "visits")
    private Visit nextVisit;
    @ShadowVariable(variableListenerClass = ArrivalTimeUpdatingVariableListener.class, sourceVariableName = "vehicle")
    @ShadowVariable(variableListenerClass = ArrivalTimeUpdatingVariableListener.class, sourceVariableName = "previousVisit")
    private LocalDateTime arrivalTime;
}

Next, let’s implement the ArrivalTimeUpdatingVariableListener:

public class ArrivalTimeUpdatingVariableListener
    implements VariableListener<VehicleRoutePlan, Visit> {
    ...
    @Override
    public void afterVariableChanged(ScoreDirector<VehicleRoutePlan> scoreDirector, Visit visit) {
        if (visit.getVehicle() == null) {
            if (visit.getArrivalTime() != null) {
                scoreDirector.beforeVariableChanged(visit, ARRIVAL_TIME_FIELD);
                visit.setArrivalTime(null);
                scoreDirector.afterVariableChanged(visit, ARRIVAL_TIME_FIELD);
            }
            return;
        }
        Visit previousVisit = visit.getPreviousVisit();
        LocalDateTime departureTime =
                previousVisit == null ? visit.getVehicle().getDepartureTime() : previousVisit.getDepartureTime();
        Visit nextVisit = visit;
        LocalDateTime arrivalTime = calculateArrivalTime(nextVisit, departureTime);
        while (nextVisit != null && !Objects.equals(nextVisit.getArrivalTime(), arrivalTime)) {
            scoreDirector.beforeVariableChanged(nextVisit, ARRIVAL_TIME_FIELD);
            nextVisit.setArrivalTime(arrivalTime);
            scoreDirector.afterVariableChanged(nextVisit, ARRIVAL_TIME_FIELD);
            departureTime = nextVisit.getDepartureTime();
            nextVisit = nextVisit.getNextVisit();
            arrivalTime = calculateArrivalTime(nextVisit, departureTime);
        }
    }
    ...
}

Whenever the vehicle or previousVisit changes, the listener automatically updates the subsequent visits.

# The new way: simplifying with Cascading Updates

As of Timefold Solver 1.13.0, the @CascadingUpdateShadowVariable does not require a separate listener class. Instead, we add a new method to the domain class that updates the related shadow variables. Let’s update the Visit class with the cascading shadow variable annotation:

public class Visit {
    ...
    @InverseRelationShadowVariable(sourceVariableName = "visits")
    private Vehicle vehicle;
    @PreviousElementShadowVariable(sourceVariableName = "visits")
    private Visit previousVisit;
    @NextElementShadowVariable(sourceVariableName = "visits")
    private Visit nextVisit;
    @CascadingUpdateShadowVariable(targetMethodName = "updateArrivalTime")
    private LocalDateTime arrivalTime;
}

After that, we need to add a method called updateArrivalTime with the logic to update arrivalTime:

private void updateArrivalTime() {
    if (previousVisit == null && vehicle == null) {
        arrivalTime = null;
        return;
    }
    LocalDateTime departureTime = previousVisit == null ? vehicle.getDepartureTime() : previousVisit.getDepartureTime();
    arrivalTime = departureTime != null ? departureTime.plusSeconds(getDrivingTimeSecondsFromPreviousStandstill()) : null;
}

The method must not be static and must not accept any parameters. Timefold Solver triggers updateArrivalTime after all events are processed. Therefore, the listener will be the last one executed during the event lifecycle. Additionally, it automatically propagates changes to the subsequent visits and stops when the arrivalTime value does not change or when it reaches the end.

# Conclusion

Updating a set of interconnected elements in a specific order is a common scenario when defining your optimization model. Timefold Solver provides different solutions for defining listeners that update shadow variable sources in a specific sequence. The new Cascading Update Shadow Variable simplifies creating listeners for planning list variables, resulting in code that is easier to write, read, and maintain.

Continue reading