Search code examples
javaspringspring-bootoptaplanner

Opraplanner uniform employees shifts planning


I have a problem with planning employees shifts where employees are distributed uniformly (randomly) across the shifts.

In my minimal example I use Spring boot, Lombock and Optaplanner spring boot starter (8.15.0.Final) package.

My minimal example in one file:

package com.example.planner;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.api.domain.lookup.PlanningId;
import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
import org.optaplanner.core.api.domain.solution.PlanningScore;
import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
import org.optaplanner.core.api.domain.variable.PlanningVariable;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
import org.optaplanner.core.api.score.stream.Constraint;
import org.optaplanner.core.api.score.stream.ConstraintFactory;
import org.optaplanner.core.api.score.stream.ConstraintProvider;
import org.optaplanner.core.api.solver.SolverManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.ArrayList;
import java.util.List;

@SpringBootApplication
public class PlannerApplication implements CommandLineRunner {

    @Autowired
    private SolverManager<Problem, Long> solverManager;

    public static void main(String[] args) {
        SpringApplication.run(PlannerApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        final var problem = new Problem(
                List.of(new Employee(1L), new Employee(2L), new Employee(3L)),
                List.of(new Shift(1L), new Shift(2L), new Shift(3L), new Shift(4L), new Shift(5L), new Shift(6L))
        );
        final var job = solverManager.solveAndListen(1L, id -> problem, bestSolution -> {
            for (final var shift : bestSolution.shifts) {
                System.err.println("Shift " + shift.id + ": Employee " + shift.employee.id);
            }
        });
    }

    @NoArgsConstructor
    public static class PlannerConstraintProvider implements ConstraintProvider {

        @Override
        public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
            return new Constraint[]{};
        }

    }

    @PlanningSolution
    @Data @NoArgsConstructor @AllArgsConstructor
    public static class Problem {
        @ValueRangeProvider(id = "employeeRange")
        @ProblemFactCollectionProperty
        private List<Employee> employees;
        @PlanningEntityCollectionProperty
        private List<Shift> shifts = new ArrayList<>(0);
        @PlanningScore
        private HardSoftScore score;

        public Problem(List<Employee> employees, List<Shift> shifts) {
            this.employees = employees;
            this.shifts = shifts;
        }
    }

    @Data @NoArgsConstructor @AllArgsConstructor
    public static class Employee {
        @PlanningId
        private Long id;
    }

    @PlanningEntity
    @Data @NoArgsConstructor @AllArgsConstructor
    public static class Shift {
        @PlanningId
        private Long id;
        @PlanningVariable(valueRangeProviderRefs = "employeeRange")
        private Employee employee;

        public Shift(Long id) {
            this.id = id;
        }
    }

}

Output of this example is:

Shift 1: Employee 1
Shift 2: Employee 1
Shift 3: Employee 1
Shift 4: Employee 1
Shift 5: Employee 1
Shift 6: Employee 1

Desired output is:

Shift 1: Employee 1
Shift 2: Employee 2
Shift 3: Employee 3
Shift 4: Employee 1
Shift 5: Employee 2
Shift 6: Employee 3

(or another uniform combinations)


Solution

  • You haven't defined any constraints, therefore OptaPlaner has no reason to come up with a better solution. You are not telling it what is better.

    OptaPlanner "thinks" this solution is the best possible because (I guess) it has a score of 0hard/0soft (you can check it in the console), which is an ideal score.

    To achieve the desired output you should define a fair workload distribution constraint that will penalize each employee with a square of its workload. Probably something like this should work in your case:

            @Override
            public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
                return new Constraint[] {
                        fairWorkloadDistribution(constraintFactory)
                };
            }
    
    ...
    
    Constraint fairWorkloadDistribution(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Shift.class)
                .groupBy(Shift::getEmployee, ConstraintCollectors.count())
                .penalize(
                        "Employee workload squared",
                        HardSoftScore.ONE_SOFT,
                        (employee, shifts) -> shifts * shifts);
    }