Search code examples
optaplanner

OptaPlanner - The entity was never added to this ScoreDirector error


I am implementing an algorithm similar to the NurseRoster one in OptaPlanner. I need to implement a rule in drools that check if the Employee cannot work more days than the number of days in his contract. Since i couldn't figure out how to make this in drools, i decided to write it as a method in a class, and then use it in drools to check if the constraint has been broken. Since i needed a List of ShiftAssignments in the Employee class, i needed to use an @InverseRelationShadowVariable that updated that list automatically an Employee got assigned to a Shift. Since my Employee now has to be a PlanningEntity, the error The entity was never added to this ScoreDirector appeared. I believe the error is caused by my ShiftAssignment entity, which has a @ValueRangeProvider of employees that can work in that Shift. I think this is due to the fact that ScoreDirector.beforeEntityAdded and ScoreDirector.afterEntityAdded were never called, hence the error. For some reason when i removed that range provider from ShiftAssignment and put it on NurseRoster which is the @PlanningSolution, it worked.

Here is the code:

Employee:

@InverseRelationShadowVariable(sourceVariableName = "employee")
public List<ShiftAssignment> getEmployeeAssignedToShiftAssignments() {
    return employeeAssignedToShiftAssignments;
}

ShiftAssignment:

@PlanningVariable(valueRangeProviderRefs = {
    "employeeRange" }, strengthComparatorClass =    EmployeeStrengthComparator.class,nullable = true)
public Employee getEmployee() {
    return employee;
}

// the value range for this planning entity
@ValueRangeProvider(id = "employeeRange")
public List<Employee> getPossibleEmployees() {
    return getShift().getEmployeesThatCanWorkThisShift();
}

NurseRoster:

@ValueRangeProvider(id = "employeeRange")
@PlanningEntityCollectionProperty
public List<Employee> getEmployeeList() {
    return employeeList;
}

And this is the method i use to update that listOfEmployeesThatCanWorkThisShift:

public static void checkIfAnEmployeeCanBelongInGivenShiftAssignmentValueRange(NurseRoster nurseRoster) {
    List<Shift> shiftList = nurseRoster.getShiftList();
    List<Employee> employeeList = nurseRoster.getEmployeeList();
    for (Shift shift : shiftList) {
        List<Employee> employeesThatCanWorkThisShift = new ArrayList<>();
        String shiftDate = shift.getShiftDate().getDateString();
        ShiftTypeDefinition shiftTypeDefinitionForShift = shift.getShiftType().getShiftTypeDefinition();
        for (Employee employee : employeeList) {
            AgentDailySettings agentDailySetting = SearchThroughSolution.findAgentDailySetting(employee, shiftDate);
            List<ShiftTypeDefinition> shiftTypeDefinitions = agentDailySetting.getShiftTypeDefinitions();
            if (shiftTypeDefinitions.contains(shiftTypeDefinitionForShift)) {
                employeesThatCanWorkThisShift.add(employee);

            }
        }
        shift.setEmployeesThatCanWorkThisShift(employeesThatCanWorkThisShift);
    }
}

And the rule that i use:

rule "maxDaysInPeriod"
when
$shiftAssignment : ShiftAssignment(employee != null)
then
int differentDaysInPeriod = MethodsUsedInScoreCalculation.employeeMaxDaysPerPeriod($shiftAssignment.getEmployee());
int maxDaysInPeriod = $shiftAssignment.getEmployee().getAgentPeriodSettings().getMaxDaysInPeriod();
if(differentDaysInPeriod > maxDaysInPeriod)
{
scoreHolder.addHardConstraintMatch(kcontext, differentDaysInPeriod - maxDaysInPeriod);
}
end

How can i fix this error?


Solution

  • This has definitely something to do with the solution cloning that is happening when a "new best solution" is created.

    I encountered the same error when i implemented custom solution cloning. In my project i have multiple planning entity classes and all of them have references to each other (either a single value or a List). So when solution cloning is happening the references need to be updated so they can point to the cloned values. This is something that the default cloning process is doing without a problem, and thus leaving the solution in a consistent state. It even updates the Lists of planning entity instances in the parent planning entities correctly (covered by the method "cloneCollectionsElementIfNeeded" from the class "FieldAccessingSolutionCloner" from the OptaPlanner core).

    Just a demonstration what i have when it comes to the planning entity classes:

    @PlanningEntity
    public class ParentPlanningEntityClass{
        List<ChildPlanningEntityClass> childPlanningEntityClassList;
    }
    
    @PlanningEntity
    public class ChildPlanningEntityClass{
        ParentPlanningEntityClass parentPlanningEntityClass;
    }
    

    At first i did not update any of the references and got the error even for "ChildPlanningEntityClass". Then i have written the code that updates the references. When it comes to the planning entity instances that were coming from the class "ChildPlanningEntityClass" everything was okay at this point because they were pointing to the cloned object. What i did wrong in the "ParentPlanningEntityClass" case was that i did not create the "childPlanningEntityClassList" list from scratch with "new ArrayList();", but instead i just updated the elements of the list (using the "set" method) to point at the cloned instances of the "ChildPlanningEntityClass" class. When creating a "new ArrayList();", filling the elements to point to the cloned objects and setting the "childPlanningEntityClassList" list everything was consistent (tested with FULL_ASSERT).

    So just connecting it to my issue maybe the list "employeeAssignedToShiftAssignments" is not created from scratch with "new ArrayList();" and elements instead just get added or removed from the list. So what could happen (if the list is not created from scratch) here is that both the working and the new best solution (the clone) will point to the same list and when the working solution would continue to change this list it would corrupt the best solution.