Brahim Lokaj
Brahim Lokaj

Reputation: 33

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

Upvotes: 3

Views: 305

Answers (2)

sudo
sudo

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

Moni
Moni

Reputation: 48

You should add this configuration in solver config file

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

Upvotes: 3

Related Questions