0

I'm working on an employee rostering system using OptaPlanner, and I'm facing a scenario where I need to ensure that priority 1 shifts are filled before priority 2 shifts. The shifts are identified by a priority field in the Shift class, which serves as the planning entity.

Here's what I'm trying to achieve:

  • Priority 1 shifts must be assigned first, and they have higher importance.
  • Only after all priority 1 shifts are assigned should the solver consider filling priority 2 shifts. Could someone guide me on how to set up the OptaPlanner configuration and constraints to achieve this behavior? I've tried a few approaches, but I'm not sure how to make sure the solver respects this priority order when assigning shifts.

Any help, sample code snippets, or guidance on how to structure my constraint configuration for this scenario would be greatly appreciated. Thank you in advance for your assistance!

OptaPlanner Version: 8.29.0.Final

I have tried created a constraint like this

Constraint constraint = constraintFactory.forEach(Shift.class)
                .groupBy(shift -> shift.getWorksite(), ConstraintCollectors.toList())
                .filter((worksite, shiftList) -> filterAlternateShifts(shiftList)).penalizeConfigurable((u, s) -> 20)
                .asConstraint(CONSTRAINT_ALTERNATES_SHOULD_NOT_BE_ASSIGNED_BEFORE_SCHEDULED);

with the filter method defined as:

    private boolean filterAlternateShifts(List<Shift> shiftList) {
        List<Shift> alternates = shiftList.stream().filter(s -> s.getRank().equals(SchedulingConstants.ALTERNATE_SHIFT))
                .collect(Collectors.toList());
        List<Shift> scheduled = shiftList.stream().filter(s -> s.getRank().equals(SchedulingConstants.SCHEDULED_SHIFT))
                .collect(Collectors.toList());

        boolean unassignedScheduledShiftExists = scheduled.stream().anyMatch(s -> s.getClinician() == null);

        boolean assignedAlternateExists = alternates.stream().anyMatch(s -> s.getClinician() != null);

        return unassignedScheduledShiftExists && assignedAlternateExists;

    }

But this is not working. I have tried another approach similar to the already defined assignEveryShift constraint


    Constraint assignEveryShift(ConstraintFactory constraintFactory) {
        LOG.trace(SchedulingConstants.METHOD_ENTRY);
        Constraint constraint = constraintFactory.forEachIncludingNullVars(Shift.class)
                .filter(shift -> shift.getClinician() == null).penalizeConfigurable()
                .asConstraint(CONSTRAINT_ASSIGN_EVERY_SHIFT);

        LOG.trace(SchedulingConstants.METHOD_EXIT);
        return constraint;
    }

    Constraint prioritizeScheduledRankShift(ConstraintFactory constraintFactory) {
        LOG.trace(SchedulingConstants.METHOD_ENTRY);
        Constraint constraint = constraintFactory.forEachIncludingNullVars(Shift.class).filter(
                shift -> shift.getClinician() == null && shift.getRank().equals(SchedulingConstants.SCHEDULED_SHIFT))
                .penalizeConfigurable().asConstraint(CONSTRAINT_SHIFT_ALLOCATION_PREFERENCE_TO_SCHEDULED_RANK);

        LOG.trace(SchedulingConstants.METHOD_EXIT);
        return constraint;
    }

The difference being i have used a hard score to penalize it. This is working to an extend but not on all occasions and I am scared that it can break something.

1 Answers1

0

In Timefold, you can not really say that the solver should only work on a constraint after it is finished with another constraint. The solver is never finished with any constraint, and it always evaluates all of them together.

The way of getting your priority is penalizing the less important shifts more. Either on a lower score level, or on the same score level but by larger amounts; maybe even significantly higher amounts, 10x, 100x, x^2... The solver will work to improve the score and as it finds solutions with better and better scores, the likelihood of accomplishing your desired goal is increasing.

In this case, I recommend making the higher-priority shifts a hard constraint, as I'm assuming unfilled higher-priority shifts are a problem and therefore the solution is not feasible.

But you need to understand one thing most of all - with local search, there are no guarantees that you will find any particular solution. If the solver can only find solutions that break hard constraints, then that's what you'll get. Quality of the resulting solution will be a factor of move diversity, score calculation speed and time spent solving. The higher you can push those variables, the better your solution quality will be.

Lukáš Petrovický
  • 3,945
  • 1
  • 11
  • 20
  • Thanks Lukas, I understood the point and I would like to ask if there is any other way to do it other than using the constraints. Like maybe I will initially provide only the priority 1 shifts to the solver and then trigger another solve with a new list of priority 2 shifts when it gets completed. – sicario_23 Aug 11 '23 at 18:20
  • You could do it like that, but I wouldn't. The guarantee you're looking for simply doesn't exist - even if there are no P2 shifts, the solver still may not find a way to satisfy all P1 shifts. You are chasing something which you may never get and in fact if you include the P2 shifts, the likelihood of getting your ideal outcome is actually higher. Because with all those extra shifts, there will be a larger amount of different solutions for the solver to look at, with possibly less score traps. – Lukáš Petrovický Aug 11 '23 at 18:25