eOptaplanner: My custom timetable for school does not work

Viewed 32

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();
        }

    }
}


The result in json format

0 Answers
Related