This is the second day of my participation in Gwen Challenge
Contents summary
This chapter is the first introduction to OptaPlanner, a Demo similar to Java’s HelloWorld. For the “black on a red background” part of this article, keep this concept in mind and we’ll cover it in more detail in a later chapter.
OptaPlanner introduction
OptaPlanner is a lightweight, embeddable constraint satisfaction engine for solving planning problems that cleverly combines scores to compare each solution, with the highest score as the best solution. OptaPlanner itself supports a variety of algorithms, has high scalability and usability, a relatively active community environment abroad, and stable version iteration. There is no point in talking more. Here is the video introduction for you to understand OptaPlanner intuitively (the subtitles were translated by myself, please forgive me for being inaccurate).
OptaPlanner video introduction
What problems are you going to solve?
Today we are going to optimize a school schedule for high school students and teachers.
We will use the OptaPlanner algorithm to automatically assign courses to the corresponding classrooms and classrooms, and follow the rules of hard and soft constraints as follows: Hard constraints
- One room can have up to one class at a time.
- A teacher can teach up to one class at a time.
- A student can take up to one class at a time.
Soft constraints
- A teacher likes to teach every lesson in one room.
- A teacher likes to teach consecutive lessons and doesn’t like gaps between lessons.
Mathematically, school scheduling is a NP-hard problem. Simply using an exhaustive algorithm to iterate over all possible combinations would take millions of years, even on a supercomputer, for a small data set. Fortunately, ai constraint solvers like OptaPlanner have advanced algorithms that can provide a near-optimal solution in a reasonable amount of time.
The preparatory work
JDK, MAVEN and editor:
- JDK 8 or later
- Maven 3.2 + or Gradle4 +
- An IDE, such as IntelliJ IDEA, VSCode or Eclipse
Project creation and Maven configuration
To initialize an application with IDEA:
- Spring Web (spring-boot-starter-web)
MAVEN configuration:
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>{version-org-spring-framework-boot}</version>
</parent>
<groupId>com.example</groupId>
<artifactId>constraint-solving-ai-optaplanner</artifactId>
<version>0.1.0 from - the SNAPSHOT</version>
<name>Constraint Solving AI with OptaPlanner</name>
<description>A Spring Boot OptaPlanner example to generate a school timetable.</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.optaplanner</groupId>
<artifactId>optaplanner-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.optaplanner</groupId>
<artifactId>optaplanner-spring-boot-starter</artifactId>
<version>{project-version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Copy the code
Business model the problem
Our goal is to assign each class to a time period and a room. So you need to create courses. The following class diagram:
Timeslot
Time class indicates the time period of class, for example, 10:30 to 11:30 on Monday or 13:30 to 14:30 on Tuesday. For simplicity, all time slots have the same length of time, with no time slots during lunch or other breaks. A time period has no date, because the high school curriculum only repeats once a week. Therefore, there is no need to add a date attribute.
public class Timeslot {
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
private Timeslot(a) {}public Timeslot(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
this.dayOfWeek = dayOfWeek;
this.startTime = startTime;
this.endTime = endTime;
}
@Override
public String toString(a) {
return dayOfWeek + "" + startTime.toString();
}
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// Getters and setters
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
public DayOfWeek getDayOfWeek(a) {
return dayOfWeek;
}
public LocalTime getStartTime(a) {
return startTime;
}
public LocalTime getEndTime(a) {
returnendTime; }}Copy the code
Because the Timeslot object does not change during the solution, a Timeslot is called a Problem Fact. Such classes do not require any OptaPlanner annotations. Note: The toString() method keeps the output short, so it’s easier to read OptaPlanner’s DEBUG or TRACE logs.
Room
The room represents the location of the lecture, for example, room A or room B. For simplicity, there are no capacity limits in any of the rooms and they can accommodate all classes.
public class Room {
private String name;
private Room(a) {}public Room(String name) {
this.name = name;
}
@Override
public String toString(a) {
return name;
}
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// Getters and setters
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
public String getName(a) {
returnname; }}Copy the code
Room objects don’t change during solution, so Room is also a Problem Fact.
Lesson
In a Lesson represented by the Lesson class, a teacher teaches a subject to a group of students, for example, math by A. Uring in grade 9 or chemistry by M.Curie in grade 10. If a subject is taught by the same teacher to the same group of students many times a week, there will be multiple instances of Lesson, which can only be distinguished by ID. For example, grade 9 students have six math classes a week. During the solution process, OptaPlanner changes the timeSlot and Room fields of the Lesson class to assign each Lesson to a time period and a room. Because OptaPlanner changed these fields, Lesson is a Planning Entity.
One coursetimeslot
androom
The field is empty at initialization. OptaPlanner changes the values of these fields as it solves. These fields are calledPlanning Variable. In order for OptaPlanner to identify them,timeslot
和room
Each field requires one@PlanningVariable
Annotation. Their containing classesLesson
, need a@PlanningEntity
Annotation.
@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(a) {}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(a) {
return subject + "(" + id + ")";
}
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// Getters and setters
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
public Long getId(a) {
return id;
}
public String getSubject(a) {
return subject;
}
public String getTeacher(a) {
return teacher;
}
public String getStudentGroup(a) {
return studentGroup;
}
public Timeslot getTimeslot(a) {
return timeslot;
}
public void setTimeslot(Timeslot timeslot) {
this.timeslot = timeslot;
}
public Room getRoom(a) {
return room;
}
public void setRoom(Room room) {
this.room = room; }}Copy the code
Define constraints and calculate scores
The score represents the quality of a solution, and the higher the better. OptaPlanner looks for the optimal solution, that is, the solution with the highest score found in the time available, and it may be the optimal solution. Because this use case has hard and soft constraints, the HardSoftScore class is used to represent the score.
- Hard constraints cannot be broken. For example, one room can have up to one class at a time.
- Soft constraints should not be broken. For example, a teacher would prefer to teach in one room.
Hard constraints are weighted with other hard constraints. Soft constraints are also weighted. Compared with other soft constraints, hard constraints are always larger than soft constraints, regardless of their respective weights. The two are of different levels. Create a TimeTableConstraintProvider. Java classes to perform the incremental fraction calculations. It uses the ConstraintStream API of OptaPlanner, which is inspired by Java 8 Streams and SQL.
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 hold up to one class at a time.
// Select a course...
return constraintFactory.from(Lesson.class)
/ /... And pair with another course......
.join(Lesson.class,
/ /... During the same period of time...
Joiners.equal(Lesson::getTimeslot),
/ /... In the same room...
Joiners.equal(Lesson::getRoom),
/ /... And this pair is unique (different ids, no reverse pair).
Joiners.lessThan(Lesson::getId))
// Then punish each pair with a hard weight.
.penalize("Room conflict", HardSoftScore.ONE_HARD);
}
private Constraint teacherConflict(ConstraintFactory constraintFactory) {
// A teacher can teach up to one subject at a 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 have no more than one lesson at a 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); }}Copy the code
Create PlanningSolution class
Creating the TimeTable class wraps all Timeslot, Room, and Lesson instances of a single data set. Also, because it includes all courses, each course has a specific state of planning variables, so it is a planning solution, and it has a score. The Timeable class can be thought of as the entry point for OptaPlanner to manipulate data during the solving process. All constant data, variable modification, and fractional calculations are performed through this class. The scores are as follows:
- If the course is still unassigned, then it is an uninitialized solution, for example, a score of
-4init/0hard/0soft
The solution to the problem. - If it breaks hard constraints, then it is an unworkable solution, for example, a score of
-2hard/3soft
The solution to the problem. - If it complies with all the hard constraints, then it is a viable solution, for example, a score of
0hard/7soft
The solution to the problem.
@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(a) {}public TimeTable(List
timeslotList, List
roomList, List
lessonList)
{
this.timeslotList = timeslotList;
this.roomList = roomList;
this.lessonList = lessonList;
}
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// Getters and setters
/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
public List<Timeslot> getTimeslotList(a) {
return timeslotList;
}
public List<Room> getRoomList(a) {
return roomList;
}
public List<Lesson> getLessonList(a) {
return lessonList;
}
public HardSoftScore getScore(a) {
returnscore; }}Copy the code
The TimeTable class has an @PlanningSolution annotation, so OptaPlanner knows that this class contains all the input and output data. Specifically, this class is the input to the problem:
timeslotList
field- This is a
Problem facts
Because they don’t change during the course of the problem.
- This is a
- RoomList field
- This is a
Problem facts
Because they don’t change during the course of the problem.
- This is a
lessonList
field- This is a
Planning entities
Because they change as we go along. lesson
timeslot
androom
The value of the field is usually still empty, so it is not allocated. They arePlanning variables
.- Other fields, such as
subject
.teacher
,studentGroup
, are filled in. These fields areProblem Properties
.
Of course, this class is also the output of the solution.
lessonList
Field, eachLesson
Instances have non-empty TimesLot and Room room fields after resolutionscore
The score field, which represents the quality of the output solution, for example,0hard/-5soft
Create the solution business class
Now let’s put everything together and create a REST service. But solving planning problems on REST threads can cause HTTP timeouts. Thus, the Spring Boot launcher injects an instance of SolverManager, which runs the solver in a separate thread pool and can solve multiple data sets in parallel.
@RestController
@RequestMapping("/timeTable")public class TimeTableController {
@Autowired
private SolverManager<TimeTable, UUID> solverManager;
@PostMapping("/solve")
public TimeTable solve(@RequestBody TimeTable problem) {
UUID problemId = UUID.randomUUID();
// Submit the problem and start solving
SolverJob<TimeTable, UUID> solverJob = solverManager.solve(problemId, problem);
TimeTable solution;
try {
// Wait for the solution to end
solution = solverJob.getFinalBestSolution();
} catch (InterruptedException | ExecutionException e) {
throw new IllegalStateException("Solving failed.", e);
}
returnsolution; }}Copy the code
For simplicity, the implementation waits for the solver to complete, which still causes HTTP timeouts. The full implementation avoids HTTP timeouts more elegantly.
Set termination time
If there is no termination setting or termination event, the solver runs forever. To avoid this, limit the solution time to 5 seconds. This is short enough to avoid HTTP timeouts. Create the src/main/resources/application.properties file:
optaplanner.solver.termination.spent-limit=5s
Copy the code
Start the program
Use the SpringBoot Application class to start it.
@SpringBootApplication
public class TimeTableSpringBootApp {
public static void main(String[] args) { SpringApplication.run(TimeTableSpringBootApp.class, args); }}Copy the code
Try to solve
After starting the service, we access it through PostMan. URL: http://localhost:8080/timeTable/solve to solve the JSON data:
{
"timeslotList": [{"dayOfWeek": "MONDAY"."startTime": "08:30:00"."endTime": "09:30:00"
},
{
"dayOfWeek": "MONDAY"."startTime": "09:30:00"."endTime": "10:30:00"}]."roomList": [{"name": "Room A"
},
{
"name": "Room B"}]."lessonList": [{"id": 1."subject": "Math"."teacher": "A. Turing"."studentGroup": "9th grade"
},
{
"id": 2."subject": "Chemistry"."teacher": "M. Curie"."studentGroup": "9th grade"
},
{
"id": 3."subject": "French"."teacher": "M. Curie"."studentGroup": "10th grade"
},
{
"id": 4."subject": "History"."teacher": "I. Jones"."studentGroup": "10th grade"}}]Copy the code
After about 5 seconds, the service returns an output based on the termination time defined in application.properties.
Result output:
{
"timeslotList": [{"dayOfWeek": "MONDAY"."startTime": "08:30:00"."endTime": "09:30:00"
},
{
"dayOfWeek": "MONDAY"."startTime": "09:30:00"."endTime": "10:30:00"}]."roomList": [{"name": "Room A"
},
{
"name": "Room B"}]."lessonList": [{"id": 1."subject": "Math"."teacher": "A. Turing"."studentGroup": "9th grade"."timeslot": {
"dayOfWeek": "MONDAY"."startTime": "08:30:00"."endTime": "09:30:00"
},
"room": {
"name": "Room A"}}, {"id": 2."subject": "Chemistry"."teacher": "M. Curie"."studentGroup": "9th grade"."timeslot": {
"dayOfWeek": "MONDAY"."startTime": "09:30:00"."endTime": "10:30:00"
},
"room": {
"name": "Room A"}}, {"id": 3."subject": "French"."teacher": "M. Curie"."studentGroup": "10th grade"."timeslot": {
"dayOfWeek": "MONDAY"."startTime": "08:30:00"."endTime": "09:30:00"
},
"room": {
"name": "Room B"}}, {"id": 4."subject": "History"."teacher": "I. Jones"."studentGroup": "10th grade"."timeslot": {
"dayOfWeek": "MONDAY"."startTime": "09:30:00"."endTime": "10:30:00"
},
"room": {
"name": "Room B"}}]."score": "0hard/0soft"
}
Copy the code
As you can see, the program assigns all four classes to one of two periods and one of two rooms. Notice also that it meets all the hard constraints. For example, the two classes at M. Curie’s are at different times. On the server side, the informational log shows what OptaPlanner did in those five seconds.
. 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).
Copy the code
The log configuration
When we add constraints to ConstraintProvider, note the speed at which the score is computed in the information log and evaluate the performance impact after solving the same time.
. Solving ended: ... , score calculation speed (29455/sec), ...
Copy the code
To see how OptaPlanner solves problems internally, change logging in the application.properties file or with the -D system property.
logging.level.org.optaplanner=debug
Copy the code
Use the debug log to display each step:
. 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}])....Copy the code
conclusion
Give ourselves a thumbs up! We just developed a Spring application with OptaPlanner. This article code address: gitee.com/yqsoftware/…
homework
Through this article, we have been able to build a simple solution program, we mentioned in the article hard constraints/soft constraints, only the implementation of hard constraints, you can try to add soft constraints to it, for example: soft constraints: M. Curie likes afternoon classes.
conclusion
This time, you have been able to build a simple solution program. In the next chapter, I will study various examples to deepen my understanding of OptaPlanner. Finally, we will systematically study each deep application. Finally writing is not easy, trouble to give a small praise, pay attention to me, we learn together ~~~