Search code examples
javadroolsoptaplanner

Equal distribution of Schedules


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

Solution

  • You should add this configuration in solver config file

    <localSearch>
      <unionMoveSelector>
        <changeMoveSelector>
          <selectionOrder>RANDOM</selectionOrder>
        </changeMoveSelector>
      </unionMoveSelector>
    </localSearch>