A Guide to OptaPlanner – OptaPlanner指南

最后修改: 2018年 9月 13日

中文/混合/英文(键盘快捷键:t)

1. Introduction to OptaPlanner

1.OptaPlanner简介

In this tutorial, we look at a Java constraint satisfaction solver called OptaPlanner.

在本教程中,我们看看一个名为OptaPlanner的Java约束条件满足求解器。

OptaPlanner solves planning problems using a suite of algorithms with minimal setup.

OptaPlanner使用一套算法解决规划问题,只需最少的设置。

Although an understanding of the algorithms may provide helpful detail, with the framework performing the hard work for us.

尽管对算法的理解可能会提供有用的细节,但由于框架为我们进行了艰苦的工作。

2. Maven Dependency

2.Maven的依赖性

First, we’ll add a Maven dependency for OptaPlanner:

首先,我们要为OptaPlanner添加一个Maven依赖项。

<dependency>
    <groupId>org.optaplanner</groupId>
    <artifactId>optaplanner-core</artifactId>
    <version>8.24.0.Final</version>
</dependency>

We locate the most recent version of OptaPlanner from Maven Central repository.

我们从Maven Central资源库中找到OptaPlanner的最新版本。

3. Problem/Solution Class

3.问题/解决方案类

To solve a problem we certainly need a specific one as an example.

为了解决一个问题,我们当然需要一个具体的问题作为例子。

Lecture timetabling is a suitable example due to the difficulty in balancing resources such as rooms, time and teachers.

由于难以平衡教室、时间和教师等资源,讲座时间安排是一个合适的例子。

3.1. CourseSchedule

3.1.课程表

CourseSchedule contains a combination of our problem variables and planning entities consequently it is the solution class. As a result, we use multiple annotations to configure it.

CourseSchedule包含了我们的问题变量和规划实体的组合,因此它是一个解决方案类。因此,我们使用多个注解来配置它。

Let’s take a closer look at each separately:

让我们分别仔细研究一下。

@PlanningSolution
public class CourseSchedule {

    @ValueRangeProvider(id = "availableRooms")
    @ProblemFactCollectionProperty
    private List<Integer> roomList;
    @ValueRangeProvider(id = "availablePeriods")
    @ProblemFactCollectionProperty
    private List<Integer> periodList;
    @ProblemFactCollectionProperty
    private List<Lecture> lectureList;
    @PlanningScore
    private HardSoftScore score;

The PlanningSolution annotation tells OptaPlanner that this class contains the data to encompass a solution.

PlanningSolution注解告诉OptaPlanner,这个类包含了包含一个解决方案的数据。

OptaPlanner expects these minimum components: the planning entity, problem facts, and a score.

OptaPlanner期待这些最小的组件:规划实体、问题事实和分数。

3.2. Lecture

3.2 阅读

Lecture, a POJO, looks like:

Lecture,一个POJO,看起来像。

@PlanningEntity
public class Lecture {

    @PlaningId
    private Long id;
    public Integer roomNumber;
    public Integer period;
    public String teacher;

    @PlanningVariable(
      valueRangeProviderRefs = {"availablePeriods"})
    public Integer getPeriod() {
        return period;
    }

