Assignment Recommendation API
| This feature is exclusive to Timefold Solver Plus and Enterprise Editions. |
With real-time planning, we can respond to a continuous stream of external changes. However, it is often necessary to respond to ad hoc changes too, for example when a call center operator needs to arrange an appointment with a customer. In such cases, it is not necessary to use the full power of real-time planning. Instead, immediate response to the customer and a selection of available time windows are more important. This is where Assignment Recommendation API comes in.
The Assignment Recommendation API allows you to quickly respond to ad hoc changes, while providing a selection of the best available options for fitting the change in the existing schedule. It doesn’t use the full local search algorithm. Instead, it uses a simple greedy algorithm together with incremental calculation. This combination allows the API to find the best possible fit within the existing solution in a matter of milliseconds, even for large planning problems.
Once the customer has accepted one of the available options and the change has been reflected in the solution, the full local search algorithm can be used to optimize the entire solution around this change. This would be an example of continuous planning.
1. Using the Assignment Recommendation API
The Assignment Recommendation API requires an entity to be evaluated for assignment:
EmployeeSchedule employeeSchedule = ...; // Our planning solution.
Shift unassignedShift = new Shift(...); // A new shift needs to be assigned.
employeeSchedule.getShifts().add(unassignedShift);
If the entity is unassigned, then it must be the only unassigned entity in the planning solution.
The SolutionManager is then used to retrieve the recommended assignments for this entity:
SolutionManager<EmployeeSchedule, HardSoftScore> solutionManager = ...;
List<RecommendedAssignment<Employee, HardSoftScore>> recommendations =
solutionManager.recommendAssignment(employeeSchedule, unassignedShift, Shift::getEmployee);
Breaking this down, we have:
-
employeeSchedule, the planning solution. -
unassignedShift, the uninitialized entity, which is part of the planning solution. -
Shift::getEmployee, a function extracting the planning variable from the entity, also called a "proposition function". -
List<RecommendedAssignment<Employee, HardSoftScore>>, the list of recommended employees to assign to the shift, in the order of decreasing preference. Each recommendation contains the employee and the difference in score caused by assigning the employee to the shift. This difference has the full explanatory power of score analysis.
This list of recommendations can be used to present the operator with a selection of available options, as it is fully serializable to JSON and can be sent to a web browser or mobile app. The operator can then select the best available recommendation and assign the employee to the shift, represented here by the necessary backend code:
RecommendedAssignment<Employee, HardSoftScore> bestRecommendation = recommendations.get(0);
Employee bestEmployee = bestRecommendation.proposition();
unassignedShift.setEmployee(bestEmployee);
If required, continuous planning can be used to optimize the entire solution afterwards.
|
Assignment Recommendation API requires the |
2. Using mutable types in the proposition function
In the previous example, we used a simple proposition function that extracts the planning variable from the entity. However, it is also possible to use a more complex proposition function that extracts the entire planning entity, or any values that will mutate as the solver tries to find the best fit. In that case, there are some caveats to consider.
The solver will try to find the best fit for the uninitialized entity, and it will start from the solution it received on input. Before trying the next value to assign, it will first return to that original solution. The consequence of this is that if our proposition function returns any values that change during this process, those changes will also affect the previously processed propositions. In other words, if we decide to return the entire entity from the proposition function, we will find that each of the final recommendations is the same. And because the solver will return to the original solution after trying the last value, the final recommendation will be unassigned, defeating the purpose of the API. Consider the following example:
SolutionManager<EmployeeSchedule, HardSoftScore> solutionManager = ...;
List<RecommendedAssignment<Shift, HardSoftScore>> recommendations =
solutionManager.recommendAssignment(employeeSchedule, unassignedShift, shift -> shift);
The proposition function (shift → shift) returns the entire Shift entity.
Because of the behavior described above,
every RecommendedAssignment in the recommendations list will point to the same unassignedShift,
and its employee variable will be null.
This is not what we want,
because none of the RecommendedAssignment instances give us the Employee we need to assign to the shift.
To avoid this, the proposition function should preferably return a value that does not change during the process, such as the planning variable instead of the entire entity. If it’s necessary to return a value that could be mutated by the solver, we should make a defensive copy.