Reputation: 33
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
Upvotes: 3
Views: 305
Reputation: 316
It may be that your time span is too short. Supposing it isn't...
You should make a constraint that scores a fair distribution of the workload. If the 2 employees do 100% of all the work, and the 4 other employees 0%, then there is a "fairness" issue.
As an example, let's assume there is a total effort of 12 units of work to be assigned to these 6 employees. Even if these 12 units don't exceed the yearly/monthly/weekly, and not even the daily employee contract's constraints nor any other constraints you made, then that extra constraint will take care that the 12 units of work will be fairly distributed over the 6 employees, so 2 units of work for each of them in the ideal case.
Now if the 2 employees get all 12 units, the constraint could go something like : "for each employee, square the difference of his efforts and the average effort, and penalize for the result". In the case mentioned above, that would be
(6-2) squared + (6-2) squared + (0-2) squared + (0-2) squared + (0-2) squared + (0-2) squared
so 16+16+4+4+4+4=48 penalisations
In the ideal case, you have :
(2-2) squared + (2-2) squared + (2-2) squared + (2-2) squared + (2-2) squared + (2-2) squared
so 0 penalisations
Should you consider NOT to square, you would have :
(6-2) + (6-2) + (0-2) + (0-2) + (0-2) + (0-2)
so 4+4-2-2-2-2 = 0 penalisations, which would be totally against the goal of the constraint
Upvotes: 0
Reputation: 48
You should add this configuration in solver config file
<localSearch>
<unionMoveSelector>
<changeMoveSelector>
<selectionOrder>RANDOM</selectionOrder>
</changeMoveSelector>
</unionMoveSelector>
</localSearch>
Upvotes: 3