I am trying to create a domain model for my situation, using Timefold's Employee Scheduling example as a starting point. The main difference in our business domain is that, while there is a (known) minimum number of employees that should be assigned to each shift, it is possible to assign additional employees ("supernumeraries"), to ensure that the team assigned to the shift collectively has the necessary skills. Furthermore, in our domain, there is no 1-to-1 matching between the skills of an individual employee and the skills needed during a shift; each employee has 2 to 9 skills; but through the combination of all employees on the team, we calculate a collective set of skills (which is greater that the sum of its parts), and check whether this collective set of skills is adequate for the needs of a given shift. After reading the domain modelling guide (https://timefold.ai/docs/timefold-solver/latest/design-patterns/design-patterns#domainModelingGuide) I'm not sure what would be the best way to express the possibility of supernumerary employees per shift. Should I use a PlanningListVariable, or would that increase the search space unnecessarily? Any help would be appreciated. Note: I understand that domain modelling is a skill that brings a lot of value to a business; we're trying to create some simple models in-house, in order to explore Timefold in the context of our business domain, and if it all goes well, we'll probably contact Timefold for consulting services on building the full-scale complex models.
Tried to modify Timefold's Employee Scheduling example to adapt it to my business domain.
I would model it as follows:
The planning entity is Shift, with a nullable planning variable employee and fields shiftGroup, startTime, and endTime
ShiftGroup is a problem fact with fields shifts, minimumEmployeeCount and requiredSkills
Employee is a problem fact with fields id and skills
Do not use @PlanningListVariable
, it has the addition restriction that each planning value will be used exactly once, which is useful for domains like vehicle routing, but undesired for employee scheduling.
Assuming all shifts has a ShiftGroup, the constraint provider would look like this
Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[] {
allRequiredSkills(constraintFactory),
shiftGroupHasMinimumEmployees(constraintFactory)
// other constraints
};
}
Constraint allRequiredSkills(ConstraintFactory constraintFactory) {
return constraintFactory.forEachIncludingNullVars(Shift.class)
.groupBy(Shift::getShiftGroup)
.expand(shiftGroup -> shiftGroup.getMissingSkillCount())
.filter((shiftGroup, missingSkillCount) -> missingSkillCount != 0)
.penalize(HardSoftScore.ONE_HARD, (shiftGroup, missingSkillCount) -> missingSkillCount)
.asConstraint("Missing required skill in shift");
}
Constraint shiftGroupHasMinimumEmployees(ConstraintFactory constraintFactory) {
return constraintFactory.forEachIncludingNullVars(Shift.class)
.groupBy(Shift::getShiftGroup)
.expand(shiftGroup -> shiftGroup.getAssignedEmployeeCount())
.filter((shiftGroup, employeeCount) -> employeeCount < shiftGroup.getMinimumEmployeeCount())
.penalize(HardSoftScore.ONE_HARD, (shiftGroup, employeeCount) -> shiftGroup.getMinimumEmployeeCount() - employeeCount)
.asConstraint("Less than minimum employees in shift group");
}
getMissingSkillCount()
and getAssignedEmployeeCount()
are methods in ShiftGroup and would look like this:
int getMissingSkillCount() {
Set<String> missingSkills = new HashSet<>(requiredSkills);
for (Shift shift : shifts) {
if (shift.getEmployee() != null) {
missingSkills.removeAll(shift.getEmployee().getSkills());
if (missingSkills.isEmpty()) {
return 0;
}
}
}
return missingSkills.size();
}
int getAssignedEmployeeCount() {
int assignedEmployees = 0;
for (Shift shift : shifts) {
if (shift.getEmployee() != null) {
assignedEmployees++;
}
}
return assignedEmployees;
}