写点什么

OptaPlanner 快速入门 -helloworld

作者:OptaPlanner
  • 2022 年 9 月 29 日
    广东
  • 本文字数:16105 字

    阅读完需:约 53 分钟

​本指南将引导您完成使用 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.完成以下任务之一: 1.克隆 Git 存储库:$ git clone https://github.com/kiegroup/optaplanner-quickstarts 2.下载档案。

  2. ​在 hello-world 目录中找到解决方案。

  3. 按照 README 文件中的说明运行应用程序。

3. 先决条件

要完成本指南,您需要:

  • JAVA_HOME正确配置的 JDK 11+

  • ​Apache Maven 3.8.1+ 或 Gradle 4+

  • IDE,例如 IntelliJ IDEA、VSCode 或 Eclipse

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:30Tuesday 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()方法使输出保持简短,因此更容易阅读 OptaPlannerDEBUGTRACE日志,如下所示。

5.2. 教室

类表示教授课程的Room位置,例如,Room ARoom 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 gradeChemistry by M.Curie for 10th grade。如果同一位老师每周向同一学生组多次教授同一科目,则有多个Lesson实例只能通过 区分id。例如,9 年级每周有六节数学课。

在求解过程中,OptaPlanner 会更改课程的timeslotroom字段Lesson,将每节课分配到一个时间段和一个房间。因为 OptaPlanner 更改了这些字段,Lesson所以是一个计划实体

学校时间表班级图注解

上图中的大多数字段都包含输入数据,橙色字段除外:课程timeslotroom字段null在输入数据中未分配( ),在输出数据中未分配(未分配null)。OptaPlanner 在求解过程中更改这些字段。这些字段称为计划变量。为了让 OptaPlanner 识别它们,timeslotroom字段都需要@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包装单个数据集的所有TimeslotRoom和实例。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填写其他字段,例如teacherstudentGroup。这些字段是问题属性。

然而,这个类也是解决方案的输出:

  • 求解后lessonList每个Lesson实例都有非空timeslot和字段的字段room

  • 表示输出解决方案质量的score字段,例如,0hard/-5soft

7.1.价值范围提供者

timeslotList字段是一个值范围提供程序。它包含TimeslotOptaPlanner 可以从中选择以分配给实例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. 1.创建SolverFactory以构建Solver每个数据集。

  2. 2.加载数据集。

  3. 3.解决它Solver.solve()

  4. 4.可视化该数据集的解决方案。

通常,应用程序有一个单独的实例来为每个要解决的问题数据集SolverFactory 构建一个新实例。SolverASolverFactory是线程安全的,但 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 应用程序!

如果您遇到任何问题,请查看快速入门源代码。

用户头像

OptaPlanner

关注

找到世界最强求解器,找出人生难题最佳答案 2017.12.08 加入

一个码农

评论

发布
暂无评论
OptaPlanner快速入门-helloworld_OptaPlanner_InfoQ写作社区