    @PlanningVariable(
      valueRangeProviderRefs = {"availableRooms"})
    public Integer getRoomNumber() {
        return roomNumber;
    }
}

We use Lecture class as the planning entity, so we add another annotation on the getter in CourseSchedule:

我们使用 Lecture类作为计划实体,所以我们在CourseSchedule的getter上添加另一个注解。

@PlanningEntityCollectionProperty
public List<Lecture> getLectureList() {
    return lectureList;
}

Our planning entity contains the constraints that are being set.

我们的规划实体包含正在设定的约束。

The PlanningVariable annotation and the valueRangeProviderRef annotations link the constraints to the problem facts.

PlanningVariable注解和valueRangeProviderRef注解将约束条件与问题事实联系起来。

These constraint values will be scored later across all planning entities.

这些约束值以后将在所有规划实体中进行评分。

3.3. Problem Facts

3.3 问题事实

The roomNumber and period variables act as constraints similarly to each other.

roomNumberperiod变量作为约束条件的作用类似于对方。

OptaPlanner scores the solutions as a result of logic using these variables. We add annotations to both getter methods:

OptaPlanner对解决方案的评分是使用这些变量的逻辑结果。我们为这两个getter方法添加注解。

@ValueRangeProvider(id = "availableRooms")
@ProblemFactCollectionProperty
public List<Integer> getRoomList() {
    return roomList;
}

@ValueRangeProvider(id = "availablePeriods")
@ProblemFactCollectionProperty
public List<Integer> getPeriodList() {
    return periodList;
}

These lists are all possible values used in the Lecture fields.

这些列表是Lecture字段中使用的所有可能的值。

OptaPlanner populates them in all solutions across the search space.

OptaPlanner将它们填充到整个搜索空间的所有解决方案中。

Finally, it then sets a score to each of the solutions, so we need a field to store the score:

最后,它再给每个解决方案设置一个分数,所以我们需要一个字段来存储这个分数。

@PlanningScore
public HardSoftScore getScore() {
    return score;
}

Without a score, OptaPlanner cannot find the optimal solution hence the stressed importance earlier.

如果没有分数,OptaPlanner就无法找到最佳解决方案,因此前面强调了重要性。

4. Scoring

4.得分

In contrast to what we have looked at so far, the scoring class requires more custom code.

与我们到目前为止所看的不同,评分类需要更多的自定义代码。

This is because the score calculator is specific to the problem and the domain model.

这是因为分数计算器是针对问题和领域模型的。

4.1. Custom Java

4.1.定制的Java

We use a simple score calculation to solve this problem (although it may not seem like it):

我们用一个简单的分数计算来解决这个问题(虽然看起来不是这样)。

public class ScoreCalculator 
  implements EasyScoreCalculator<CourseSchedule, HardSoftScore> {

    @Override
    public HardSoftScore calculateScore(CourseSchedule courseSchedule) {
        int hardScore = 0;
        int softScore = 0;

        Set<String> occupiedRooms = new HashSet<>();
        for(Lecture lecture : courseSchedule.getLectureList()) {
            String roomInUse = lecture.getPeriod()
              .toString() + ":" + lecture.getRoomNumber().toString();
            if(occupiedRooms.contains(roomInUse)){
                hardScore += -1;
            } else {
                occupiedRooms.add(roomInUse);
            }
        }

        return HardSoftScore.Of(hardScore, softScore);
    }
}

If we take a closer look at the above code, the important parts become more clear. We calculate a score in the loop because the List<Lecture> contains specific non-unique combinations of rooms and periods.

如果我们仔细看一下上面的代码,重要的部分会变得更加清晰。我们在循环中计算分数,因为List<Lecture> 包含了房间和时段的特定非唯一组合。

The HashSet is used to save a unique key (string) so that we can penalize duplicate lectures in the same room and period.

HashSet 用于保存一个唯一的键(字符串),这样我们就可以惩罚同一房间和时期的重复讲座。

As a result, we receive unique sets of rooms and periods.

因此,我们收到了独特的成套房间和时期。

5. Testing

5.测试

We configured our solution, solver and problem classes. Let’s test it!

我们配置了我们的解决方案、解算器和问题类。让我们来测试一下!

5.1. Setting up Our Test

5.1.设置我们的测试

First, we do some setup:

首先,我们做一些设置。

SolverFactory<CourseSchedule> solverFactory = SolverFactory.create(new SolverConfig() 
                                                      .withSolutionClass(CourseSchedule.class)
                                                      .withEntityClasses(Lecture.class)
                                                      .withEasyScoreCalculatorClass(ScoreCalculator.class)
                                                      .withTerminationSpentLimit(Duration.ofSeconds(1))); 
solver = solverFactory.buildSolver();
unsolvedCourseSchedule = new CourseSchedule();

Second, we populate data into the planning entity collection and problem fact List objects.

其次,我们将数据填充到规划实体集合和问题事实List对象中。

5.2. Test Execution and Verification

5.2.测试执行和验证

Finally, we test it by calling solve.

最后,我们通过调用solve来测试它。

CourseSchedule solvedCourseSchedule = solver.solve(unsolvedCourseSchedule);

assertNotNull(solvedCourseSchedule.getScore());
assertEquals(-4, solvedCourseSchedule.getScore().getHardScore());

We check that the solvedCourseSchedule has a score which tells us that we have the “optimal” solution.

我们检查solvedCourseSchedule是否有一个分数,告诉我们有 “最佳 “解决方案。

For a bonus, we create a print method that will display our optimized solution:

作为奖励,我们创建了一个打印方法,将显示我们的优化解决方案。

public void printCourseSchedule() {
    lectureList.stream()
      .map(c -> "Lecture in Room "
        + c.getRoomNumber().toString() 
        + " during Period " + c.getPeriod().toString())
      .forEach(k -> logger.info(k));
}

This method displays:

这种方法显示。

Lecture in Room 1 during Period 1
Lecture in Room 2 during Period 1
Lecture in Room 1 during Period 2
Lecture in Room 2 during Period 2
Lecture in Room 1 during Period 3
Lecture in Room 2 during Period 3
Lecture in Room 1 during Period 1
Lecture in Room 1 during Period 1
Lecture in Room 1 during Period 1
Lecture in Room 1 during Period 1

Notice how the last three entries are repeating. This happens because there is no optimal solution to our problem. We chose three periods, two classrooms and ten lectures.

注意最后三个条目是如何重复的。发生这种情况是因为我们的问题没有最佳解决方案。我们选择了三个时段,两个教室和十个讲座。

There are only six possible lectures due to these fixed resources. At the very least this answer shows the user that there are not enough rooms or periods to contain all the lectures.

由于这些固定资源,只有六个可能的讲座。这个答案至少告诉用户,没有足够的房间或时段来容纳所有的讲座。

6. Extra Features

6.额外功能

Our example for OptaPlanner we created was a simple one, however, the framework has added features for more diverse use cases. We may want to implement or alter our algorithm for optimization and then specify the framework to use it.

我们创建的OptaPlanner的例子是一个简单的例子,然而,该框架为更多不同的用例增加了功能。我们可能想实现或改变我们的优化算法,然后指定框架来使用它。

Due to recent improvements in Java’s multi-threading capabilities, OptaPlanner also gives developers the ability to use multiple implementations of multi-threading such as fork and join, incremental solving and multitenancy.

由于最近对Java的多线程能力进行了改进,OptaPlanner还为开发者提供了使用多线程的多种实现方式的能力,如分叉和连接、增量解决和多租户。

Refer to the documentation for more information.

请参考文献以了解更多信息。

7. Conclusion

7.结语

The OptaPlanner framework provides developers with a powerful tool to solve constraint satisfaction problems such as scheduling and resource allocation.

OptaPlanner框架为开发者提供了一个强大的工具来解决约束条件的满足问题,如调度和资源分配。

OptaPlanner offers minimal JVM resource usage as well as integrating with Jakarta EE. The author continues to support the framework, and Red Hat has added it as part of its Business Rules Management Suite.

OptaPlanner提供了最小的JVM资源使用量,以及与Jakarta EE的整合。作者继续支持该框架,而红帽公司已将其作为其业务规则管理套件的一部分。

As always the code can be found over on Github.

像往常一样,代码可以在Github上找到over