Chapter 13. Red Hat build of OptaPlanner and Java: a school timetable quickstart guide
Chapter 13. Red Hat build of OptaPlanner and Java: a school timetable quickstart guide This guide walks you through the process of creating a simple Java application with OptaPlanner's constraint solving artificial intelligence (AI). You will build a command-line application that optimizes a school timetable for students and teachers: Your application will assign Lesson instances to Timeslot and Room instances automatically by using AI to adhere to hard and soft scheduling constraints , for example: A room can have at most one lesson at the same time. A teacher can teach at most one lesson at the same time. A student can attend at most one lesson at the same time. A teacher prefers to teach all lessons in the same room. A teacher prefers to teach sequential lessons and dislikes gaps between lessons. A student dislikes sequential lessons on the same subject. Mathematically speaking, school timetabling is an NP-hard problem. This means it is difficult to scale. Simply brute force iterating through all possible combinations takes millions of years for a non-trivial data set, even on a supercomputer. Fortunately, AI constraint solvers such as OptaPlanner have advanced algorithms that deliver a near-optimal solution in a reasonable amount of time. Prerequisites OpenJDK (JDK) 11 is installed. Red Hat build of Open JDK is available from the Software Downloads page in the Red Hat Customer Portal (login required). Apache Maven 3.6 or higher is installed. Maven is available from the Apache Maven Project website. An IDE, such as IntelliJ IDEA , VS Code or Eclipse 13.1. Create the Maven or Gradle build file and add dependencies You can use Maven or Gradle for the OptaPlanner school timetable application. After you create the build files, add the following dependencies: optaplanner-core (compile scope) to solve the school timetable problem optaplanner-test (test scope) to JUnit test the school timetabling constraints An implementation such as logback-classic (runtime scope) to view the steps that OptaPlanner takes Procedure Create the Maven or Gradle build file. Add optaplanner-core , optaplanner-test , and logback-classic dependencies to your build file: For Maven, add the following dependencies to the pom.xml file: The following example shows the complete pom.xml file. <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.acme</groupId> <artifactId>optaplanner-hello-world-school-timetabling-quickstart</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.release>11</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <version.org.optaplanner>8.13.0.Final-redhat-00013</version.org.optaplanner> <version.org.logback>1.2.3</version.org.logback> <version.compiler.plugin>3.8.1</version.compiler.plugin> <version.surefire.plugin>3.0.0-M5</version.surefire.plugin> <version.exec.plugin>3.0.0</version.exec.plugin> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.optaplanner</groupId> <artifactId>optaplanner-bom</artifactId> <version>USD{version.org.optaplanner}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>USD{version.org.logback}</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.optaplanner</groupId> <artifactId>optaplanner-core</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <scope>runtime</scope> </dependency> <!-- Testing --> <dependency> <groupId>org.optaplanner</groupId> <artifactId>optaplanner-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>USD{version.compiler.plugin}</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>USD{version.surefire.plugin}</version> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>USD{version.exec.plugin}</version> <configuration> <mainClass>org.acme.schooltimetabling.TimeTableApp</mainClass> </configuration> </plugin> </plugins> </build> <repositories> <repository> <id>jboss-public-repository-group</id> <url>https://repository.jboss.org/nexus/content/groups/public/</url> <releases> <!-- Get releases only from Maven Central which is faster. --> <enabled>false</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> </project> For Gradle, add the following dependencies to the gradle.build file: The following example shows the completed gradle.build file. plugins { id "java" id "application" } def optaplannerVersion = "{project-version}" def logbackVersion = "1.2.3" group = "org.acme" version = "0.1.0-SNAPSHOT" repositories { mavenCentral() } dependencies { implementation "org.optaplanner:optaplanner-core:USD{optaplannerVersion}" runtimeOnly "ch.qos.logback:logback-classic:USD{logbackVersion}" testImplementation "org.optaplanner:optaplanner-test:USD{optaplannerVersion}" } java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } compileJava { options.encoding = "UTF-8" options.compilerArgs << "-parameters" } compileTestJava { options.encoding = "UTF-8" } application { mainClass = "org.acme.schooltimetabling.TimeTableApp" } test { // Log the test execution results. testLogging { events "passed", "skipped", "failed" } } 13.2. Model the domain objects The goal of the Red Hat build of OptaPlanner timetable project is to assign each lesson to a time slot and a room. To do this, add three classes, Timeslot , Lesson , and Room , as shown in the following diagram: Timeslot The Timeslot class represents a time interval when lessons are taught, for example, Monday 10:30 - 11:30 or Tuesday 13:30 - 14:30 . In this example, all time slots have the same duration and there are no time slots during lunch or other breaks. A time slot has no date because a high school schedule just repeats every week. There is no need for continuous planning . A timeslot is called a problem fact because no Timeslot instances change during solving. Such classes do not require any OptaPlanner-specific annotations. Room The Room class represents a location where lessons are taught, for example, Room A or Room B . In this example, all rooms are without capacity limits and they can accommodate all lessons. Room instances do not change during solving so Room is also a problem fact . Lesson During a lesson, represented by the Lesson class, a teacher teaches a subject to a group of students, for example, Math by A.Turing for 9th grade or Chemistry by M.Curie for 10th grade . If a subject is taught multiple times each week by the same teacher to the same student group, there are multiple Lesson instances that are only distinguishable by id . For example, the 9th grade has six math lessons a week. During solving, OptaPlanner changes the timeslot and room fields of the Lesson class to assign each lesson to a time slot and a room. Because OptaPlanner changes these fields, Lesson is a planning entity : Most of the fields in the diagram contain input data, except for the orange fields. A lesson's timeslot and room fields are unassigned ( null ) in the input data and assigned (not null ) in the output data. OptaPlanner changes these fields during solving. Such fields are called planning variables. In order for OptaPlanner to recognize them, both the timeslot and room fields require an @PlanningVariable annotation. Their containing class, Lesson , requires an @PlanningEntity annotation. Procedure Create the src/main/java/com/example/domain/Timeslot.java class: package com.example.domain; import java.time.DayOfWeek; import java.time.LocalTime; public class Timeslot { private DayOfWeek dayOfWeek; private LocalTime startTime; 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.toString(); } // ******************************** // Getters and setters // ******************************** public DayOfWeek getDayOfWeek() { return dayOfWeek; } public LocalTime getStartTime() { return startTime; } public LocalTime getEndTime() { return endTime; } } Notice the toString() method keeps the output short so it is easier to read OptaPlanner's DEBUG or TRACE log, as shown later. Create the src/main/java/com/example/domain/Room.java class: package com.example.domain; public class Room { private String name; private Room() { } public Room(String name) { this.name = name; } @Override public String toString() { return name; } // ******************************** // Getters and setters // ******************************** public String getName() { return name; } } Create the src/main/java/com/example/domain/Lesson.java class: package com.example.domain; import org.optaplanner.core.api.domain.entity.PlanningEntity; import org.optaplanner.core.api.domain.variable.PlanningVariable; @PlanningEntity public class Lesson { private Long id; private String subject; private String teacher; private String studentGroup; @PlanningVariable(valueRangeProviderRefs = "timeslotRange") private Timeslot timeslot; @PlanningVariable(valueRangeProviderRefs = "roomRange") private Room room; private Lesson() { } public Lesson(Long id, String subject, String teacher, String studentGroup) { this.id = id; this.subject = subject; this.teacher = teacher; this.studentGroup = studentGroup; } @Override public String toString() { return subject + "(" + id + ")"; } // ******************************** // Getters and setters // ******************************** public Long getId() { return id; } public String getSubject() { return subject; } public String getTeacher() { return teacher; } public String getStudentGroup() { return studentGroup; } public Timeslot getTimeslot() { return timeslot; } public void setTimeslot(Timeslot timeslot) { this.timeslot = timeslot; } public Room getRoom() { return room; } public void setRoom(Room room) { this.room = room; } } The Lesson class has an @PlanningEntity annotation, so OptaPlanner knows that this class changes during solving because it contains one or more planning variables. The timeslot field has an @PlanningVariable annotation, so OptaPlanner knows that it can change its value. In order to find potential Timeslot instances to assign to this field, OptaPlanner uses the valueRangeProviderRefs property to connect to a value range provider that provides a List<Timeslot> to pick from. See Section 13.4, "Gather the domain objects in a planning solution" for information about value range providers. The room field also has an @PlanningVariable annotation for the same reasons. 13.3. Define the constraints and calculate the score When solving a problem, a score represents the quality of a specific solution. The higher the score the better. Red Hat build of OptaPlanner looks for the best solution, which is the solution with the highest score found in the available time. It might be the optimal solution. Because the timetable example use case has hard and soft constraints, use the HardSoftScore class to represent the score: Hard constraints must not be broken. For example: A room can have at most one lesson at the same time. Soft constraints should not be broken. For example: A teacher prefers to teach in a single room. Hard constraints are weighted against other hard constraints. Soft constraints are weighted against other soft constraints. Hard constraints always outweigh soft constraints, regardless of their respective weights. To calculate the score, you could implement an EasyScoreCalculator class: public class TimeTableEasyScoreCalculator implements EasyScoreCalculator<TimeTable> { @Override public HardSoftScore calculateScore(TimeTable timeTable) { List<Lesson> lessonList = timeTable.getLessonList(); int hardScore = 0; for (Lesson a : lessonList) { for (Lesson b : lessonList) { if (a.getTimeslot() != null && a.getTimeslot().equals(b.getTimeslot()) && a.getId() < b.getId()) { // A room can accommodate at most one lesson at the same time. if (a.getRoom() != null && a.getRoom().equals(b.getRoom())) { hardScore--; } // A teacher can teach at most one lesson at the same time. if (a.getTeacher().equals(b.getTeacher())) { hardScore--; } // A student can attend at most one lesson at the same time. if (a.getStudentGroup().equals(b.getStudentGroup())) { hardScore--; } } } } int softScore = 0; // Soft constraints are only implemented in the "complete" implementation return HardSoftScore.of(hardScore, softScore); } } Unfortunately, this solution does not scale well because it is non-incremental: every time a lesson is assigned to a different time slot or room, all lessons are re-evaluated to calculate the new score. A better solution is to create a src/main/java/com/example/solver/TimeTableConstraintProvider.java class to perform incremental score calculation. This class uses OptaPlanner's ConstraintStream API which is inspired by Java 8 Streams and SQL. The ConstraintProvider scales an order of magnitude better than the EasyScoreCalculator : O (n) instead of O (n2). Procedure Create the following src/main/java/com/example/solver/TimeTableConstraintProvider.java class: package com.example.solver; import com.example.domain.Lesson; 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.score.stream.Joiners; public class TimeTableConstraintProvider implements ConstraintProvider { @Override public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { return new Constraint[] { // Hard constraints roomConflict(constraintFactory), teacherConflict(constraintFactory), studentGroupConflict(constraintFactory), // Soft constraints are only implemented in the "complete" implementation }; } private Constraint roomConflict(ConstraintFactory constraintFactory) { // A room can accommodate at most one lesson at the same time. // Select a lesson ... return constraintFactory.from(Lesson.class) // ... and pair it with another lesson ... .join(Lesson.class, // ... in the same timeslot ... Joiners.equal(Lesson::getTimeslot), // ... in the same room ... Joiners.equal(Lesson::getRoom), // ... and the pair is unique (different id, no reverse pairs) Joiners.lessThan(Lesson::getId)) // then penalize each pair with a hard weight. .penalize("Room conflict", HardSoftScore.ONE_HARD); } private Constraint teacherConflict(ConstraintFactory constraintFactory) { // A teacher can teach at most one lesson at the same time. return constraintFactory.from(Lesson.class) .join(Lesson.class, Joiners.equal(Lesson::getTimeslot), Joiners.equal(Lesson::getTeacher), Joiners.lessThan(Lesson::getId)) .penalize("Teacher conflict", HardSoftScore.ONE_HARD); } private Constraint studentGroupConflict(ConstraintFactory constraintFactory) { // A student can attend at most one lesson at the same time. return constraintFactory.from(Lesson.class) .join(Lesson.class, Joiners.equal(Lesson::getTimeslot), Joiners.equal(Lesson::getStudentGroup), Joiners.lessThan(Lesson::getId)) .penalize("Student group conflict", HardSoftScore.ONE_HARD); } } 13.4. Gather the domain objects in a planning solution A TimeTable instance wraps all Timeslot , Room , and Lesson instances of a single dataset. Furthermore, because it contains all lessons, each with a specific planning variable state, it is a planning solution and it has a score: If lessons are still unassigned, then it is an uninitialized solution, for example, a solution with the score -4init/0hard/0soft . If it breaks hard constraints, then it is an infeasible solution, for example, a solution with the score -2hard/-3soft . If it adheres to all hard constraints, then it is a feasible solution, for example, a solution with the score 0hard/-7soft . The TimeTable class has an @PlanningSolution annotation, so Red Hat build of OptaPlanner knows that this class contains all of the input and output data. Specifically, this class is the input of the problem: A timeslotList field with all time slots This is a list of problem facts, because they do not change during solving. A roomList field with all rooms This is a list of problem facts, because they do not change during solving. A lessonList field with all lessons This is a list of planning entities because they change during solving. Of each Lesson : The values of the timeslot and room fields are typically still null , so unassigned. They are planning variables. The other fields, such as subject , teacher and studentGroup , are filled in. These fields are problem properties. However, this class is also the output of the solution: A lessonList field for which each Lesson instance has non-null timeslot and room fields after solving A score field that represents the quality of the output solution, for example, 0hard/-5soft Procedure Create the src/main/java/com/example/domain/TimeTable.java class: package com.example.domain; import java.util.List; 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.score.buildin.hardsoft.HardSoftScore; @PlanningSolution public class TimeTable { @ValueRangeProvider(id = "timeslotRange") @ProblemFactCollectionProperty private List<Timeslot> timeslotList; @ValueRangeProvider(id = "roomRange") @ProblemFactCollectionProperty private List<Room> roomList; @PlanningEntityCollectionProperty private List<Lesson> lessonList; @PlanningScore private HardSoftScore score; private TimeTable() { } public TimeTable(List<Timeslot> timeslotList, List<Room> roomList, List<Lesson> lessonList) { this.timeslotList = timeslotList; this.roomList = roomList; this.lessonList = lessonList; } // ******************************** // Getters and setters // ******************************** public List<Timeslot> getTimeslotList() { return timeslotList; } public List<Room> getRoomList() { return roomList; } public List<Lesson> getLessonList() { return lessonList; } public HardSoftScore getScore() { return score; } } The value range providers The timeslotList field is a value range provider. It holds the Timeslot instances which OptaPlanner can pick from to assign to the timeslot field of Lesson instances. The timeslotList field has an @ValueRangeProvider annotation to connect those two, by matching the id with the valueRangeProviderRefs of the @PlanningVariable in the Lesson . Following the same logic, the roomList field also has an @ValueRangeProvider annotation. The problem fact and planning entity properties Furthermore, OptaPlanner needs to know which Lesson instances it can change as well as how to retrieve the Timeslot and Room instances used for score calculation by your TimeTableConstraintProvider . The timeslotList and roomList fields have an @ProblemFactCollectionProperty annotation, so your TimeTableConstraintProvider can select from those instances. The lessonList has an @PlanningEntityCollectionProperty annotation, so OptaPlanner can change them during solving and your TimeTableConstraintProvider can select from those too. 13.5. The TimeTableApp.java class After you have created all of the components of the school timetable application, you will put them all together in the TimeTableApp.java class. The main() method performs the following tasks: Creates the SolverFactory to build a Solver for each data set. Loads a data set. Solves it with Solver.solve() . Visualizes the solution for that data set. Typically, an application has a single SolverFactory to build a new Solver instance for each problem data set to solve. A SolverFactory is thread-safe, but a Solver is not. For the school timetable application, there is only one data set, so only one Solver instance. Here is the completed TimeTableApp.java class: package org.acme.schooltimetabling; import java.time.DayOfWeek; import java.time.Duration; import java.time.LocalTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.acme.schooltimetabling.domain.Lesson; import org.acme.schooltimetabling.domain.Room; import org.acme.schooltimetabling.domain.TimeTable; import org.acme.schooltimetabling.domain.Timeslot; import org.acme.schooltimetabling.solver.TimeTableConstraintProvider; import org.optaplanner.core.api.solver.Solver; import org.optaplanner.core.api.solver.SolverFactory; import org.optaplanner.core.config.solver.SolverConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TimeTableApp { private static final Logger LOGGER = LoggerFactory.getLogger(TimeTableApp.class); public static void main(String[] args) { SolverFactory<TimeTable> solverFactory = SolverFactory.create(new SolverConfig() .withSolutionClass(TimeTable.class) .withEntityClasses(Lesson.class) .withConstraintProviderClass(TimeTableConstraintProvider.class) // The solver runs only for 5 seconds on this small data set. // It's recommended to run for at least 5 minutes ("5m") otherwise. .withTerminationSpentLimit(Duration.ofSeconds(10))); // Load the problem TimeTable problem = generateDemoData(); // Solve the problem Solver<TimeTable> solver = solverFactory.buildSolver(); TimeTable solution = solver.solve(problem); // Visualize the solution printTimetable(solution); } public static TimeTable generateDemoData() { List<Timeslot> timeslotList = new ArrayList<>(10); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); List<Room> roomList = new ArrayList<>(3); roomList.add(new Room("Room A")); roomList.add(new Room("Room B")); roomList.add(new Room("Room C")); List<Lesson> lessonList = new ArrayList<>(); long id = 0; lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade")); lessonList.add(new Lesson(id++, "Physics", "M. Curie", "9th grade")); lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "9th grade")); lessonList.add(new Lesson(id++, "Biology", "C. Darwin", "9th grade")); lessonList.add(new Lesson(id++, "History", "I. Jones", "9th grade")); lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade")); lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade")); lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade")); lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade")); lessonList.add(new Lesson(id++, "Physics", "M. Curie", "10th grade")); lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "10th grade")); lessonList.add(new Lesson(id++, "French", "M. Curie", "10th grade")); lessonList.add(new Lesson(id++, "Geography", "C. Darwin", "10th grade")); lessonList.add(new Lesson(id++, "History", "I. Jones", "10th grade")); lessonList.add(new Lesson(id++, "English", "P. Cruz", "10th grade")); lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "10th grade")); return new TimeTable(timeslotList, roomList, lessonList); } private static void printTimetable(TimeTable timeTable) { LOGGER.info(""); List<Room> roomList = timeTable.getRoomList(); List<Lesson> lessonList = timeTable.getLessonList(); Map<Timeslot, Map<Room, List<Lesson>>> lessonMap = lessonList.stream() .filter(lesson -> lesson.getTimeslot() != null && lesson.getRoom() != null) .collect(Collectors.groupingBy(Lesson::getTimeslot, Collectors.groupingBy(Lesson::getRoom))); LOGGER.info("| | " + roomList.stream() .map(room -> String.format("%-10s", room.getName())).collect(Collectors.joining(" | ")) + " |"); LOGGER.info("|" + "------------|".repeat(roomList.size() + 1)); for (Timeslot timeslot : timeTable.getTimeslotList()) { List<List<Lesson>> cellList = roomList.stream() .map(room -> { Map<Room, List<Lesson>> byRoomMap = lessonMap.get(timeslot); if (byRoomMap == null) { return Collections.<Lesson>emptyList(); } List<Lesson> cellLessonList = byRoomMap.get(room); if (cellLessonList == null) { return Collections.<Lesson>emptyList(); } return cellLessonList; }) .collect(Collectors.toList()); LOGGER.info("| " + String.format("%-10s", timeslot.getDayOfWeek().toString().substring(0, 3) + " " + timeslot.getStartTime()) + " | " + cellList.stream().map(cellLessonList -> String.format("%-10s", cellLessonList.stream().map(Lesson::getSubject).collect(Collectors.joining(", ")))) .collect(Collectors.joining(" | ")) + " |"); LOGGER.info("| | " + cellList.stream().map(cellLessonList -> String.format("%-10s", cellLessonList.stream().map(Lesson::getTeacher).collect(Collectors.joining(", ")))) .collect(Collectors.joining(" | ")) + " |"); LOGGER.info("| | " + cellList.stream().map(cellLessonList -> String.format("%-10s", cellLessonList.stream().map(Lesson::getStudentGroup).collect(Collectors.joining(", ")))) .collect(Collectors.joining(" | ")) + " |"); LOGGER.info("|" + "------------|".repeat(roomList.size() + 1)); } List<Lesson> unassignedLessons = lessonList.stream() .filter(lesson -> lesson.getTimeslot() == null || lesson.getRoom() == null) .collect(Collectors.toList()); if (!unassignedLessons.isEmpty()) { LOGGER.info(""); LOGGER.info("Unassigned lessons"); for (Lesson lesson : unassignedLessons) { LOGGER.info(" " + lesson.getSubject() + " - " + lesson.getTeacher() + " - " + lesson.getStudentGroup()); } } } } The main() method first creates the SolverFactory : SolverFactory<TimeTable> solverFactory = SolverFactory.create(new SolverConfig() .withSolutionClass(TimeTable.class) .withEntityClasses(Lesson.class) .withConstraintProviderClass(TimeTableConstraintProvider.class) // The solver runs only for 5 seconds on this small data set. // It's recommended to run for at least 5 minutes ("5m") otherwise. .withTerminationSpentLimit(Duration.ofSeconds(5))); The SolverFactory creation registers the @PlanningSolution class, the @PlanningEntity classes, and the ConstraintProvider class, all of which you created earlier. Without a termination setting or a terminationEarly() event, the solver runs forever. To avoid that, the solver limits the solving time to five seconds. After five seconds, the main() method loads the problem, solves it, and prints the solution: // Load the problem TimeTable problem = generateDemoData(); // Solve the problem Solver<TimeTable> solver = solverFactory.buildSolver(); TimeTable solution = solver.solve(problem); // Visualize the solution printTimetable(solution); The solve() method doesn't return instantly. It runs for five seconds before returning the best solution. OptaPlanner returns the best solution found in the available termination time. Due to the nature of NP-hard problems, the best solution might not be optimal, especially for larger data sets. Increase the termination time to potentially find a better solution. The generateDemoData() method generates the school timetable problem to solve. The printTimetable() method prettyprints the timetable to the console, so it's easy to determine visually whether or not it's a good schedule. 13.6. Create and run the school timetable application Now that you have completed all of the components of the school timetable Java application, you are ready to put them all together in the TimeTableApp.java class and run it. Prerequisites You have created all of the required components of the school timetable application. Procedure Create the src/main/java/org/acme/schooltimetabling/TimeTableApp.java class: package org.acme.schooltimetabling; import java.time.DayOfWeek; import java.time.Duration; import java.time.LocalTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.acme.schooltimetabling.domain.Lesson; import org.acme.schooltimetabling.domain.Room; import org.acme.schooltimetabling.domain.TimeTable; import org.acme.schooltimetabling.domain.Timeslot; import org.acme.schooltimetabling.solver.TimeTableConstraintProvider; import org.optaplanner.core.api.solver.Solver; import org.optaplanner.core.api.solver.SolverFactory; import org.optaplanner.core.config.solver.SolverConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TimeTableApp { private static final Logger LOGGER = LoggerFactory.getLogger(TimeTableApp.class); public static void main(String[] args) { SolverFactory<TimeTable> solverFactory = SolverFactory.create(new SolverConfig() .withSolutionClass(TimeTable.class) .withEntityClasses(Lesson.class) .withConstraintProviderClass(TimeTableConstraintProvider.class) // The solver runs only for 5 seconds on this small data set. // It's recommended to run for at least 5 minutes ("5m") otherwise. .withTerminationSpentLimit(Duration.ofSeconds(10))); // Load the problem TimeTable problem = generateDemoData(); // Solve the problem Solver<TimeTable> solver = solverFactory.buildSolver(); TimeTable solution = solver.solve(problem); // Visualize the solution printTimetable(solution); } public static TimeTable generateDemoData() { List<Timeslot> timeslotList = new ArrayList<>(10); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); List<Room> roomList = new ArrayList<>(3); roomList.add(new Room("Room A")); roomList.add(new Room("Room B")); roomList.add(new Room("Room C")); List<Lesson> lessonList = new ArrayList<>(); long id = 0; lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade")); lessonList.add(new Lesson(id++, "Physics", "M. Curie", "9th grade")); lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "9th grade")); lessonList.add(new Lesson(id++, "Biology", "C. Darwin", "9th grade")); lessonList.add(new Lesson(id++, "History", "I. Jones", "9th grade")); lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade")); lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade")); lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade")); lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade")); lessonList.add(new Lesson(id++, "Physics", "M. Curie", "10th grade")); lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "10th grade")); lessonList.add(new Lesson(id++, "French", "M. Curie", "10th grade")); lessonList.add(new Lesson(id++, "Geography", "C. Darwin", "10th grade")); lessonList.add(new Lesson(id++, "History", "I. Jones", "10th grade")); lessonList.add(new Lesson(id++, "English", "P. Cruz", "10th grade")); lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "10th grade")); return new TimeTable(timeslotList, roomList, lessonList); } private static void printTimetable(TimeTable timeTable) { LOGGER.info(""); List<Room> roomList = timeTable.getRoomList(); List<Lesson> lessonList = timeTable.getLessonList(); Map<Timeslot, Map<Room, List<Lesson>>> lessonMap = lessonList.stream() .filter(lesson -> lesson.getTimeslot() != null && lesson.getRoom() != null) .collect(Collectors.groupingBy(Lesson::getTimeslot, Collectors.groupingBy(Lesson::getRoom))); LOGGER.info("| | " + roomList.stream() .map(room -> String.format("%-10s", room.getName())).collect(Collectors.joining(" | ")) + " |"); LOGGER.info("|" + "------------|".repeat(roomList.size() + 1)); for (Timeslot timeslot : timeTable.getTimeslotList()) { List<List<Lesson>> cellList = roomList.stream() .map(room -> { Map<Room, List<Lesson>> byRoomMap = lessonMap.get(timeslot); if (byRoomMap == null) { return Collections.<Lesson>emptyList(); } List<Lesson> cellLessonList = byRoomMap.get(room); if (cellLessonList == null) { return Collections.<Lesson>emptyList(); } return cellLessonList; }) .collect(Collectors.toList()); LOGGER.info("| " + String.format("%-10s", timeslot.getDayOfWeek().toString().substring(0, 3) + " " + timeslot.getStartTime()) + " | " + cellList.stream().map(cellLessonList -> String.format("%-10s", cellLessonList.stream().map(Lesson::getSubject).collect(Collectors.joining(", ")))) .collect(Collectors.joining(" | ")) + " |"); LOGGER.info("| | " + cellList.stream().map(cellLessonList -> String.format("%-10s", cellLessonList.stream().map(Lesson::getTeacher).collect(Collectors.joining(", ")))) .collect(Collectors.joining(" | ")) + " |"); LOGGER.info("| | " + cellList.stream().map(cellLessonList -> String.format("%-10s", cellLessonList.stream().map(Lesson::getStudentGroup).collect(Collectors.joining(", ")))) .collect(Collectors.joining(" | ")) + " |"); LOGGER.info("|" + "------------|".repeat(roomList.size() + 1)); } List<Lesson> unassignedLessons = lessonList.stream() .filter(lesson -> lesson.getTimeslot() == null || lesson.getRoom() == null) .collect(Collectors.toList()); if (!unassignedLessons.isEmpty()) { LOGGER.info(""); LOGGER.info("Unassigned lessons"); for (Lesson lesson : unassignedLessons) { LOGGER.info(" " + lesson.getSubject() + " - " + lesson.getTeacher() + " - " + lesson.getStudentGroup()); } } } } Run the TimeTableApp class as the main class of a normal Java application. The following output should result: Verify the console output. Does it conform to all hard constraints? What happens if you comment out the roomConflict constraint in TimeTableConstraintProvider ? The info log shows what OptaPlanner did in those five seconds: 13.7. Test the application A good application includes test coverage. Test the constraints and the solver in your timetable project. 13.7.1. Test the school timetable constraints To test each constraint of the timetable project in isolation, use a ConstraintVerifier in unit tests. This tests each constraint's corner cases in isolation from the other tests, which lowers maintenance when adding a new constraint with proper test coverage. This test verifies that the constraint TimeTableConstraintProvider::roomConflict , when given three lessons in the same room and two of the lessons have the same timeslot, penalizes with a match weight of 1. So if the constraint weight is 10hard it reduces the score by -10hard . Procedure Create the src/test/java/org/acme/optaplanner/solver/TimeTableConstraintProviderTest.java class: package org.acme.optaplanner.solver; import java.time.DayOfWeek; import java.time.LocalTime; import javax.inject.Inject; import io.quarkus.test.junit.QuarkusTest; import org.acme.optaplanner.domain.Lesson; import org.acme.optaplanner.domain.Room; import org.acme.optaplanner.domain.TimeTable; import org.acme.optaplanner.domain.Timeslot; import org.junit.jupiter.api.Test; import org.optaplanner.test.api.score.stream.ConstraintVerifier; @QuarkusTest class TimeTableConstraintProviderTest { private static final Room ROOM = new Room("Room1"); private static final Timeslot TIMESLOT1 = new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9,0), LocalTime.NOON); private static final Timeslot TIMESLOT2 = new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9,0), LocalTime.NOON); @Inject ConstraintVerifier<TimeTableConstraintProvider, TimeTable> constraintVerifier; @Test void roomConflict() { Lesson firstLesson = new Lesson(1, "Subject1", "Teacher1", "Group1"); Lesson conflictingLesson = new Lesson(2, "Subject2", "Teacher2", "Group2"); Lesson nonConflictingLesson = new Lesson(3, "Subject3", "Teacher3", "Group3"); firstLesson.setRoom(ROOM); firstLesson.setTimeslot(TIMESLOT1); conflictingLesson.setRoom(ROOM); conflictingLesson.setTimeslot(TIMESLOT1); nonConflictingLesson.setRoom(ROOM); nonConflictingLesson.setTimeslot(TIMESLOT2); constraintVerifier.verifyThat(TimeTableConstraintProvider::roomConflict) .given(firstLesson, conflictingLesson, nonConflictingLesson) .penalizesBy(1); } } Notice how ConstraintVerifier ignores the constraint weight during testing even if those constraint weights are hardcoded in the ConstraintProvider . This is because constraint weights change regularly before going into production. This way, constraint weight tweaking does not break the unit tests. 13.7.2. Test the school timetable solver This example tests the Red Hat build of OptaPlanner school timetable project on Red Hat build of Quarkus. It uses a JUnit test to generate a test data set and send it to the TimeTableController to solve. Procedure Create the src/test/java/com/example/rest/TimeTableResourceTest.java class with the following content: package com.exmaple.optaplanner.rest; import java.time.DayOfWeek; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import io.quarkus.test.junit.QuarkusTest; import com.exmaple.optaplanner.domain.Room; import com.exmaple.optaplanner.domain.Timeslot; import com.exmaple.optaplanner.domain.Lesson; import com.exmaple.optaplanner.domain.TimeTable; import com.exmaple.optaplanner.rest.TimeTableResource; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @QuarkusTest public class TimeTableResourceTest { @Inject TimeTableResource timeTableResource; @Test @Timeout(600_000) public void solve() { TimeTable problem = generateProblem(); TimeTable solution = timeTableResource.solve(problem); assertFalse(solution.getLessonList().isEmpty()); for (Lesson lesson : solution.getLessonList()) { assertNotNull(lesson.getTimeslot()); assertNotNull(lesson.getRoom()); } assertTrue(solution.getScore().isFeasible()); } private TimeTable generateProblem() { List<Timeslot> timeslotList = new ArrayList<>(); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); List<Room> roomList = new ArrayList<>(); roomList.add(new Room("Room A")); roomList.add(new Room("Room B")); roomList.add(new Room("Room C")); List<Lesson> lessonList = new ArrayList<>(); lessonList.add(new Lesson(101L, "Math", "B. May", "9th grade")); lessonList.add(new Lesson(102L, "Physics", "M. Curie", "9th grade")); lessonList.add(new Lesson(103L, "Geography", "M. Polo", "9th grade")); lessonList.add(new Lesson(104L, "English", "I. Jones", "9th grade")); lessonList.add(new Lesson(105L, "Spanish", "P. Cruz", "9th grade")); lessonList.add(new Lesson(201L, "Math", "B. May", "10th grade")); lessonList.add(new Lesson(202L, "Chemistry", "M. Curie", "10th grade")); lessonList.add(new Lesson(203L, "History", "I. Jones", "10th grade")); lessonList.add(new Lesson(204L, "English", "P. Cruz", "10th grade")); lessonList.add(new Lesson(205L, "French", "M. Curie", "10th grade")); return new TimeTable(timeslotList, roomList, lessonList); } } This test verifies that after solving, all lessons are assigned to a time slot and a room. It also verifies that it found a feasible solution (no hard constraints broken). Add test properties to the src/main/resources/application.properties file: Normally, the solver finds a feasible solution in less than 200 milliseconds. Notice how the application.properties file overwrites the solver termination during tests to terminate as soon as a feasible solution (0hard/*soft) is found. This avoids hard coding a solver time, because the unit test might run on arbitrary hardware. This approach ensures that the test runs long enough to find a feasible solution, even on slow systems. But it does not run a millisecond longer than it strictly must, even on fast systems. 13.8. Logging After you complete the Red Hat build of OptaPlanner school timetable project, you can use logging information to help you fine-tune the constraints in the ConstraintProvider . Review the score calculation speed in the info log file to assess the impact of changes to your constraints. Run the application in debug mode to show every step that your application takes or use trace logging to log every step and every move. Procedure Run the school timetable application for a fixed amount of time, for example, five minutes. Review the score calculation speed in the log file as shown in the following example: Change a constraint, run the planning application again for the same amount of time, and review the score calculation speed recorded in the log file. Run the application in debug mode to log every step that the application makes: To run debug mode from the command line, use the -D system property. To permanently enable debug mode, add the following line to the application.properties file: quarkus.log.category."org.optaplanner".level=debug The following example shows output in the log file in debug mode: Use trace logging to show every step and every move for each step. 13.9. Using Micrometer and Prometheus to monitor your school timetable OptaPlanner Java application OptaPlanner exposes metrics through Micrometer , a metrics instrumentation library for Java applications. You can use Micrometer with Prometheus to monitor the OptaPlanner solver in the school timetable application. Prerequisites You have created the OptaPlanner school timetable application with Java. Prometheus is installed. For information about installing Prometheus, see the Prometheus website. Procedure Add the Micrometer Prometheus dependencies to the school timetable pom.xml file where <MICROMETER_VERSION> is the version of Micrometer that you installed: Note The micrometer-core dependency is also required. However this dependency is contained in the optaplanner-core dependency so you do not need to add it to the pom.xml file. Add the following import statements to the TimeTableApp.java class. Add the following lines to the top of the main method of the TimeTableApp.java class so Prometheus can scrap data from com.sun.net.httpserver.HttpServer before the solution starts: PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); try { HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); server.createContext("/prometheus", httpExchange -> { String response = prometheusRegistry.scrape(); httpExchange.sendResponseHeaders(200, response.getBytes().length); try (OutputStream os = httpExchange.getResponseBody()) { os.write(response.getBytes()); } }); new Thread(server::start).start(); } catch (IOException e) { throw new RuntimeException(e); } Metrics.addRegistry(prometheusRegistry); solve(); } Add the following line to control the solving time. By adjusting the solving time, you can see how the metrics change based on the time spent solving. Start the school timetable application. Open http://localhost:8080/prometheus in a web browser to view the timetable application in Prometheus. Open your monitoring system to view the metrics for your OptaPlanner project. The following metrics are exposed: optaplanner_solver_errors_total : the total number of errors that occurred while solving since the start of the measuring. optaplanner_solver_solve_duration_seconds_active_count : the number of solvers currently solving. optaplanner_solver_solve_duration_seconds_max : run time of the longest-running currently active solver. optaplanner_solver_solve_duration_seconds_duration_sum : the sum of each active solver's solve duration. For example, if there are two active solvers, one running for three minutes and the other for one minute, the total solve time is four minutes.
INFO Solving ended: time spent (5000), best score (0hard/9soft), INFO INFO | | Room A | Room B | Room C | INFO |------------|------------|------------|------------| INFO | MON 08:30 | English | Math | | INFO | | I. Jones | A. Turing | | INFO | | 9th grade | 10th grade | | INFO |------------|------------|------------|------------| INFO | MON 09:30 | History | Physics | | INFO | | I. Jones | M. Curie | | INFO | | 9th grade | 10th grade | | INFO |------------|------------|------------|------------| INFO | MON 10:30 | History | Physics | | INFO | | I. Jones | M. Curie | | INFO | | 10th grade | 9th grade | | INFO |------------|------------|------------|------------| INFO |------------|------------|------------|------------| <dependency> <groupId>org.optaplanner</groupId> <artifactId>optaplanner-core</artifactId> </dependency> <dependency> <groupId>org.optaplanner</groupId> <artifactId>optaplanner-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.acme</groupId> <artifactId>optaplanner-hello-world-school-timetabling-quickstart</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.release>11</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <version.org.optaplanner>8.13.0.Final-redhat-00013</version.org.optaplanner> <version.org.logback>1.2.3</version.org.logback> <version.compiler.plugin>3.8.1</version.compiler.plugin> <version.surefire.plugin>3.0.0-M5</version.surefire.plugin> <version.exec.plugin>3.0.0</version.exec.plugin> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.optaplanner</groupId> <artifactId>optaplanner-bom</artifactId> <version>USD{version.org.optaplanner}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>USD{version.org.logback}</version> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.optaplanner</groupId> <artifactId>optaplanner-core</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <scope>runtime</scope> </dependency> <!-- Testing --> <dependency> <groupId>org.optaplanner</groupId> <artifactId>optaplanner-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>USD{version.compiler.plugin}</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>USD{version.surefire.plugin}</version> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>USD{version.exec.plugin}</version> <configuration> <mainClass>org.acme.schooltimetabling.TimeTableApp</mainClass> </configuration> </plugin> </plugins> </build> <repositories> <repository> <id>jboss-public-repository-group</id> <url>https://repository.jboss.org/nexus/content/groups/public/</url> <releases> <!-- Get releases only from Maven Central which is faster. --> <enabled>false</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> </project> dependencies { implementation "org.optaplanner:optaplanner-core:USD{optaplannerVersion}" runtimeOnly "ch.qos.logback:logback-classic:USD{logbackVersion}" testImplementation "org.optaplanner:optaplanner-test:USD{optaplannerVersion}" } plugins { id "java" id "application" } def optaplannerVersion = "{project-version}" def logbackVersion = "1.2.3" group = "org.acme" version = "0.1.0-SNAPSHOT" repositories { mavenCentral() } dependencies { implementation "org.optaplanner:optaplanner-core:USD{optaplannerVersion}" runtimeOnly "ch.qos.logback:logback-classic:USD{logbackVersion}" testImplementation "org.optaplanner:optaplanner-test:USD{optaplannerVersion}" } java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } compileJava { options.encoding = "UTF-8" options.compilerArgs << "-parameters" } compileTestJava { options.encoding = "UTF-8" } application { mainClass = "org.acme.schooltimetabling.TimeTableApp" } test { // Log the test execution results. testLogging { events "passed", "skipped", "failed" } } package com.example.domain; import java.time.DayOfWeek; import java.time.LocalTime; public class Timeslot { private DayOfWeek dayOfWeek; private LocalTime startTime; 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.toString(); } // ******************************** // Getters and setters // ******************************** public DayOfWeek getDayOfWeek() { return dayOfWeek; } public LocalTime getStartTime() { return startTime; } public LocalTime getEndTime() { return endTime; } } package com.example.domain; public class Room { private String name; private Room() { } public Room(String name) { this.name = name; } @Override public String toString() { return name; } // ******************************** // Getters and setters // ******************************** public String getName() { return name; } } package com.example.domain; import org.optaplanner.core.api.domain.entity.PlanningEntity; import org.optaplanner.core.api.domain.variable.PlanningVariable; @PlanningEntity public class Lesson { private Long id; private String subject; private String teacher; private String studentGroup; @PlanningVariable(valueRangeProviderRefs = "timeslotRange") private Timeslot timeslot; @PlanningVariable(valueRangeProviderRefs = "roomRange") private Room room; private Lesson() { } public Lesson(Long id, String subject, String teacher, String studentGroup) { this.id = id; this.subject = subject; this.teacher = teacher; this.studentGroup = studentGroup; } @Override public String toString() { return subject + "(" + id + ")"; } // ******************************** // Getters and setters // ******************************** public Long getId() { return id; } public String getSubject() { return subject; } public String getTeacher() { return teacher; } public String getStudentGroup() { return studentGroup; } public Timeslot getTimeslot() { return timeslot; } public void setTimeslot(Timeslot timeslot) { this.timeslot = timeslot; } public Room getRoom() { return room; } public void setRoom(Room room) { this.room = room; } } public class TimeTableEasyScoreCalculator implements EasyScoreCalculator<TimeTable> { @Override public HardSoftScore calculateScore(TimeTable timeTable) { List<Lesson> lessonList = timeTable.getLessonList(); int hardScore = 0; for (Lesson a : lessonList) { for (Lesson b : lessonList) { if (a.getTimeslot() != null && a.getTimeslot().equals(b.getTimeslot()) && a.getId() < b.getId()) { // A room can accommodate at most one lesson at the same time. if (a.getRoom() != null && a.getRoom().equals(b.getRoom())) { hardScore--; } // A teacher can teach at most one lesson at the same time. if (a.getTeacher().equals(b.getTeacher())) { hardScore--; } // A student can attend at most one lesson at the same time. if (a.getStudentGroup().equals(b.getStudentGroup())) { hardScore--; } } } } int softScore = 0; // Soft constraints are only implemented in the "complete" implementation return HardSoftScore.of(hardScore, softScore); } } package com.example.solver; import com.example.domain.Lesson; 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.score.stream.Joiners; public class TimeTableConstraintProvider implements ConstraintProvider { @Override public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { return new Constraint[] { // Hard constraints roomConflict(constraintFactory), teacherConflict(constraintFactory), studentGroupConflict(constraintFactory), // Soft constraints are only implemented in the "complete" implementation }; } private Constraint roomConflict(ConstraintFactory constraintFactory) { // A room can accommodate at most one lesson at the same time. // Select a lesson return constraintFactory.from(Lesson.class) // ... and pair it with another lesson .join(Lesson.class, // ... in the same timeslot Joiners.equal(Lesson::getTimeslot), // ... in the same room Joiners.equal(Lesson::getRoom), // ... and the pair is unique (different id, no reverse pairs) Joiners.lessThan(Lesson::getId)) // then penalize each pair with a hard weight. .penalize("Room conflict", HardSoftScore.ONE_HARD); } private Constraint teacherConflict(ConstraintFactory constraintFactory) { // A teacher can teach at most one lesson at the same time. return constraintFactory.from(Lesson.class) .join(Lesson.class, Joiners.equal(Lesson::getTimeslot), Joiners.equal(Lesson::getTeacher), Joiners.lessThan(Lesson::getId)) .penalize("Teacher conflict", HardSoftScore.ONE_HARD); } private Constraint studentGroupConflict(ConstraintFactory constraintFactory) { // A student can attend at most one lesson at the same time. return constraintFactory.from(Lesson.class) .join(Lesson.class, Joiners.equal(Lesson::getTimeslot), Joiners.equal(Lesson::getStudentGroup), Joiners.lessThan(Lesson::getId)) .penalize("Student group conflict", HardSoftScore.ONE_HARD); } } package com.example.domain; import java.util.List; 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.score.buildin.hardsoft.HardSoftScore; @PlanningSolution public class TimeTable { @ValueRangeProvider(id = "timeslotRange") @ProblemFactCollectionProperty private List<Timeslot> timeslotList; @ValueRangeProvider(id = "roomRange") @ProblemFactCollectionProperty private List<Room> roomList; @PlanningEntityCollectionProperty private List<Lesson> lessonList; @PlanningScore private HardSoftScore score; private TimeTable() { } public TimeTable(List<Timeslot> timeslotList, List<Room> roomList, List<Lesson> lessonList) { this.timeslotList = timeslotList; this.roomList = roomList; this.lessonList = lessonList; } // ******************************** // Getters and setters // ******************************** public List<Timeslot> getTimeslotList() { return timeslotList; } public List<Room> getRoomList() { return roomList; } public List<Lesson> getLessonList() { return lessonList; } public HardSoftScore getScore() { return score; } } package org.acme.schooltimetabling; import java.time.DayOfWeek; import java.time.Duration; import java.time.LocalTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.acme.schooltimetabling.domain.Lesson; import org.acme.schooltimetabling.domain.Room; import org.acme.schooltimetabling.domain.TimeTable; import org.acme.schooltimetabling.domain.Timeslot; import org.acme.schooltimetabling.solver.TimeTableConstraintProvider; import org.optaplanner.core.api.solver.Solver; import org.optaplanner.core.api.solver.SolverFactory; import org.optaplanner.core.config.solver.SolverConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TimeTableApp { private static final Logger LOGGER = LoggerFactory.getLogger(TimeTableApp.class); public static void main(String[] args) { SolverFactory<TimeTable> solverFactory = SolverFactory.create(new SolverConfig() .withSolutionClass(TimeTable.class) .withEntityClasses(Lesson.class) .withConstraintProviderClass(TimeTableConstraintProvider.class) // The solver runs only for 5 seconds on this small data set. // It's recommended to run for at least 5 minutes ("5m") otherwise. .withTerminationSpentLimit(Duration.ofSeconds(10))); // Load the problem TimeTable problem = generateDemoData(); // Solve the problem Solver<TimeTable> solver = solverFactory.buildSolver(); TimeTable solution = solver.solve(problem); // Visualize the solution printTimetable(solution); } public static TimeTable generateDemoData() { List<Timeslot> timeslotList = new ArrayList<>(10); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); List<Room> roomList = new ArrayList<>(3); roomList.add(new Room("Room A")); roomList.add(new Room("Room B")); roomList.add(new Room("Room C")); List<Lesson> lessonList = new ArrayList<>(); long id = 0; lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade")); lessonList.add(new Lesson(id++, "Physics", "M. Curie", "9th grade")); lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "9th grade")); lessonList.add(new Lesson(id++, "Biology", "C. Darwin", "9th grade")); lessonList.add(new Lesson(id++, "History", "I. Jones", "9th grade")); lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade")); lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade")); lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade")); lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade")); lessonList.add(new Lesson(id++, "Physics", "M. Curie", "10th grade")); lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "10th grade")); lessonList.add(new Lesson(id++, "French", "M. Curie", "10th grade")); lessonList.add(new Lesson(id++, "Geography", "C. Darwin", "10th grade")); lessonList.add(new Lesson(id++, "History", "I. Jones", "10th grade")); lessonList.add(new Lesson(id++, "English", "P. Cruz", "10th grade")); lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "10th grade")); return new TimeTable(timeslotList, roomList, lessonList); } private static void printTimetable(TimeTable timeTable) { LOGGER.info(""); List<Room> roomList = timeTable.getRoomList(); List<Lesson> lessonList = timeTable.getLessonList(); Map<Timeslot, Map<Room, List<Lesson>>> lessonMap = lessonList.stream() .filter(lesson -> lesson.getTimeslot() != null && lesson.getRoom() != null) .collect(Collectors.groupingBy(Lesson::getTimeslot, Collectors.groupingBy(Lesson::getRoom))); LOGGER.info("| | " + roomList.stream() .map(room -> String.format("%-10s", room.getName())).collect(Collectors.joining(" | ")) + " |"); LOGGER.info("|" + "------------|".repeat(roomList.size() + 1)); for (Timeslot timeslot : timeTable.getTimeslotList()) { List<List<Lesson>> cellList = roomList.stream() .map(room -> { Map<Room, List<Lesson>> byRoomMap = lessonMap.get(timeslot); if (byRoomMap == null) { return Collections.<Lesson>emptyList(); } List<Lesson> cellLessonList = byRoomMap.get(room); if (cellLessonList == null) { return Collections.<Lesson>emptyList(); } return cellLessonList; }) .collect(Collectors.toList()); LOGGER.info("| " + String.format("%-10s", timeslot.getDayOfWeek().toString().substring(0, 3) + " " + timeslot.getStartTime()) + " | " + cellList.stream().map(cellLessonList -> String.format("%-10s", cellLessonList.stream().map(Lesson::getSubject).collect(Collectors.joining(", ")))) .collect(Collectors.joining(" | ")) + " |"); LOGGER.info("| | " + cellList.stream().map(cellLessonList -> String.format("%-10s", cellLessonList.stream().map(Lesson::getTeacher).collect(Collectors.joining(", ")))) .collect(Collectors.joining(" | ")) + " |"); LOGGER.info("| | " + cellList.stream().map(cellLessonList -> String.format("%-10s", cellLessonList.stream().map(Lesson::getStudentGroup).collect(Collectors.joining(", ")))) .collect(Collectors.joining(" | ")) + " |"); LOGGER.info("|" + "------------|".repeat(roomList.size() + 1)); } List<Lesson> unassignedLessons = lessonList.stream() .filter(lesson -> lesson.getTimeslot() == null || lesson.getRoom() == null) .collect(Collectors.toList()); if (!unassignedLessons.isEmpty()) { LOGGER.info(""); LOGGER.info("Unassigned lessons"); for (Lesson lesson : unassignedLessons) { LOGGER.info(" " + lesson.getSubject() + " - " + lesson.getTeacher() + " - " + lesson.getStudentGroup()); } } } } SolverFactory<TimeTable> solverFactory = SolverFactory.create(new SolverConfig() .withSolutionClass(TimeTable.class) .withEntityClasses(Lesson.class) .withConstraintProviderClass(TimeTableConstraintProvider.class) // The solver runs only for 5 seconds on this small data set. // It's recommended to run for at least 5 minutes ("5m") otherwise. .withTerminationSpentLimit(Duration.ofSeconds(5))); // Load the problem TimeTable problem = generateDemoData(); // Solve the problem Solver<TimeTable> solver = solverFactory.buildSolver(); TimeTable solution = solver.solve(problem); // Visualize the solution printTimetable(solution); package org.acme.schooltimetabling; import java.time.DayOfWeek; import java.time.Duration; import java.time.LocalTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.acme.schooltimetabling.domain.Lesson; import org.acme.schooltimetabling.domain.Room; import org.acme.schooltimetabling.domain.TimeTable; import org.acme.schooltimetabling.domain.Timeslot; import org.acme.schooltimetabling.solver.TimeTableConstraintProvider; import org.optaplanner.core.api.solver.Solver; import org.optaplanner.core.api.solver.SolverFactory; import org.optaplanner.core.config.solver.SolverConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TimeTableApp { private static final Logger LOGGER = LoggerFactory.getLogger(TimeTableApp.class); public static void main(String[] args) { SolverFactory<TimeTable> solverFactory = SolverFactory.create(new SolverConfig() .withSolutionClass(TimeTable.class) .withEntityClasses(Lesson.class) .withConstraintProviderClass(TimeTableConstraintProvider.class) // The solver runs only for 5 seconds on this small data set. // It's recommended to run for at least 5 minutes ("5m") otherwise. .withTerminationSpentLimit(Duration.ofSeconds(10))); // Load the problem TimeTable problem = generateDemoData(); // Solve the problem Solver<TimeTable> solver = solverFactory.buildSolver(); TimeTable solution = solver.solve(problem); // Visualize the solution printTimetable(solution); } public static TimeTable generateDemoData() { List<Timeslot> timeslotList = new ArrayList<>(10); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); timeslotList.add(new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); List<Room> roomList = new ArrayList<>(3); roomList.add(new Room("Room A")); roomList.add(new Room("Room B")); roomList.add(new Room("Room C")); List<Lesson> lessonList = new ArrayList<>(); long id = 0; lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "9th grade")); lessonList.add(new Lesson(id++, "Physics", "M. Curie", "9th grade")); lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "9th grade")); lessonList.add(new Lesson(id++, "Biology", "C. Darwin", "9th grade")); lessonList.add(new Lesson(id++, "History", "I. Jones", "9th grade")); lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade")); lessonList.add(new Lesson(id++, "English", "I. Jones", "9th grade")); lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade")); lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "9th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade")); lessonList.add(new Lesson(id++, "Math", "A. Turing", "10th grade")); lessonList.add(new Lesson(id++, "Physics", "M. Curie", "10th grade")); lessonList.add(new Lesson(id++, "Chemistry", "M. Curie", "10th grade")); lessonList.add(new Lesson(id++, "French", "M. Curie", "10th grade")); lessonList.add(new Lesson(id++, "Geography", "C. Darwin", "10th grade")); lessonList.add(new Lesson(id++, "History", "I. Jones", "10th grade")); lessonList.add(new Lesson(id++, "English", "P. Cruz", "10th grade")); lessonList.add(new Lesson(id++, "Spanish", "P. Cruz", "10th grade")); return new TimeTable(timeslotList, roomList, lessonList); } private static void printTimetable(TimeTable timeTable) { LOGGER.info(""); List<Room> roomList = timeTable.getRoomList(); List<Lesson> lessonList = timeTable.getLessonList(); Map<Timeslot, Map<Room, List<Lesson>>> lessonMap = lessonList.stream() .filter(lesson -> lesson.getTimeslot() != null && lesson.getRoom() != null) .collect(Collectors.groupingBy(Lesson::getTimeslot, Collectors.groupingBy(Lesson::getRoom))); LOGGER.info("| | " + roomList.stream() .map(room -> String.format("%-10s", room.getName())).collect(Collectors.joining(" | ")) + " |"); LOGGER.info("|" + "------------|".repeat(roomList.size() + 1)); for (Timeslot timeslot : timeTable.getTimeslotList()) { List<List<Lesson>> cellList = roomList.stream() .map(room -> { Map<Room, List<Lesson>> byRoomMap = lessonMap.get(timeslot); if (byRoomMap == null) { return Collections.<Lesson>emptyList(); } List<Lesson> cellLessonList = byRoomMap.get(room); if (cellLessonList == null) { return Collections.<Lesson>emptyList(); } return cellLessonList; }) .collect(Collectors.toList()); LOGGER.info("| " + String.format("%-10s", timeslot.getDayOfWeek().toString().substring(0, 3) + " " + timeslot.getStartTime()) + " | " + cellList.stream().map(cellLessonList -> String.format("%-10s", cellLessonList.stream().map(Lesson::getSubject).collect(Collectors.joining(", ")))) .collect(Collectors.joining(" | ")) + " |"); LOGGER.info("| | " + cellList.stream().map(cellLessonList -> String.format("%-10s", cellLessonList.stream().map(Lesson::getTeacher).collect(Collectors.joining(", ")))) .collect(Collectors.joining(" | ")) + " |"); LOGGER.info("| | " + cellList.stream().map(cellLessonList -> String.format("%-10s", cellLessonList.stream().map(Lesson::getStudentGroup).collect(Collectors.joining(", ")))) .collect(Collectors.joining(" | ")) + " |"); LOGGER.info("|" + "------------|".repeat(roomList.size() + 1)); } List<Lesson> unassignedLessons = lessonList.stream() .filter(lesson -> lesson.getTimeslot() == null || lesson.getRoom() == null) .collect(Collectors.toList()); if (!unassignedLessons.isEmpty()) { LOGGER.info(""); LOGGER.info("Unassigned lessons"); for (Lesson lesson : unassignedLessons) { LOGGER.info(" " + lesson.getSubject() + " - " + lesson.getTeacher() + " - " + lesson.getStudentGroup()); } } } } INFO | | Room A | Room B | Room C | INFO |------------|------------|------------|------------| INFO | MON 08:30 | English | Math | | INFO | | I. Jones | A. Turing | | INFO | | 9th grade | 10th grade | | INFO |------------|------------|------------|------------| INFO | MON 09:30 | History | Physics | | INFO | | I. Jones | M. Curie | | INFO | | 9th grade | 10th grade | | ... Solving started: time spent (33), best score (-8init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0). ... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), score calculation speed (459/sec), step total (4). ... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), score calculation speed (28949/sec), step total (28398). ... Solving ended: time spent (5000), best score (0hard/0soft), score calculation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE). package org.acme.optaplanner.solver; import java.time.DayOfWeek; import java.time.LocalTime; import javax.inject.Inject; import io.quarkus.test.junit.QuarkusTest; import org.acme.optaplanner.domain.Lesson; import org.acme.optaplanner.domain.Room; import org.acme.optaplanner.domain.TimeTable; import org.acme.optaplanner.domain.Timeslot; import org.junit.jupiter.api.Test; import org.optaplanner.test.api.score.stream.ConstraintVerifier; @QuarkusTest class TimeTableConstraintProviderTest { private static final Room ROOM = new Room("Room1"); private static final Timeslot TIMESLOT1 = new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9,0), LocalTime.NOON); private static final Timeslot TIMESLOT2 = new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9,0), LocalTime.NOON); @Inject ConstraintVerifier<TimeTableConstraintProvider, TimeTable> constraintVerifier; @Test void roomConflict() { Lesson firstLesson = new Lesson(1, "Subject1", "Teacher1", "Group1"); Lesson conflictingLesson = new Lesson(2, "Subject2", "Teacher2", "Group2"); Lesson nonConflictingLesson = new Lesson(3, "Subject3", "Teacher3", "Group3"); firstLesson.setRoom(ROOM); firstLesson.setTimeslot(TIMESLOT1); conflictingLesson.setRoom(ROOM); conflictingLesson.setTimeslot(TIMESLOT1); nonConflictingLesson.setRoom(ROOM); nonConflictingLesson.setTimeslot(TIMESLOT2); constraintVerifier.verifyThat(TimeTableConstraintProvider::roomConflict) .given(firstLesson, conflictingLesson, nonConflictingLesson) .penalizesBy(1); } } package com.exmaple.optaplanner.rest; import java.time.DayOfWeek; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import io.quarkus.test.junit.QuarkusTest; import com.exmaple.optaplanner.domain.Room; import com.exmaple.optaplanner.domain.Timeslot; import com.exmaple.optaplanner.domain.Lesson; import com.exmaple.optaplanner.domain.TimeTable; import com.exmaple.optaplanner.rest.TimeTableResource; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @QuarkusTest public class TimeTableResourceTest { @Inject TimeTableResource timeTableResource; @Test @Timeout(600_000) public void solve() { TimeTable problem = generateProblem(); TimeTable solution = timeTableResource.solve(problem); assertFalse(solution.getLessonList().isEmpty()); for (Lesson lesson : solution.getLessonList()) { assertNotNull(lesson.getTimeslot()); assertNotNull(lesson.getRoom()); } assertTrue(solution.getScore().isFeasible()); } private TimeTable generateProblem() { List<Timeslot> timeslotList = new ArrayList<>(); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(8, 30), LocalTime.of(9, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9, 30), LocalTime.of(10, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(10, 30), LocalTime.of(11, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(13, 30), LocalTime.of(14, 30))); timeslotList.add(new Timeslot(DayOfWeek.MONDAY, LocalTime.of(14, 30), LocalTime.of(15, 30))); List<Room> roomList = new ArrayList<>(); roomList.add(new Room("Room A")); roomList.add(new Room("Room B")); roomList.add(new Room("Room C")); List<Lesson> lessonList = new ArrayList<>(); lessonList.add(new Lesson(101L, "Math", "B. May", "9th grade")); lessonList.add(new Lesson(102L, "Physics", "M. Curie", "9th grade")); lessonList.add(new Lesson(103L, "Geography", "M. Polo", "9th grade")); lessonList.add(new Lesson(104L, "English", "I. Jones", "9th grade")); lessonList.add(new Lesson(105L, "Spanish", "P. Cruz", "9th grade")); lessonList.add(new Lesson(201L, "Math", "B. May", "10th grade")); lessonList.add(new Lesson(202L, "Chemistry", "M. Curie", "10th grade")); lessonList.add(new Lesson(203L, "History", "I. Jones", "10th grade")); lessonList.add(new Lesson(204L, "English", "P. Cruz", "10th grade")); lessonList.add(new Lesson(205L, "French", "M. Curie", "10th grade")); return new TimeTable(timeslotList, roomList, lessonList); } } The solver runs only for 5 seconds to avoid a HTTP timeout in this simple implementation. It's recommended to run for at least 5 minutes ("5m") otherwise. quarkus.optaplanner.solver.termination.spent-limit=5s Effectively disable this termination in favor of the best-score-limit %test.quarkus.optaplanner.solver.termination.spent-limit=1h %test.quarkus.optaplanner.solver.termination.best-score-limit=0hard/*soft ... Solving ended: ..., score calculation speed (29455/sec), quarkus.log.category."org.optaplanner".level=debug ... Solving started: time spent (67), best score (-20init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0). ... CH step (0), time spent (128), score (-18init/0hard/0soft), selected move count (15), picked move ([Math(101) {null -> Room A}, Math(101) {null -> MONDAY 08:30}]). ... CH step (1), time spent (145), score (-16init/0hard/0soft), selected move count (15), picked move ([Physics(102) {null -> Room A}, Physics(102) {null -> MONDAY 09:30}]). <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version><MICROMETER_VERSION></version> </dependency> import io.micrometer.core.instrument.Metrics; import io.micrometer.prometheus.PrometheusConfig; import io.micrometer.prometheus.PrometheusMeterRegistry; PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT); try { HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); server.createContext("/prometheus", httpExchange -> { String response = prometheusRegistry.scrape(); httpExchange.sendResponseHeaders(200, response.getBytes().length); try (OutputStream os = httpExchange.getResponseBody()) { os.write(response.getBytes()); } }); new Thread(server::start).start(); } catch (IOException e) { throw new RuntimeException(e); } Metrics.addRegistry(prometheusRegistry); solve(); } withTerminationSpentLimit(Duration.ofMinutes(5)));