I am a beginner with Optaplanner. I'm trying to implement a timetable management for a school with Spring Boot. The principle is exactly the same as the example in the Quickstart (https://github.com/kiegroup/optaplanner-quickstarts/tree/stable/use-cases/school-timetabling) but without the Room entity and with a Teacher. Each Class has several Lesson taught by Teachers. And each Teacher has one or more availability (Timeslot list). So the constraint that differentiates from the Quickstarts example is this: Teachers prefer to teach when they are available. But when I run my code, it gives a distorted result. So I wonder why it doesn't work? Thanks in advance!
This is my code.
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@PlanningEntity
@Entity
@Table(name = "lesson")
public class Lesson {
@PlanningId
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String name;
private int hours;
@ManyToOne
@JoinColumn(name = "teacher_id")
private Teacher teacher;
@ManyToOne
private Classe classe;
@PlanningVariable(valueRangeProviderRefs = "timeslotRange")
@ManyToOne
private Timeslot timeslot;
}
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name="classe",uniqueConstraints = @UniqueConstraint(columnNames = {"name"}))
public class Classe {
@PlanningId
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String name;
}
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name = "teacher")
public class Teacher {
@PlanningId
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String name;
@ManyToMany
private List<Timeslot> preferences;
@Column(columnDefinition = "integer default 0")
private int hours;
}
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"dayOfWeek", "startTime", "endTime"}))
public class Timeslot {
@PlanningId
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
private DayOfWeek dayOfWeek;
@NotNull
@JsonFormat(pattern="HH:mm:ss")
private LocalTime startTime;
@NotNull
@JsonFormat(pattern="HH:mm:ss")
private LocalTime endTime;
private Timeslot() {
}
public Timeslot(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
this.dayOfWeek = dayOfWeek;
this.startTime = startTime;
this.endTime = endTime;
}
@Override
public String toString() {
return dayOfWeek + " " + startTime;
}
// ************************************************************************
// Getters and setters
// ************************************************************************
public Long getId() {
return id;
}
public DayOfWeek getDayOfWeek() {
return dayOfWeek;
}
public LocalTime getStartTime() {
return startTime;
}
public LocalTime getEndTime() {
return endTime;
}
}
@PlanningSolution
public class TimeTable {
@PlanningEntityCollectionProperty
private List<Lesson> lessonList;
@ValueRangeProvider(id = "timeslotRange")
@ProblemFactCollectionProperty
private List<Timeslot> timeslotList;
@ValueRangeProvider(id = "classeRange")
@ProblemFactCollectionProperty
private List<Classe> classeList;
@ValueRangeProvider(id = "teacherRange")
@ProblemFactCollectionProperty
private List<Teacher> teacherList;
@PlanningScore
private HardSoftScore score;
public TimeTable(List<Timeslot> timeslotList, List<Classe> classeList, List<Lesson> lessonList, List<Teacher> teacherList) {
this.timeslotList = timeslotList;
this.classeList = classeList;
this.lessonList = lessonList;
this.teacherList = teacherList;
}
}
public class TimeTableConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[]{
// Hard constraints
teacherConflict(constraintFactory),
classeConflict(constraintFactory),
teacherWorkingDayPreferences(constraintFactory),
teacherUndesiredDayWorking(constraintFactory),
// Soft constraints
classLessonVariety(constraintFactory),
teacherTimeEfficiency(constraintFactory),
};
}
Constraint classeConflict(ConstraintFactory constraintFactory) {
return constraintFactory
.forEachUniquePair(Lesson.class,
Joiners.equal(Lesson::getClasse),
Joiners.equal(Lesson::getTimeslot)
)
.penalize("Room conflict", HardSoftScore.ONE_HARD);
}
Constraint teacherConflict(ConstraintFactory constraintFactory) {
// A teacher can teach at most one lesson at the same time.
return constraintFactory
.forEachUniquePair(Lesson.class,
Joiners.equal(Lesson::getTeacher),
Joiners.equal(Lesson::getTimeslot)
)
.penalize("Teacher conflict", HardSoftScore.ONE_HARD);
}
Constraint teacherTimeEfficiency(ConstraintFactory constraintFactory) {
// A teacher prefers to teach sequential lessons and dislikes gaps between lessons.
return constraintFactory
.forEach(Lesson.class)
.join(
Lesson.class,
Joiners.equal(Lesson::getClasse),
Joiners.equal(Lesson::getTeacher),
Joiners.equal((lesson) -> lesson.getTimeslot().getDayOfWeek()))
.filter((lesson1, lesson2) -> {
Duration between = Duration.between(lesson1.getTimeslot().getEndTime(),
lesson2.getTimeslot().getStartTime());
return !between.isNegative() && between.compareTo(Duration.ofMinutes(30)) <= 0;
})
.reward("Teacher time efficiency", HardSoftScore.ONE_SOFT);
}
Constraint classLessonVariety(ConstraintFactory constraintFactory) {
// A student group dislikes sequential lessons on the same subject.
return constraintFactory
.forEach(Lesson.class)
.join(Lesson.class,
Joiners.equal(Lesson::getName),
Joiners.equal(Lesson::getClasse),
Joiners.equal((lesson) -> lesson.getTimeslot().getDayOfWeek()))
.filter((lesson1, lesson2) -> {
Duration between = Duration.between(lesson1.getTimeslot().getEndTime(),
lesson2.getTimeslot().getStartTime());
return !between.isNegative() && between.compareTo(Duration.ofMinutes(30)) <= 0;
})
.penalize("Student group subject variety", HardSoftScore.ONE_SOFT);
}
Constraint teacherWorkingDayPreferences(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Lesson.class)
.filter((lesson) -> lesson.getTeacher().getPreferences().contains(lesson.getTimeslot()))
.reward("Teacher working day preferences", HardSoftScore.ONE_HARD);
}
Constraint teacherUndesiredDayWorking(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Lesson.class)
.filter((lesson) -> !lesson.getTeacher().getPreferences().contains(lesson.getTimeslot()))
.penalize("Teacher undesired day working", HardSoftScore.ONE_HARD);
}
}
@CrossOrigin(origins = "*", allowedHeaders = "*")
@RestController
@RequestMapping("/timeTable")
public class TimeTableResource {
@Autowired
private TimeTableRepository timeTableRepository;
@Autowired
private SolverManager<TimeTable, Long> solverManager;
@Autowired
private ScoreManager<TimeTable, HardSoftScore> scoreManager;
// private SolverManager<TimeTable, UUID> solverManager;
@GetMapping("/solve")
public ResponseEntity<TimeTable> solve() {
UUID problemId = UUID.randomUUID();
TimeTable problem = timeTableRepository.findById(TimeTableRepository.SINGLETON_TIME_TABLE_ID);
// Submit the problem to start solving
SolverJob<TimeTable, Long> solverJob = solverManager.solve(1L, problem);
TimeTable solution;
try {
// Wait until the solving ends
solution = solverJob.getFinalBestSolution();
} catch (InterruptedException | ExecutionException e) {
throw new IllegalStateException("Solving failed.", e);
}
// printTimeTable(solution);
System.out.println(scoreManager.getSummary(solution));
return new ResponseEntity<>(solution, HttpStatus.OK);
}
private void printTimeTable(TimeTable timeTable){
Map<DayOfWeek, List<Lesson>> lessons = timeTable.getLessonList()
.stream()
// .filter(l -> l.getClasse().getId() == 1L)
.collect(Collectors.groupingBy(l -> l.getTimeslot().getDayOfWeek()));
try {
ObjectMapper objectMapper =
new ObjectMapper().registerModule(new JavaTimeModule())
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(lessons));
} catch (Exception e) {
e.printStackTrace();
}
}
}