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
?
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)
...