I'm trying to implement a Shift Scheduling system based on Nurse Rostering example. Unfortunately, i cannot get it right. Apparently,seems that it is an overconstrained problem which needs special attention and detailed custom moves (i have not developed any custom move,yet) and/or tweaking of weights and maybe redefinition of some constraints.
I am not sure and i can't find a way out.
Here are the problem details:
- 365 days (or at least 180 days)
- Each day has 2 shifts Day and Night
- 25 pharmacies (with no special skills) divided in 2 groups:
- Group A contains zone1 Pharmacies
- Group B contains zone2 Pharmacies
- 3 Pharmacies have the same address (members of the same group B) but are considered independent.
Constraints
- One shift per day for employees Hard constraint
- Min and Max number of Day Shifts,
Night Shifts and Total Assignments fairly distributed.Soft Constraint.
- Alternate shift pattern(D-N or N-D) ->unwanted patterns (N-N) and (D-D) Hard constraint
- minimum free days between workdays Soft Constraint
- no consecutive workdays Hard constraint
- Zone coverage: DAY and NIGHT shifts should be assigned to 1 pharmacy from each zone .Soft Constraint
- For same address employees: No 2 assignments in the same day Hard constraint,time between them = 3 days (Hard constraint but if needed for problem relaxation could be Soft constraint)
From input file:dataset.xml
<Contract ID="0"><Description>fulltime</Description>
<SingleAssignmentPerDay weight="1">true</SingleAssignmentPerDay><MaxNumAssignments on="1" weight="1">13</MaxNumAssignments><MinNumAssignments on="1" weight="1">11</MinNumAssignments><MaxNumDayAssignments on="1" weight="1">7</MaxNumDayAssignments>
<MinNumDayAssignments on="1" weight="1">5</MinNumDayAssignments>
<MaxNumNightAssignments on="1" weight="1">7</MaxNumNightAssignments>
<MinNumNightAssignments on="1" weight="1">5</MinNumNightAssignments>
<MaxConsecutiveWorkingDays on="1" weight="1">1</MaxConsecutiveWorkingDays><MinConsecutiveWorkingDays on="0" weight="1">1</MinConsecutiveWorkingDays> <MaxConsecutiveFreeDays on="1" weight="1">12</MaxConsecutiveFreeDays>
<MinConsecutiveFreeDays on="1" weight="4">5</MinConsecutiveFreeDays>
<UnwantedPatterns>
<Pattern>0</Pattern>
<Pattern>1</Pattern>
</UnwantedPatterns>
</Contract>
Where Pattern{0,1}={(DAY,DAY),(NIGHT,NIGHT)}`
RULES
rule "oneShiftPerDay"
dialect "mvel"
when
$leftAssignment : ShiftAssignment($leftId : id, $pharmacy : pharmacy, $shiftDate : shiftDate, pharmacy != null)
$rightAssignment : ShiftAssignment(pharmacy == $pharmacy, shiftDate == $shiftDate, id > $leftId)
then
scoreHolder.addHardConstraintMatch( kcontext, -10);
end
rule "insertPharmacyAssignmentTotal"
salience 2 // Do these rules first (optional, for performance)
dialect "mvel"
when
MinMaxContractLine(contractLineType == ContractLineType.TOTAL_ASSIGNMENTS, enabled == true,$contract : contract)
$pharmacy : Pharmacy(contract == $contract)
$assignmentTotal : Number() from accumulate(
$assignment : ShiftAssignment(pharmacy == $pharmacy,$sdi1:shiftDateDayIndex),
count($assignment)
)
then
insertLogical(new PharmacyAssignmentTotal($pharmacy, $assignmentTotal.intValue()));
end
rule "insertPharmacyWorkSequence"
salience 1 // Do these rules first (optional, for performance)
when
PharmacyConsecutiveAssignmentStart(
$pharmacy : pharmacy,
$firstDayIndex : shiftDateDayIndex
)
PharmacyConsecutiveAssignmentEnd(
pharmacy == $pharmacy,
shiftDateDayIndex >= $firstDayIndex,
$lastDayIndex : shiftDateDayIndex
)
// There are no free days between the first and last day
not PharmacyConsecutiveAssignmentEnd(
pharmacy == $pharmacy,
shiftDateDayIndex >= $firstDayIndex && < $lastDayIndex
)
then
insertLogical(new PharmacyWorkSequence($pharmacy, $firstDayIndex, $lastDayIndex));
end
rule "insertFirstPharmacyFreeSequence"
salience 1 // Do these rules first (optional, for performance)
when
PharmacyConsecutiveAssignmentStart(
$pharmacy : pharmacy,
$lastDayIndexPlusOne : shiftDateDayIndex
)
// There are no working days before the first day
not PharmacyConsecutiveAssignmentEnd(
pharmacy == $pharmacy,
shiftDateDayIndex < $lastDayIndexPlusOne
)
PharmacyRosterInfo(firstShiftDateDayIndex < $lastDayIndexPlusOne, $firstDayIndex : firstShiftDateDayIndex)
then
insertLogical(new PharmacyFreeSequence($pharmacy, $firstDayIndex, $lastDayIndexPlusOne - 1));
end
rule "insertLastPharmacyFreeSequence"
salience 1 // Do these rules first (optional, for performance)
when
PharmacyConsecutiveAssignmentEnd(
$pharmacy : pharmacy,
$firstDayIndexMinusOne : shiftDateDayIndex
)
// There are no working days after the last day
not PharmacyConsecutiveAssignmentStart(
pharmacy == $pharmacy,
shiftDateDayIndex > $firstDayIndexMinusOne
)
PharmacyRosterInfo(lastShiftDateDayIndex > $firstDayIndexMinusOne, $lastDayIndex : lastShiftDateDayIndex)
then
insertLogical(new PharmacyFreeSequence($pharmacy, $firstDayIndexMinusOne + 1, $lastDayIndex));
end
rule "insertEntirePharmacyFreeSequence"
salience 1 // Do these rules first (optional, for performance)
when
$pharmacy : Pharmacy()
// There are no working days after the last day
not PharmacyConsecutiveAssignmentStart(
pharmacy == $pharmacy
)
PharmacyRosterInfo($firstDayIndex : firstShiftDateDayIndex, $lastDayIndex : lastShiftDateDayIndex)
then
insertLogical(new PharmacyFreeSequence($pharmacy, $firstDayIndex, $lastDayIndex));
end
rule "insertPharmacyFreeSequence"
salience 1 // Do these rules first (optional, for performance)
when
PharmacyConsecutiveAssignmentEnd(
$pharmacy : pharmacy,
$firstDayIndexMinusOne : shiftDateDayIndex
)
PharmacyConsecutiveAssignmentStart(
pharmacy == $pharmacy,
shiftDateDayIndex > $firstDayIndexMinusOne,
$lastDayIndexPlusOne : shiftDateDayIndex
)
// There are no working days between the first and last day
not PharmacyConsecutiveAssignmentStart(
pharmacy == $pharmacy,
shiftDateDayIndex > $firstDayIndexMinusOne && < $lastDayIndexPlusOne
)
then
insertLogical(new PharmacyFreeSequence($pharmacy, $firstDayIndexMinusOne + 1, $lastDayIndexPlusOne - 1));
end
rule "Minimum and Maximum total assignments"
salience 1
no-loop
when
$contractLine : MinMaxContractLine(
contractLineType == ContractLineType.TOTAL_ASSIGNMENTS, maximumEnabled == true,
$contract : contract, $maximumValue : maximumValue
)
$pharmacy: Pharmacy(contract == $contract)
accumulate(
$assignment : ShiftAssignment($pharmacy == pharmacy);
$total : count($assignment)
)
then
int totalInt = $total.intValue();
if (totalInt < $contractLine.getMinimumValue()) {
scoreHolder.addSoftConstraintMatch(kcontext,
(totalInt - $contractLine.getMinimumValue()) * $contractLine.getMinimumWeight());
helperWithMessage(drools,"minimum total ass for "+$pharmacy.getName()+" is "+$contractLine.getMinimumValue()+" > "+totalInt);
} else if (totalInt > $contractLine.getMaximumValue()) {
scoreHolder.addSoftConstraintMatch(kcontext,
($contractLine.getMaximumValue() - totalInt) * $contractLine.getMaximumWeight());
helperWithMessage(drools,"maximum total ass for "+$pharmacy.getName()+" is "+$contractLine.getMaximumValue()+" < "+totalInt);
} else {
// Workaround for https://issues.redhat.com/browse/PLANNER-761
scoreHolder.addSoftConstraintMatch(kcontext, 0);
}
end
rule "Minimum and maximum number of day service assignments"
dialect "mvel"
when
$contractLine : MinMaxContractLine(contractLineType == ContractLineType.TOTAL_DAY_ASSIGNMENTS, enabled == true,
$contract : contract)
$pharmacy: Pharmacy(contract == $contract)
accumulate(
$assignment : ShiftAssignment($pharmacy == pharmacy,$shiftType:shift.getShiftType,$shiftType.toString=="DAY");
$total : count($assignment)
)
then
int totalInt = $total.intValue();
if ($contractLine.isMinimumEnabled() && totalInt < $contractLine.getMinimumValue()) {
scoreHolder.addSoftConstraintMatch(kcontext,
(totalInt - $contractLine.getMinimumValue()) * $contractLine.getMinimumWeight());
} else if ($contractLine.isMaximumEnabled() && totalInt > $contractLine.getMaximumValue()) {
scoreHolder.addSoftConstraintMatch(kcontext,
($contractLine.getMaximumValue() - totalInt) * $contractLine.getMaximumWeight());
} else {
// Workaround for https://issues.redhat.com/browse/PLANNER-761
scoreHolder.addSoftConstraintMatch(kcontext, 0);
}
end
rule "Minimum and maximum number of night service assignments"
dialect "mvel"
when
$contractLine : MinMaxContractLine(contractLineType == ContractLineType.TOTAL_NIGHT_ASSIGNMENTS, enabled == true,
$contract : contract)
$pharmacy: Pharmacy(contract == $contract)
$total : Number() from accumulate(
$assignment : ShiftAssignment($pharmacy == pharmacy,$shiftType:shift.getShiftType,$shiftType.toString=="NIGHT");
count($assignment)
)
then
int totalInt = $total.intValue();
if ($contractLine.isMinimumEnabled() && totalInt < $contractLine.getMinimumValue()) {
scoreHolder.addSoftConstraintMatch(kcontext,
(totalInt - $contractLine.getMinimumValue()) * $contractLine.getMinimumWeight());
} else if ($contractLine.isMaximumEnabled() && totalInt > $contractLine.getMaximumValue()) {
scoreHolder.addSoftConstraintMatch(kcontext,
($contractLine.getMaximumValue() - totalInt) * $contractLine.getMaximumWeight());
} else {
// Workaround for