本指南将引导您完成使用 OptaPlanner 的约束求解人工智能 (AI)创建简单 Java 应用程序的过程。
1. 建造目标
您将构建一个命令行应用程序,为学生和教师优化学校时间表:
...
| | ROOM A | ROOM B | ROOM C |
|------------|------------|------------|------------|
| MON 08:30 | Math | | Physics |
| | A. Turing | | M. Curie |
| | 10th grade | | 9th grade |
|------------|------------|------------|------------|
| MON 09:30 | Math | Geography | |
| | A. Turing | C. Darwin | |
| | 9th grade | 10th grade | |
|------------|------------|------------|------------|
复制代码
您的应用程序将使用 AI 自动为实例分配Lesson
实例以遵守硬调度约束和软调度Timeslot
约束*,*例如:Room
一个房间最多可以同时上一节课。
一位老师最多可以同时教一堂课。
一个学生最多可以同时上一堂课。
老师更喜欢在同一个房间里教授所有课程。
老师更喜欢按顺序教授课程,不喜欢课程之间的间隙。
学生不喜欢同一主题的连续课程。
从数学上讲,学校时间表是一个 NP-hard 问题。这意味着很难扩展。对于一个不平凡的数据集,即使在超级计算机上,简单地暴力迭代所有可能的组合也需要数百万年的时间。幸运的是,OptaPlanner 等 AI 约束求解器拥有先进的算法,可以在合理的时间内提供接近最优的解决方案。
2.方案源码
按照下一部分中的说明逐步创建应用程序(推荐)。
或者,查看完整的示例:
1.完成以下任务之一: 1.克隆 Git 存储库:$ git clone https://github.com/kiegroup/optaplanner-quickstarts 2.下载档案。
在 hello-world 目录中找到解决方案。
按照 README 文件中的说明运行应用程序。
3. 先决条件
要完成本指南,您需要:
4.构建文件和依赖
创建一个 Maven 或 Gradle 构建文件并添加这些依赖项:
optaplanner-core
(编译范围)解决学校时间表问题。
optaplanner-test
(测试范围)以 JUnit 测试学校的时间表约束。
一个日志实现,例如logback-classic
(运行时范围),以查看 OptaPlanner 正在做什么。
如果选择 Maven,您的pom.xml
文件包含以下内容:
<?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>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.optaplanner</groupId>
<artifactId>optaplanner-bom</artifactId>
<version>8.27.0.Final</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</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>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<mainClass>org.acme.schooltimetabling.TimeTableApp</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
复制代码
另一方面,在 Gradle 中,您的build.gradle
文件具有以下内容:
plugins {
id "java"
id "application"
}
def optaplannerVersion = "8.27.0.Final"
def logbackVersion = "1.2.9"
group = "org.acme"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
implementation platform("org.optaplanner:optaplanner-bom:${optaplannerVersion}")
implementation "org.optaplanner:optaplanner-core"
testImplementation "org.optaplanner:optaplanner-test"
runtimeOnly "ch.qos.logback:logback-classic:${logbackVersion}"
}
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"
}
}
复制代码
5. 为领域对象建模
您的目标是将每节课分配到一个时间段和一个房间。您将创建这些类:
学校时间表类图纯
5.1.时间槽
例如,Timeslot
班级代表教授课程的时间间隔,Monday 10:30 - 11:30
或Tuesday 13:30 - 14:30
。为简单起见,所有时间段都具有相同的持续时间,并且在午餐或其他休息时间没有时间段。
时间段没有日期,因为高中的时间表每周都会重复。所以没有必要进行持续的计划。
创建src/main/java/org/acme/schooltimetabling/domain/Timeslot.java
类:
package org.acme.schooltimetabling.domain;
import java.time.DayOfWeek;
import java.time.LocalTime;
public class Timeslot {
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public Timeslot() {
}
public Timeslot(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
this.dayOfWeek = dayOfWeek;
this.startTime = startTime;
this.endTime = endTime;
}
public DayOfWeek getDayOfWeek() {
return dayOfWeek;
}
public LocalTime getStartTime() {
return startTime;
}
public LocalTime getEndTime() {
return endTime;
}
@Override
public String toString() {
return dayOfWeek + " " + startTime;
}
}
复制代码
因为Timeslot
在求解过程中没有实例发生变化,Timeslot
所以称为问题事实。此类类不需要任何 OptaPlanner 特定的注释。
请注意,该toString()
方法使输出保持简短,因此更容易阅读 OptaPlannerDEBUG
或TRACE
日志,如下所示。
5.2. 教室
类表示教授课程的Room
位置,例如,Room A
或Room B
。为简单起见,所有房间都没有容量限制,可以容纳所有课程。
创建src/main/java/org/acme/schooltimetabling/domain/Room.java
类:
package org.acme.schooltimetabling.domain;
public class Room {
private String name;
public Room() {
}
public Room(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return name;
}
}
复制代码
Room
实例在求解过程中不会改变,所以Room
也是一个问题事实。
5.3. 课程
在以班级为代表的Lesson
课程中,教师向一群学生教授一门学科,例如,Math by A.Turing for 9th grade
或Chemistry by M.Curie for 10th grade
。如果同一位老师每周向同一学生组多次教授同一科目,则有多个Lesson
实例只能通过 区分id
。例如,9 年级每周有六节数学课。
在求解过程中,OptaPlanner 会更改课程的timeslot
和room
字段Lesson
,将每节课分配到一个时间段和一个房间。因为 OptaPlanner 更改了这些字段,Lesson
所以是一个计划实体:
学校时间表班级图注解
上图中的大多数字段都包含输入数据,橙色字段除外:课程timeslot
和room
字段null
在输入数据中未分配( ),在输出数据中未分配(未分配null
)。OptaPlanner 在求解过程中更改这些字段。这些字段称为计划变量。为了让 OptaPlanner 识别它们,timeslot
和room
字段都需要@PlanningVariable
注释。它们的包含类 ,Lesson
需要@PlanningEntity
注释。
创建src/main/java/org/acme/schooltimetabling/domain/Lesson.java
类:
package org.acme.schooltimetabling.domain;
import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.api.domain.lookup.PlanningId;
import org.optaplanner.core.api.domain.variable.PlanningVariable;
@PlanningEntity
public class Lesson {
@PlanningId
private Long id;
private String subject;
private String teacher;
private String studentGroup;
@PlanningVariable(valueRangeProviderRefs = "timeslotRange")
private Timeslot timeslot;
@PlanningVariable(valueRangeProviderRefs = "roomRange")
private Room room;
public Lesson() {
}
public Lesson(Long id, String subject, String teacher, String studentGroup) {
this.id = id;
this.subject = subject;
this.teacher = teacher;
this.studentGroup = studentGroup;
}
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;
}
@Override
public String toString() {
return subject + "(" + id + ")";
}
}
复制代码
该类Lesson
具有@PlanningEntity
注释,因此 OptaPlanner 知道该类在求解过程中会发生变化,因为它包含一个或多个计划变量。
该timeslot
字段具有@PlanningVariable
注释,因此 OptaPlanner 知道它可以更改其值。为了找到Timeslot
分配给该字段的潜在实例,OptaPlanner 使用该valueRangeProviderRefs
属性连接到一个值范围提供程序(稍后解释),该提供程序提供一个List
可供选择的值。
出于同样的原因,该room
字段也有注释。@PlanningVariable
第一次确定@PlanningVariable
任意约束解决用例的字段通常具有挑战性。阅读领域建模指南以避免常见的陷阱。
6. 定义约束并计算分数
分数代表特定解决方案的质量。越高越好。OptaPlanner 寻找最佳解决方案,即在可用时间内找到的得分最高的解决方案。这可能是最佳解决方案。
因为这个用例有硬约束和软约束,所以使用HardSoftScore
类来表示分数:
硬约束相对于其他硬约束进行加权。软约束也被加权,相对于其他软约束。 不管它们各自的权重如何,硬约束总是超过软约束。
要计算分数,您可以实现一个EasyScoreCalculator
类:
public class TimeTableEasyScoreCalculator implements EasyScoreCalculator<TimeTable, HardSoftScore> {
@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 optaplanner-quickstarts code
return HardSoftScore.of(hardScore, softScore);
}
}
不幸的是**,这不能很好地扩展**,因为它是非增量的:每次将课程分配到不同的时间段或房间时,都会重新评估所有课程以计算新分数。
相反,创建一个src/main/java/org/acme/schooltimetabling/solver/TimeTableConstraintProvider.java类来执行增量分数计算。它使用受 Java Streams 和 SQL 启发的 OptaPlanner 的 ConstraintStream API:
package org.acme.schooltimetabling.solver;
import org.acme.schooltimetabling.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 optaplanner-quickstarts code
};
}
private Constraint roomConflict(ConstraintFactory constraintFactory) {
// A room can accommodate at most one lesson at the same time.
// Select a lesson ...
return constraintFactory
.forEach(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.forEach(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.forEach(Lesson.class)
.join(Lesson.class,
Joiners.equal(Lesson::getTimeslot),
Joiners.equal(Lesson::getStudentGroup),
Joiners.lessThan(Lesson::getId))
.penalize("Student group conflict", HardSoftScore.ONE_HARD);
}
}
复制代码
比 O (n) 比 O ( n²)ConstraintProvider
好一个数量级。EasyScoreCalculator
7. 在规划解决方案中收集领域对象
TimeTable
包装单个数据集的所有Timeslot
、Room
和实例。Lesson
此外,因为它包含所有课程,每个课程都有一个特定的规划变量状态,所以它是一个规划解决方案,它有一个分数:
如果课程仍未分配,则它是未初始化的解决方案,例如,具有分数的解决方案-4init/0hard/0soft
。
如果它打破了硬约束,那么它就是一个不可行的解决方案,例如,一个分数为 的解决方案-2hard/-3soft
。
如果它遵守所有的硬约束,那么它是一个可行的解决方案,例如,得分为 的解决方案0hard/-7soft
。
创建src/main/java/org/acme/schooltimetabling/domain/TimeTable.java
类:
package org.acme.schooltimetabling.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;
public TimeTable() {
}
public TimeTable(List<Timeslot> timeslotList, List<Room> roomList, List<Lesson> lessonList) {
this.timeslotList = timeslotList;
this.roomList = roomList;
this.lessonList = lessonList;
}
public List<Timeslot> getTimeslotList() {
return timeslotList;
}
public List<Room> getRoomList() {
return roomList;
}
public List<Lesson> getLessonList() {
return lessonList;
}
public HardSoftScore getScore() {
return score;
}
}
复制代码
该类TimeTable
具有@PlanningSolution
注释,因此 OptaPlanner 知道该类包含所有输入和输出数据。
具体来说,这个类是问题的输入:
timeslotList
具有所有时隙的字段
这是问题事实的列表,因为它们在求解过程中不会改变。
roomList
包含所有房间的字段
这是问题事实的列表,因为它们在求解过程中不会改变。
一个lessonList
包含所有课程的领域
这是计划实体的列表,因为它们在求解过程中会发生变化。
每个Lesson
:
timeslot
和字段的值room
通常仍然是null
,因此未分配。他们正在计划变量。
subject
填写其他字段,例如teacher
和studentGroup
。这些字段是问题属性。
然而,这个类也是解决方案的输出:
7.1.价值范围提供者
该timeslotList
字段是一个值范围提供程序。它包含Timeslot
OptaPlanner 可以从中选择以分配给实例timeslot
字段的Lesson
实例。该timeslotList
字段通过将属性的值与类中注释的属性的值匹配@ValueRangeProvider
来将@PlanningVariable
与连接起来。@ValueRangeProvider``id``valueRangeProviderRefs``@PlanningVariable``Lesson
按照同样的逻辑,该roomList
字段也有一个@ValueRangeProvider
注解。
7.2. 问题事实和计划实体属性
此外,OptaPlanner 需要知道Lesson
它可以更改哪些实例以及如何检索您Timeslot
的.Room``TimeTableConstraintProvider
和字段有一个注释,因此您timeslotList
可以从这些实例中进行选择。roomList``@ProblemFactCollectionProperty``TimeTableConstraintProvider
有lessonList
一个@PlanningEntityCollectionProperty
注释,因此 OptaPlanner 可以在求解过程中更改它们,您也可以从中TimeTableConstraintProvider
进行选择。
8. 创建应用程序
现在您已准备好将所有内容放在一起并创建一个 Java 应用程序。该main()
方法执行以下任务:
1.创建SolverFactory
以构建Solver
每个数据集。
2.加载数据集。
3.解决它Solver.solve()
。
4.可视化该数据集的解决方案。
通常,应用程序有一个单独的实例来为每个要解决的问题数据集SolverFactory
构建一个新实例。Solver
ASolverFactory
是线程安全的,但 aSolver
不是。在这种情况下,只有一个数据集,所以只有一个Solver
实例。
创建src/main/java/org/acme/schooltimetabling/TimeTableApp.java
类:
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 dataset.
// 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());
}
}
}
}
该main()方法首先创建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 dataset.
// It's recommended to run for at least 5 minutes ("5m") otherwise.
.withTerminationSpentLimit(Duration.ofSeconds(5)));
这将注册您之前创建的所有@PlanningSolution类、@PlanningEntity类和类。ConstraintProvider
如果没有终止设置或terminationEarly()事件,求解器将永远运行。为避免这种情况,求解器将求解时间限制为 5 秒。
五秒钟后,该main()方法加载问题、解决问题并打印解决方案:
// Load the problem
TimeTable problem = generateDemoData();
// Solve the problem
Solver<TimeTable> solver = solverFactory.buildSolver();
TimeTable solution = solver.solve(problem);
// Visualize the solution
printTimetable(solution);
复制代码
该solve()
方法不会立即返回。它运行五秒钟,然后返回最佳解决方案。
OptaPlanner 返回在可用终止时间内找到*的最佳解决方案。*由于NP-hard 问题的性质,最好的解决方案可能不是最优的,尤其是对于较大的数据集。增加终止时间以可能找到更好的解决方案。
该generateDemoData()
方法生成要解决的学校时间表问题。
该printTimetable()
方法将时间表漂亮地打印到控制台,因此很容易直观地确定它是否是一个好的时间表。
8.1.配置日志记录
要在控制台中查看任何输出,必须正确配置日志记录。
创建src/main/resource/logback.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%-12.12t] %-5p %m%n</pattern>
</encoder>
</appender>
<logger name="org.optaplanner" level="info"/>
<root level="info">
<appender-ref ref="consoleAppender" />
</root>
</configuration>
9. 运行应用程序
9.1.在 IDE 中运行应用程序
将该TimeTableApp
类作为普通 Java 应用程序的主类运行:
| | ROOM A | ROOM B | ROOM C |
|------------|------------|------------|------------|
| MON 08:30 | Math | | Physics |
| | A. Turing | | M. Curie |
| | 10th grade | | 9th grade |
|------------|------------|------------|------------|
| MON 09:30 | Math | Geography | |
| | A. Turing | C. Darwin | |
| | 9th grade | 10th grade | |
|------------|------------|------------|------------|
复制代码
验证控制台输出。它是否符合所有硬约束?如果您注释掉 中的roomConflict
约束会发生什么TimeTableConstraintProvider
?
info
日志显示了 OptaPlanner 在这五秒钟内所做的事情:
... 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).
复制代码
9.2. 测试应用程序
一个好的应用程序包括测试覆盖率。
9.2.1.测试约束
要单独测试每个约束,请ConstraintVerifier
在单元测试中使用 a。这会独立于其他测试来测试每个约束的极端情况,从而在添加具有适当测试覆盖率的新约束时降低维护。
创建src/test/java/org/acme/schooltimetabling/solver/TimeTableConstraintProviderTest.java
类:
package org.acme.schooltimetabling.solver;
import java.time.DayOfWeek;
import java.time.LocalTime;
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.junit.jupiter.api.Test;
import org.optaplanner.test.api.score.stream.ConstraintVerifier;
class TimeTableConstraintProviderTest {
private static final Room ROOM1 = new Room("Room1");
private static final Timeslot TIMESLOT1 = new Timeslot(DayOfWeek.MONDAY, LocalTime.NOON);
private static final Timeslot TIMESLOT2 = new Timeslot(DayOfWeek.TUESDAY, LocalTime.NOON);
ConstraintVerifier<TimeTableConstraintProvider, TimeTable> constraintVerifier = ConstraintVerifier.build(
new TimeTableConstraintProvider(), TimeTable.class, Lesson.class);
@Test
void roomConflict() {
Lesson firstLesson = new Lesson(1, "Subject1", "Teacher1", "Group1", TIMESLOT1, ROOM1);
Lesson conflictingLesson = new Lesson(2, "Subject2", "Teacher2", "Group2", TIMESLOT1, ROOM1);
Lesson nonConflictingLesson = new Lesson(3, "Subject3", "Teacher3", "Group3", TIMESLOT2, ROOM1);
constraintVerifier.verifyThat(TimeTableConstraintProvider::roomConflict)
.given(firstLesson, conflictingLesson, nonConflictingLesson)
.penalizesBy(1);
}
}
复制代码
该测试验证了当在同一个房间中给出三节课时,该约束会TimeTableConstraintProvider::roomConflict
以匹配权重作为惩罚1
,其中两节课具有相同的时间段。因此,约束权重10hard
会降低分数-10hard
。
请注意在测试期间如何ConstraintVerifier
忽略约束权重——即使这些约束权重是硬编码的ConstraintProvider
——因为约束权重在投入生产之前会定期变化。这样,约束权重调整不会破坏单元测试。
有关更多信息,请参阅测试约束流。
9.3. 日志记录
在你ConstraintProvider
的. _info
... Solving ended: ..., score calculation speed (29455/sec), ...
要了解 OptaPlanner 如何在内部解决您的问题,请更改logback.xml
文件中的日志记录:
<logger name="org.optaplanner" level="debug"/>
使用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}]).
...
使用trace
日志记录显示每一步和每一步的每一步。
9.4。制作一个独立的应用程序
为了在 IDE 之外轻松运行应用程序,您需要对构建工具的配置进行一些更改。
9.4.1。Maven 中的可执行 JAR
在 Maven 中,将以下内容添加到您的pom.xml
:
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>${version.assembly.plugin}</version>
<configuration>
<finalName>hello-world-run</finalName>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>src/assembly/jar-with-dependencies-and-services.xml</descriptor>
</descriptors>
<archive>
<manifestEntries>
<Main-Class>org.acme.schooltimetabling.TimeTableApp</Main-Class>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
...
</build>
复制代码
...
src/assembly
此外,在名为的目录中创建一个新文件,jar-with-dependencies-and-services.xml
其内容如下:
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
<id>jar-with-dependencies-and-services</id>
<formats>
<format>jar</format>
</formats>
<containerDescriptorHandlers>
<containerDescriptorHandler>
<handlerName>metaInf-services</handlerName>
</containerDescriptorHandler>
</containerDescriptorHandlers>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<useProjectArtifact>true</useProjectArtifact>
<unpack>true</unpack>
<scope>runtime</scope>
</dependencySet>
</dependencySets>
</assembly>
复制代码
这将启用Maven 程序集插件并告诉它执行以下操作:
获取项目的所有依赖项并将它们的类和资源放入一个新的 JAR 中。
如果任何依赖项使用Java SPI,它会正确捆绑所有服务描述符。
如果任何依赖项是多版本 JAR,则将其考虑在内。
将该 JAR 的主类设置为org.acme.schooltimetabling.TimeTableApp
.
使该 JARhello-world-run.jar
在项目的构建目录中可用,很可能是target/
.
这个可执行 JAR 可以像任何其他 JAR 一样运行:
$ mvn clean install
...
$ java -jar target/hello-world-run.jar
9.4.2. Gradle 中的可执行应用程序
在 Gradle 中,将以下内容添加到您的build.gradle
:
application {
mainClass = "org.acme.schooltimetabling.TimeTableApp"
}
构建项目后,您可以在目录中找到包含可运行应用程序的存档build/distributions/
。
10. 总结
恭喜!您刚刚使用 OptaPlanner 开发了一个 Java 应用程序!
如果您遇到任何问题,请查看快速入门源代码。
评论