Reputation: 713
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)
Upvotes: 0
Views: 249
Reputation: 2358
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);
}
Upvotes: 2
Reputation: 5682
You have no constraints. You have not told OptaPlanner what to optimize for, and therefore all solutions are equally favorable to OptaPlanner.
(In fact, I am quite surprised that this code does not fail. A situation with no constraints should have thrown an exception.)
Upvotes: 0