All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.optaplanner.examples.nurserostering.solver.nurseRosteringConstraints.drl Maven / Gradle / Ivy

Go to download

OptaPlanner solves planning problems. This lightweight, embeddable planning engine implements powerful and scalable algorithms to optimize business resource scheduling and planning. This module contains the examples which demonstrate how to use it in a normal Java application.

There is a newer version: 9.44.0.Final
Show newest version
/*
 * Copyright 2010 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.optaplanner.examples.nurserostering.solver;
    dialect "java"

import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScoreHolder;

import java.time.DayOfWeek;
import org.optaplanner.examples.nurserostering.domain.Employee;
import org.optaplanner.examples.nurserostering.domain.ShiftAssignment;
import org.optaplanner.examples.nurserostering.domain.NurseRoster;
import org.optaplanner.examples.nurserostering.domain.NurseRosterParametrization;
import org.optaplanner.examples.nurserostering.domain.Shift;
import org.optaplanner.examples.nurserostering.domain.ShiftDate;
import org.optaplanner.examples.nurserostering.domain.ShiftType;
import org.optaplanner.examples.nurserostering.domain.ShiftTypeSkillRequirement;
import org.optaplanner.examples.nurserostering.domain.Skill;
import org.optaplanner.examples.nurserostering.domain.SkillProficiency;
import org.optaplanner.examples.nurserostering.domain.WeekendDefinition;
import org.optaplanner.examples.nurserostering.domain.pattern.FreeBefore2DaysWithAWorkDayPattern;
import org.optaplanner.examples.nurserostering.domain.pattern.Pattern;
import org.optaplanner.examples.nurserostering.domain.pattern.ShiftType2DaysPattern;
import org.optaplanner.examples.nurserostering.domain.pattern.ShiftType3DaysPattern;
import org.optaplanner.examples.nurserostering.domain.pattern.WorkBeforeFreeSequencePattern;
import org.optaplanner.examples.nurserostering.domain.contract.Contract;
import org.optaplanner.examples.nurserostering.domain.contract.BooleanContractLine;
import org.optaplanner.examples.nurserostering.domain.contract.ContractLine;
import org.optaplanner.examples.nurserostering.domain.contract.ContractLineType;
import org.optaplanner.examples.nurserostering.domain.contract.MinMaxContractLine;
import org.optaplanner.examples.nurserostering.domain.contract.PatternContractLine;
import org.optaplanner.examples.nurserostering.domain.request.DayOffRequest;
import org.optaplanner.examples.nurserostering.domain.request.DayOnRequest;
import org.optaplanner.examples.nurserostering.domain.request.ShiftOffRequest;
import org.optaplanner.examples.nurserostering.domain.request.ShiftOnRequest;
import org.optaplanner.examples.nurserostering.solver.drools.EmployeeConsecutiveAssignmentEnd;
import org.optaplanner.examples.nurserostering.solver.drools.EmployeeConsecutiveAssignmentStart;
import org.optaplanner.examples.nurserostering.solver.drools.EmployeeConsecutiveWeekendAssignmentEnd;
import org.optaplanner.examples.nurserostering.solver.drools.EmployeeConsecutiveWeekendAssignmentStart;
import org.optaplanner.examples.nurserostering.solver.drools.EmployeeFreeSequence;
import org.optaplanner.examples.nurserostering.solver.drools.EmployeeWeekendSequence;
import org.optaplanner.examples.nurserostering.solver.drools.EmployeeWorkSequence;

global HardSoftScoreHolder scoreHolder;

// ############################################################################
// Hard constraints
// ############################################################################

// A nurse can only work one shift per day, i.e. no two shift can be assigned to the same nurse on a day.
rule "oneShiftPerDay"
    when
        ShiftAssignment($leftId : id, $employee : employee, $shiftDate : shiftDate, employee != null)
        ShiftAssignment(employee == $employee, shiftDate == $shiftDate, id > $leftId)
    then
        scoreHolder.addHardConstraintMatch(kcontext, -1);
end

// ############################################################################
// Soft constraints
// ############################################################################

rule "Minimum and maximum number of assignments"
        salience 1 // Do these rules first (optional, for performance)
    when
        $contractLine : MinMaxContractLine(contractLineType == ContractLineType.TOTAL_ASSIGNMENTS, enabled == true,
            $contract : contract)
        $employee : Employee(contract == $contract)
        accumulate(
            $assignment : ShiftAssignment(employee == $employee);
            $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 "insertEmployeeConsecutiveAssignmentStart"
        salience 2 // Do these rules first (optional, for performance)
    when
        ShiftAssignment(
            $employee : employee, employee != null,
            $dayIndex : shiftDateDayIndex,
            $shiftDate : shiftDate
        )
        // The first day has no working day before it
        not ShiftAssignment(employee == $employee, shiftDateDayIndex == ($dayIndex - 1))
    then
        insertLogical(new EmployeeConsecutiveAssignmentStart($employee, $shiftDate));
end
rule "insertEmployeeConsecutiveAssignmentEnd"
        salience 2 // Do these rules first (optional, for performance)
    when
        ShiftAssignment(
            $employee : employee, employee != null,
            $dayIndex : shiftDateDayIndex,
            $shiftDate : shiftDate
        )
        // The last day has no working day after it
        not ShiftAssignment(employee == $employee, shiftDateDayIndex == ($dayIndex + 1))
    then
        insertLogical(new EmployeeConsecutiveAssignmentEnd($employee, $shiftDate));
end

rule "insertEmployeeWorkSequence"
        salience 1 // Do these rules first (optional, for performance)
    when
        EmployeeConsecutiveAssignmentStart(
            $employee : employee,
            $firstDayIndex : shiftDateDayIndex
        )

        EmployeeConsecutiveAssignmentEnd(
            employee == $employee,
            shiftDateDayIndex >= $firstDayIndex,
            $lastDayIndex : shiftDateDayIndex
        )

        // There are no free days between the first and last day
        not EmployeeConsecutiveAssignmentEnd(
            employee == $employee,
            shiftDateDayIndex >= $firstDayIndex && < $lastDayIndex
        )
    then
        insertLogical(new EmployeeWorkSequence($employee, $firstDayIndex, $lastDayIndex));
end

// Minimum number of consecutive working days
rule "minimumConsecutiveWorkingDays"
    when
        $contractLine : MinMaxContractLine(
            contractLineType == ContractLineType.CONSECUTIVE_WORKING_DAYS, minimumEnabled == true,
            $contract : contract, $minimumValue : minimumValue
        )

        EmployeeWorkSequence(
            getEmployee().getContract() == $contract,
            dayLength < $minimumValue,
            $dayLength : dayLength
        )
    then
        scoreHolder.addSoftConstraintMatch(kcontext, ($dayLength - $minimumValue) * $contractLine.getMinimumWeight());
end

// Maximum number of consecutive working days
rule "maximumConsecutiveWorkingDays"
    when
        $contractLine : MinMaxContractLine(
            contractLineType == ContractLineType.CONSECUTIVE_WORKING_DAYS, maximumEnabled == true,
            $contract : contract, $maximumValue : maximumValue
        )

        EmployeeWorkSequence(
            getEmployee().getContract() == $contract,
            dayLength > $maximumValue,
            $dayLength : dayLength
        )
    then
        scoreHolder.addSoftConstraintMatch(kcontext, ($maximumValue - $dayLength) * $contractLine.getMaximumWeight());
end


rule "insertEmployeeFreeSequence"
        salience 1 // Do these rules first (optional, for performance)
    when
        EmployeeConsecutiveAssignmentEnd(
            $employee : employee,
            $firstDayIndexMinusOne : shiftDateDayIndex
        )

        EmployeeConsecutiveAssignmentStart(
            employee == $employee,
            shiftDateDayIndex > $firstDayIndexMinusOne,
            $lastDayIndexPlusOne : shiftDateDayIndex
        )

        // There are no working days between the first and last day
        not EmployeeConsecutiveAssignmentStart(
            employee == $employee,
            shiftDateDayIndex > $firstDayIndexMinusOne && < $lastDayIndexPlusOne
        )
    then
        insertLogical(new EmployeeFreeSequence($employee, $firstDayIndexMinusOne + 1, $lastDayIndexPlusOne - 1));
end
rule "insertFirstEmployeeFreeSequence"
        salience 1 // Do these rules first (optional, for performance)
    when
        EmployeeConsecutiveAssignmentStart(
            $employee : employee,
            $lastDayIndexPlusOne : shiftDateDayIndex
        )

        // There are no working days before the first day
        not EmployeeConsecutiveAssignmentEnd(
            employee == $employee,
            shiftDateDayIndex < $lastDayIndexPlusOne
        )
        NurseRosterParametrization(firstShiftDateDayIndex < $lastDayIndexPlusOne, $firstDayIndex : firstShiftDateDayIndex)
    then
        insertLogical(new EmployeeFreeSequence($employee, $firstDayIndex, $lastDayIndexPlusOne - 1));
end
rule "insertLastEmployeeFreeSequence"
        salience 1 // Do these rules first (optional, for performance)
    when
        EmployeeConsecutiveAssignmentEnd(
            $employee : employee,
            $firstDayIndexMinusOne : shiftDateDayIndex
        )

        // There are no working days after the last day
        not EmployeeConsecutiveAssignmentStart(
            employee == $employee,
            shiftDateDayIndex > $firstDayIndexMinusOne
        )
        NurseRosterParametrization(lastShiftDateDayIndex > $firstDayIndexMinusOne, $lastDayIndex : lastShiftDateDayIndex)
    then
        insertLogical(new EmployeeFreeSequence($employee, $firstDayIndexMinusOne + 1, $lastDayIndex));
end
rule "insertEntireEmployeeFreeSequence"
        salience 1 // Do these rules first (optional, for performance)
    when
        $employee : Employee()
        // There are no working days at all
        not EmployeeConsecutiveAssignmentStart(
            employee == $employee
        )
        NurseRosterParametrization($firstDayIndex : firstShiftDateDayIndex, $lastDayIndex : lastShiftDateDayIndex)
    then
        insertLogical(new EmployeeFreeSequence($employee, $firstDayIndex, $lastDayIndex));
end

// Minimum number of consecutive free days
rule "minimumConsecutiveFreeDays"
    when
        $contractLine : MinMaxContractLine(
            contractLineType == ContractLineType.CONSECUTIVE_FREE_DAYS, minimumEnabled == true,
            $contract : contract, $minimumValue : minimumValue
        )

        EmployeeFreeSequence(
            getEmployee().getContract() == $contract,
            dayLength < $minimumValue,
            $dayLength : dayLength
        )
    then
        scoreHolder.addSoftConstraintMatch(kcontext, ($dayLength - $minimumValue) * $contractLine.getMinimumWeight());
end

// Maximum number of consecutive free days
rule "maximumConsecutiveFreeDays"
    when
        $contractLine : MinMaxContractLine(
            contractLineType == ContractLineType.CONSECUTIVE_FREE_DAYS, maximumEnabled == true,
            $contract : contract, $maximumValue : maximumValue
        )

        EmployeeFreeSequence(
            getEmployee().getContract() == $contract,
            dayLength > $maximumValue,
            $dayLength : dayLength
        )
    then
        scoreHolder.addSoftConstraintMatch(kcontext, ($maximumValue - $dayLength) * $contractLine.getMaximumWeight());
end


rule "insertEmployeeConsecutiveWeekendAssignmentStart"
        salience 2 // Do these rules first (optional, for performance)
    when
        ShiftAssignment(
            weekend == true,
            $employee : employee, employee != null,
            $weekendSundayIndex : weekendSundayIndex
        )
        // The first working weekend has no working weekend before it
        not ShiftAssignment(
            weekend == true,
            employee == $employee,
            weekendSundayIndex == ($weekendSundayIndex - 7)
        )
    then
        insertLogical(new EmployeeConsecutiveWeekendAssignmentStart($employee, $weekendSundayIndex));
end
rule "insertEmployeeConsecutiveWeekendAssignmentEnd"
        salience 2 // Do these rules first (optional, for performance)
    when
        ShiftAssignment(
            weekend == true,
            $employee : employee, employee != null,
            $weekendSundayIndex : weekendSundayIndex
        )
        // The last working weekend has no working weekend after it
        not ShiftAssignment(
            weekend == true,
            employee == $employee,
            weekendSundayIndex == ($weekendSundayIndex + 7)
        )
    then
        insertLogical(new EmployeeConsecutiveWeekendAssignmentEnd($employee, $weekendSundayIndex));
end

rule "insertEmployeeWeekendSequence"
    when
        EmployeeConsecutiveWeekendAssignmentStart(
            $employee : employee,
            $firstSundayIndex : sundayIndex
        )

        EmployeeConsecutiveWeekendAssignmentEnd(
            employee == $employee,
            sundayIndex >= $firstSundayIndex,
            $lastSundayIndex : sundayIndex
        )

        // There are no free weekends between the first and last weekend
        not EmployeeConsecutiveWeekendAssignmentEnd(
            employee == $employee,
            sundayIndex >= $firstSundayIndex && < $lastSundayIndex
        )
    then
        insertLogical(new EmployeeWeekendSequence($employee, $firstSundayIndex, $lastSundayIndex));
end

// Minimum number of consecutive working weekends
rule "minimumConsecutiveWorkingWeekends"
    when
        $contractLine : MinMaxContractLine(
            contractLineType == ContractLineType.CONSECUTIVE_WORKING_WEEKENDS, minimumEnabled == true,
            $contract : contract, $minimumValue : minimumValue
        )

        EmployeeWeekendSequence(
            getEmployee().getContract() == $contract,
            weekendLength < $minimumValue,
            $weekendLength : weekendLength
        )
    then
        scoreHolder.addSoftConstraintMatch(kcontext,
                ($weekendLength - $minimumValue) * $contractLine.getMinimumWeight());
end

// Maximum number of consecutive working weekends
rule "maximumConsecutiveWorkingWeekends"
    when
        $contractLine : MinMaxContractLine(
            contractLineType == ContractLineType.CONSECUTIVE_WORKING_WEEKENDS, maximumEnabled == true,
            $contract : contract, $maximumValue : maximumValue
        )

        EmployeeWeekendSequence(
            getEmployee().getContract() == $contract,
            weekendLength > $maximumValue,
            $weekendLength : weekendLength
        )
    then
        scoreHolder.addSoftConstraintMatch(kcontext,
                ($maximumValue - $weekendLength) * $contractLine.getMaximumWeight());
end


// Complete weekends
rule "startOnNotFirstDayOfWeekend"
    when
        $contractLine : BooleanContractLine(
            contractLineType == ContractLineType.COMPLETE_WEEKENDS, enabled == true,
            $contract : contract
        )
        EmployeeConsecutiveAssignmentStart(
            weekendAndNotFirstDayOfWeekend == true,
            contract == $contract,
            $distanceToFirstDayOfWeekend : distanceToFirstDayOfWeekend
        )
    then
        scoreHolder.addSoftConstraintMatch(kcontext, - $distanceToFirstDayOfWeekend * $contractLine.getWeight());
end
rule "endOnNotLastDayOfWeekend"
    when
        $contractLine : BooleanContractLine(
            contractLineType == ContractLineType.COMPLETE_WEEKENDS, enabled == true,
            $contract : contract
        )
        EmployeeConsecutiveAssignmentEnd(
            weekendAndNotLastDayOfWeekend == true,
            contract == $contract,
            $distanceToLastDayOfWeekend : distanceToLastDayOfWeekend
        )
    then
        scoreHolder.addSoftConstraintMatch(kcontext, - $distanceToLastDayOfWeekend * $contractLine.getWeight());
end

// Identical shiftTypes during a weekend
rule "identicalShiftTypesDuringWeekend"
    when
        $contractLine : BooleanContractLine(contractLineType == ContractLineType.IDENTICAL_SHIFT_TYPES_DURING_WEEKEND,
            enabled == true, $contract : contract)
        $employee : Employee(contract == $contract, $weekendLength : weekendLength)
        ShiftDate(dayOfWeek == DayOfWeek.SUNDAY, $sundayIndex : dayIndex)
        $shiftType : ShiftType()
        accumulate(
            $assignment : ShiftAssignment(
                weekend == true,
                weekendSundayIndex == $sundayIndex,
                employee == $employee,
                shiftType == $shiftType);
            $weekendAssignmentTotal : count($assignment);
            $weekendAssignmentTotal > 0 && $weekendAssignmentTotal < $weekendLength
        )
    then
        scoreHolder.addSoftConstraintMatch(kcontext,
                ($weekendAssignmentTotal.intValue() - $weekendLength) * $contractLine.getWeight());
end

// Requested day on/off
rule "dayOffRequest"
    when
        DayOffRequest($employee : employee, $shiftDate : shiftDate, $weight : weight)
        ShiftAssignment(employee == $employee, shiftDate == $shiftDate)
    then
        scoreHolder.addSoftConstraintMatch(kcontext, - $weight);
end
rule "dayOnRequest"
    when
        DayOnRequest($employee : employee, $shiftDate : shiftDate, $weight : weight)
        not ShiftAssignment(employee == $employee, shiftDate == $shiftDate)
    then
        scoreHolder.addSoftConstraintMatch(kcontext, - $weight);
end

// Requested shift on/off
rule "shiftOffRequest"
    when
        ShiftOffRequest($employee : employee, $shift : shift, $weight : weight)
        ShiftAssignment(employee == $employee, shift == $shift)
    then
        scoreHolder.addSoftConstraintMatch(kcontext, - $weight);
end
rule "shiftOnRequest"
    when
        ShiftOnRequest($employee : employee, $shift : shift, $weight : weight)
        not ShiftAssignment(employee == $employee, shift == $shift)
    then
        scoreHolder.addSoftConstraintMatch(kcontext, - $weight);
end

// Alternative skill
rule "alternativeSkill"
    when
        $contractLine : BooleanContractLine(contractLineType == ContractLineType.ALTERNATIVE_SKILL_CATEGORY,
            $contract : contract)
        ShiftAssignment(contract == $contract, $employee : employee, $shiftType : shiftType)
        ShiftTypeSkillRequirement(shiftType == $shiftType, $skill : skill)
        not SkillProficiency(employee == $employee, skill == $skill)
    then
        scoreHolder.addSoftConstraintMatch(kcontext, - $contractLine.getWeight());
end

// Unwanted patterns
rule "unwantedPatternFreeBefore2DaysWithAWorkDayPattern"
    when
        $pattern : FreeBefore2DaysWithAWorkDayPattern(
            $freeDayOfWeek : freeDayOfWeek
        )
        PatternContractLine(
            pattern == $pattern, $contract : contract
        )
        ShiftDate(dayOfWeek == $freeDayOfWeek, $freeDayIndex : dayIndex)
        $employee : Employee(contract == $contract)

        not ShiftAssignment(
            employee == $employee,
            shiftDateDayIndex == $freeDayIndex
        )
        exists ShiftAssignment(
            employee == $employee,
            shiftDateDayIndex == ($freeDayIndex + 1) || shiftDateDayIndex == ($freeDayIndex + 2)
        )
    then
        scoreHolder.addSoftConstraintMatch(kcontext, - $pattern.getWeight());
end

// TODO support WorkBeforeFreeSequencePattern too (not needed for competition)
//rule "unwantedPatternWorkBeforeFreeSequencePattern"
//    when
//        $pattern : WorkBeforeFreeSequencePattern(
//            $workDayOfWeek : workDayOfWeek,
//            $workShiftType : workShiftType,
//            $freeDayLength : freeDayLength
//        )
//        PatternContractLine(
//            pattern == $pattern, $contract : contract
//        )
//
//        ShiftAssignment(
//            ($workDayOfWeek == null) || (shiftDateDayOfWeek == $workDayOfWeek),
//            ($workShiftType == null) || (shiftType == $workShiftType),
//            contract == $contract,
//            $employee : employee, $workDayIndex : shiftDateDayIndex
//        )
//        EmployeeFreeSequence(
//            employee == $employee,
//            firstDayIndex == ($workDayIndex + 1),
//            dayLength >= $freeDayLength
//        )
//    then
//        scoreHolder.addSoftConstraintMatch(kcontext, - $pattern.getWeight());
//end
rule "unwantedPatternShiftType2DaysPattern"
    when
        $pattern : ShiftType2DaysPattern(
            $dayIndex0ShiftType : dayIndex0ShiftType,
            $dayIndex1ShiftType : dayIndex1ShiftType
        )
        PatternContractLine(
            pattern == $pattern, $contract : contract
        )

        ShiftAssignment(
            shiftType == $dayIndex0ShiftType,
            contract == $contract,
            $employee : employee, $firstDayIndex : shiftDateDayIndex
        )
        ShiftAssignment(
            ($dayIndex1ShiftType == null) || (shiftType == $dayIndex1ShiftType),
            employee == $employee,
            shiftDateDayIndex == ($firstDayIndex + 1)
        )
    then
        scoreHolder.addSoftConstraintMatch(kcontext, - $pattern.getWeight());
end
rule "unwantedPatternShiftType3DaysPattern"
    when
        $pattern : ShiftType3DaysPattern(
            $dayIndex0ShiftType : dayIndex0ShiftType,
            $dayIndex1ShiftType : dayIndex1ShiftType,
            $dayIndex2ShiftType : dayIndex2ShiftType
        )
        PatternContractLine(
            pattern == $pattern, $contract : contract
        )

        ShiftAssignment(
            shiftType == $dayIndex0ShiftType,
            contract == $contract,
            $employee : employee, $firstDayIndex : shiftDateDayIndex
        )
        ShiftAssignment(
            shiftType == $dayIndex1ShiftType,
            employee == $employee,
            shiftDateDayIndex == ($firstDayIndex + 1)
        )
        ShiftAssignment(
            shiftType == $dayIndex2ShiftType,
            employee == $employee,
            shiftDateDayIndex == ($firstDayIndex + 2)
        )
    then
        scoreHolder.addSoftConstraintMatch(kcontext, - $pattern.getWeight());
end




© 2015 - 2024 Weber Informatics LLC | Privacy Policy