Search code examples
javaquarkusoptaplanner

PlanningEntity field with an InverseRelationShadowVariable in a ProblemFact is not updated - Optaplanner


I have a ProblemFact AgentAvailability which reference a shadow planning entity Agent (Agent has no PlanningVariable, only an InverseRelationShadowVariable referencing the PlanningEntity Shift)

Here is my class definitions:

Shift.class

@PlanningEntity
public class Shift extends AbstractPersistable implements Comparable<Shift> {

@PlanningVariable(valueRangeProviderRefs = { "agentRange" }, nullable = true)
private Agent agent;
private Spot spot;

private LocalDateTime startDateTime;
private LocalDateTime endDateTime;

//Constructor, getters, setters and JPA annotations removed for clarity

Agent.class

@PlanningEntity
public class Agent implements Comparable<Agent> {

@PlanningId
private long registrationNumber;
private String username;
private boolean pro;

private SortedSet<Skill> skillSet = new TreeSet<>();

@InverseRelationShadowVariable(sourceVariableName = "agent")
private SortedSet<Shift> shiftSet = new TreeSet<>();

//Constructor, getters, setters and JPA annotations removed for clarity

AgentAvailability.class

public class AgentAvailability extends AbstractPersistable implements Comparable<AgentAvailability> {

private Agent agent;
private LocalDateTime startDateTime;
private LocalDateTime endDateTime;

//Constructor, getters, setters and JPA annotations removed for clarity

and here is my Solution definition:

Schedule.class

@PlanningSolution
public class Schedule extends AbstractPersistable {

// ...
@ProblemFactCollectionProperty
@ValueRangeProvider(id = "agentRange")
private List<Agent> agentList = new ArrayList<>();

@ProblemFactCollectionProperty
private List<AgentAvailability> agentAvailabilities = new ArrayList<>();

@PlanningEntityCollectionProperty
private List<Shift> shifts = new ArrayList<>();

// ...
//Constructor, getters, setters and JPA annotations removed for clarity

the inverseShadowVariable is working as expected: when an Agent is assigned to a Shift, the shiftSet from the assigned Agent is updated with this shift.

The problem is with the field agent from AgentAvailability.

From my understanding, when Optaplanner clones the solution, according to documentation, it reuses instances for ProblemFacts, and uses cloning for planning entities.

Since AgentAvailability is a Fact I assume it's reused (and in debugger view, this is the case, instance is the same between initial problem and bestSolution found)

Agent from Schedule are correctly cloned with their shiftSet to the solution too.

But the problem is that agent field in AgentAvailability is still the Agent instance from the original problem.

Therefore, the field shiftSet of agent in AgentAvailability instances is still empty.

It is the case at the end in the bestSolution found, but also during computing.

Therefore, I cannot use a constraint like this:

 return constraintFactory.from(AgentAvailability.class)
                            .groupBy(AgentAvailability::getAgent, count())
                            .filter((agent, availabilities) -> availabilities - agent.getShiftSet().size() > 0)
.penalizeConfigurableLong(ShiftSchedulingConstraintConfiguration.PRO_HAS_SHIFT, (agent, availabilities) -> availabilities - agent.getShiftSet().size());

since agent.getShiftSet().size() always return 0 even when the agent has been assigned to shifts.

A possible Workaround would be to use a from(Agent.class) clause, and join with AgentAvailabilities and Shift to "rebuild" the shiftSet from the Agent Entity. but is there any way to "clone" the shiftSet from Agent entities in the Agent of AgentAvailability so I can use agent field from AgentAvailability with updated shiftSet?


Solution

  • This is an issue with your model. You can not expect AgentAvailability to both be and not be cloned. If AgentAvailability is a problem fact, it will not be cloned, and therefore the value of agent will not be changed.

    (Side note: maybe we should detect if an entity is referenced from a planning fact, and fail fast? I don't see how such a situation leads to anything but trouble.)

    There are several solutions to this problem. I recommend you make Agent a problem fact and create a new entity AgentAssignment, mapping 1-to-1 to Agent. In this model, you can freely reference Agent fact instances in other facts.

    The constraint then becomes relatively easy to write.

    from(AgentAssignment.class)
        .join(AgentAvailability.class,
              Joiners.equal(
                  AgentAssignment::getAgent,
                  AgentAvailability::getAgent))
        .groupBy(
             (agentAssignment, agentAvailability) -> agentAssignment, 
             countBi())
        .filter((agentAssignment, availabilities) -> availabilities - agentAssignment.getShiftSet().size() > 0)
        ...