I have a system in optaplanner which generates shifts for employees. When there are for e.x 6 employees and shift capacity is two, the solution is generated only for 2 employees until they reach the maximum working hours. How can I add a constraint so that the solution is generated with mixed employees.
Below are rules that are defined in optaplanner:
global HardMediumSoftLongScoreHolder scoreHolder;
// Hard constraints
rule "Required skill for a shift"
when
$shift: Shift(employee != null, hasRequiredSkills() == false)
then
scoreHolder.penalize(kcontext, $shift.getLengthInMinutes());
end
rule "Unavailable time slot for an employee"
when
$availability: EmployeeAvailability(
state == EmployeeAvailabilityState.UNAVAILABLE,
$e : employee,
$startDateTime : startDateTime,
$endDateTime : endDateTime)
Shift(employee == $e,
$startDateTime < endDateTime,
$endDateTime > startDateTime)
then
scoreHolder.penalize(kcontext, $availability.getDuration().toMinutes());
end
rule "No overlapping shifts"
when
$s : Shift(employee != null, $e : employee, $firstStartDateTime: startDateTime, $firstEndDateTime : endDateTime)
$s2: Shift(employee == $e, this != $s,
$firstStartDateTime < endDateTime,
$firstEndDateTime > startDateTime)
then
scoreHolder.penalize(kcontext, $s2.getLengthInMinutes());
end
rule "No more than 2 consecutive shifts"
when
$s : Shift(
employee != null,
$e : employee,
$firstEndDateTime : endDateTime)
$s2: Shift(
employee == $e,
$firstEndDateTime == startDateTime,
this != $s,
$secondEndDateTime : endDateTime)
$s3: Shift(
employee == $e,
$secondEndDateTime == startDateTime,
this != $s,
this != $s2)
then
scoreHolder.penalize(kcontext, $s3.getLengthInMinutes());
end
rule "Break between non-consecutive shifts is at least 10 hours"
when
$s : Shift(
employee != null,
$e : employee,
$leftEndDateTime : endDateTime)
Shift(
employee == $e,
$leftEndDateTime < startDateTime,
$leftEndDateTime.until(startDateTime, ChronoUnit.HOURS) < 10,
this != $s,
$rightStartDateTime: startDateTime)
then
long breakLength = $leftEndDateTime.until($rightStartDateTime, ChronoUnit.MINUTES);
scoreHolder.penalize(kcontext, (10 * 60) - breakLength);
end
rule "Daily minutes must not exceed contract maximum"
when
$employee : Employee($contract : contract, $contract.getMaximumMinutesPerDay() != null)
$s : Shift(employee == $employee, $startDateTime : startDateTime)
accumulate(
$other : Shift(
employee == $employee, $shiftStart : startDateTime,
$shiftEnd : endDateTime,
$shiftStart.toLocalDate().equals($startDateTime.toLocalDate())
),
$shiftCount : count($other),
$totalMinutes : sum(Duration.between($shiftStart, $shiftEnd).toMinutes())
)
Number(this > $contract.getMaximumMinutesPerDay()) from $totalMinutes
then
scoreHolder.penalize(kcontext, (((long)$totalMinutes) - $contract.getMaximumMinutesPerDay()) / $shiftCount);
end
rule "Weekly minutes must not exceed contract maximum"
when
$rosterConstraintConfiguration : RosterConstraintConfiguration()
$employee : Employee($contract : contract, $contract.getMaximumMinutesPerWeek() != null)
$s : Shift(employee == $employee, $startDateTime : startDateTime)
accumulate(
$other : Shift(
employee == $employee, $shiftStart : startDateTime,
$shiftEnd : endDateTime,
DateTimeUtils.sameWeek($rosterConstraintConfiguration.getWeekStartDay(), $shiftStart, $startDateTime)
),
$shiftCount : count($other),
$totalMinutes : sum(Duration.between($shiftStart, $shiftEnd).toMinutes())
)
Number(this > $contract.getMaximumMinutesPerWeek()) from $totalMinutes
then
scoreHolder.penalize(kcontext, (((long)$totalMinutes) - $contract.getMaximumMinutesPerWeek()) / $shiftCount);
end
rule "Monthly minutes must not exceed contract maximum"
when
$employee : Employee($contract : contract, $contract.getMaximumMinutesPerMonth() != null)
$s : Shift(employee == $employee, $startDateTime : startDateTime)
accumulate(
$other : Shift(
employee == $employee, $shiftStart : startDateTime,
$shiftEnd : endDateTime,
$shiftStart.getMonth() == $startDateTime.getMonth(),
$shiftStart.getYear() == $startDateTime.getYear()
),
$shiftCount : count($other),
$totalMinutes : sum(Duration.between($shiftStart, $shiftEnd).toMinutes())
)
Number(this > $contract.getMaximumMinutesPerMonth()) from $totalMinutes
then
scoreHolder.penalize(kcontext, (((long)$totalMinutes) - $contract.getMaximumMinutesPerMonth()) / $shiftCount);
end
rule "Yearly minutes must not exceed contract maximum"
when
$employee : Employee($contract : contract, $contract.getMaximumMinutesPerYear() != null)
$s : Shift(employee == $employee, $startDateTime : startDateTime)
accumulate(
$other : Shift(
employee == $employee, $shiftStart : startDateTime,
$shiftEnd : endDateTime,
$shiftStart.getYear() == $startDateTime.getYear()
),
$shiftCount : count($other),
$totalMinutes : sum(Duration.between($shiftStart, $shiftEnd).toMinutes())
)
Number(this > $contract.getMaximumMinutesPerYear()) from $totalMinutes
then
scoreHolder.penalize(kcontext, (((long)$totalMinutes) - $contract.getMaximumMinutesPerYear()) / $shiftCount);
end
// Medium constraints
rule "Assign every shift"
when
Shift(employee == null)
then
scoreHolder.penalize(kcontext);
end
// Soft constraints
rule "Employee is not original employee"
when
$shift: Shift(originalEmployee != null,
employee != null,
employee != originalEmployee)
then
scoreHolder.penalize(kcontext, $shift.getLengthInMinutes());
end
rule "Undesired time slot for an employee"
when
$availability: EmployeeAvailability(
state == EmployeeAvailabilityState.UNDESIRED,
$e : employee,
$startDateTime : startDateTime,
$endDateTime : endDateTime)
Shift(employee == $e,
$startDateTime < endDateTime,
$endDateTime > startDateTime)
then
scoreHolder.penalize(kcontext, $availability.getDuration().toMinutes());
end
rule "Desired time slot for an employee"
when
$availability: EmployeeAvailability(
state == EmployeeAvailabilityState.DESIRED,
$e : employee,
$startDateTime : startDateTime,
$endDateTime : endDateTime)
Shift(employee == $e,
$startDateTime < endDateTime,
$endDateTime > startDateTime)
then
scoreHolder.reward(kcontext, $availability.getDuration().toMinutes());
end
rule "Employee is not rotation employee"
when
$shift: Shift(rotationEmployee != null, employee != null, employee != rotationEmployee)
then
scoreHolder.penalize(kcontext, $shift.getLengthInMinutes());
end
You should add this configuration in solver config file
<localSearch>
<unionMoveSelector>
<changeMoveSelector>
<selectionOrder>RANDOM</selectionOrder>
</changeMoveSelector>
</unionMoveSelector>
</localSearch>