OptaPlanner配置
文章目录
1. 概述
使用OptaPlanner解决规划问题包括以下步骤:
- 将规划问题建模为一个带有@PlanningSolution注解的类,例如NQueens类。
- 配置Solver,例如为任何NQueens实例配置First Fit和Tabu Search solver。
- 从数据层加载问题数据集,例如Four Queens实例。这就是规划问题。
- 使用Solver.solve(problem)解决问题,返回找到的最佳解决方案。
2. Solver配置
2.1. XML配置Solver
使用SolverFactory构建一个Solver实例。使用SolverFactory配置solver配置XML文件,该文件作为类路径资源提供(由ClassLoader.getResource()定义):
SolverFactory<NQueens> solverFactory = SolverFactory.createFromXmlResource(
"org/optaplanner/examples/nqueens/solver/nqueensSolverConfig.xml");
Solver<NQueens> solver = solverFactory.buildSolver();
在典型项目中(遵循Maven目录结构),solverConfig XML文件位于$PROJECT_DIR/src/main/resources/org/optaplanner/examples/nqueens/solver/nqueensSolverConfig.xml。另外,也可以使用SolverFactory.createFromXmlFile()从文件创建SolverFactory。然而,基于可移植性的原因,推荐使用类路径资源。
Solver和SolverFactory都具有一个泛型类型Solution_,它代表规划问题和解决方案的类。
一个solver配置XML文件的示例:
<?xml version="1.0" encoding="UTF-8"?>
<solver xmlns="https://www.optaplanner.org/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd">
<!-- Define the model -->
<solutionClass>org.optaplanner.examples.nqueens.domain.NQueens</solutionClass>
<entityClass>org.optaplanner.examples.nqueens.domain.Queen</entityClass>
<!-- Define the score function -->
<scoreDirectorFactory>
<constraintProviderClass>org.optaplanner.examples.nqueens.score.NQueensConstraintProvider</constraintProviderClass>
</scoreDirectorFactory>
<!-- Configure the optimization algorithms (optional) -->
<termination>
...
</termination>
<constructionHeuristic>
...
</constructionHeuristic>
<localSearch>
...
</localSearch>
</solver>
注意其中的三个部分:
- 定义模型。
- 定义评分函数。
- 可选地配置优化算法。
这些配置的各个部分在本手册中有进一步解释。
通过更改配置,OptaPlanner可以相对容易地切换优化算法。甚至有一个Benchmarker可以让您对不同的配置进行比较,并报告最适合您用例的配置。
2.2. Java API配置Solver
也可以使用SolverConfig API来配置solver配置。这在运行时动态更改一些值特别有用。例如,在构建Solver之前,根据系统属性更改运行时间:
SolverConfig solverConfig = SolverConfig.createFromXmlResource(
"org/optaplanner/examples/nqueens/solver/nqueensSolverConfig.xml");
solverConfig.withTerminationConfig(new TerminationConfig()
.withMinutesSpentLimit(userInput));
SolverFactory<NQueens> solverFactory = SolverFactory.create(solverConfig);
Solver<NQueens> solver = solverFactory.buildSolver();
solver配置XML中的每个元素都作为Config类或Config类上的属性在org.optaplanner.core.config包命名空间中可用。这些*Config类是XML格式的Java表示形式。它们构建运行时组件(org.optaplanner.core.impl包命名空间中的组件)并将它们组装成一个高效的Solver。
为了为每个用户请求动态配置SolverFactory,在初始化期间构建一个模板SolverConfig,并使用拷贝构造函数为每个用户请求进行复制:
private SolverConfig template;
public void init() {
template = SolverConfig.createFromXmlResource(
"org/optaplanner/examples/nqueens/solver/nqueensSolverConfig.xml");
template.setTerminationConfig(new TerminationConfig());
}
// Called concurrently from different threads
public void userRequest(..., long userInput) {
SolverConfig solverConfig = new SolverConfig(template); // Copy it
solverConfig.getTerminationConfig().setMinutesSpentLimit(userInput);
SolverFactory<NQueens> solverFactory = SolverFactory.create(solverConfig);
Solver<NQueens> solver = solverFactory.buildSolver();
...
}
2.3. 注解替代方式
OptaPlanner需要告诉它哪些类在您的领域模型中是规划实体,哪些属性是规划变量等。有几种方法可以提供这些信息:
-
在领域模型上添加类注解和JavaBean属性注解(推荐)。属性注解必须在getter方法上,而不是setter方法上。这样的getter方法不需要是public的。
-
在领域模型上添加类注解和字段注解。这样的字段不需要是public的。
-
不使用注解:将领域配置外部化到一个XML文件中。目前尚不支持此功能。
本手册侧重于第一种方式,但每个特性都支持这三种方式,即使没有明确提到。
2.4. 领域访问
默认情况下,OptaPlanner使用反射来访问您的领域,这种方法始终有效,但与直接访问相比较慢。或者,您可以配置OptaPlanner使用Gizmo来访问您的领域,它将生成直接访问领域字段/方法的字节码,而不使用反射。然而,它也有一些限制:
- 领域中的所有字段必须是public的。
- 规划注解只能放在public字段和public getter上。
- 类路径中必须存在io.quarkus.gizmo:gizmo。
当与Quarkus一起使用OptaPlanner时,这些限制不适用,默认的领域访问类型是Gizmo。
要在Quarkus之外使用Gizmo,请在Solver配置中设置domainAccessType:
<solver>
<domainAccessType>GIZMO</domainAccessType>
</solver>
2.5. 自定义属性配置
支持自定义属性的solver配置元素可以通过Benchmarker来调整动态值。例如,假设您的EasyScoreCalculator有繁重的计算(被缓存),您希望在一个基准测试中增加缓存大小:
<scoreDirectorFactory>
<easyScoreCalculatorClass>...MyEasyScoreCalculator</easyScoreCalculatorClass>
<easyScoreCalculatorCustomProperties>
<property name="myCacheSize" value="1000"/><!-- Override value -->
</easyScoreCalculatorCustomProperties>
</scoreDirectorFactory>
为每个自定义属性添加一个公共setter,在构建Solver时调用该setter。
public class MyEasyScoreCalculator extends EasyScoreCalculator<MySolution, SimpleScore> {
private int myCacheSize = 500; // Default value
@SuppressWarnings("unused")
public void setMyCacheSize(int myCacheSize) {
this.myCacheSize = myCacheSize;
}
...
}
大多数值类型都受支持(包括boolean、int、double、BigDecimal、String和枚举)。
3. 模型化规划问题
3.1. 这个类是问题事实还是规划实体?
观察您的规划问题数据集。您将在其中识别出领域类,每个类可以归类为以下之一:
一个无关的类:没有被任何评分约束使用。从规划的角度来看,这些数据是过时的。
一个问题事实类:被评分约束使用,但在规划过程中不会发生变化(只要问题保持不变)。例如:床位、房间、班次、员工、主题、时间段等。问题事实类的所有属性都是问题属性。
一个规划实体类:被评分约束使用,并且在规划过程中会发生变化。例如:床位指定、班次分配、考试等。在规划过程中发生变化的属性是规划变量,其他属性是问题属性。
问问自己:哪个类在规划过程中发生了变化?哪个类有我想让Solver为我改变的变量?这个类就是一个规划实体。大多数用例只有一个规划实体类。大多数用例也只有一个规划实体类的规划变量。
在实时规划中,尽管问题本身会改变,问题事实在规划过程中实际上并不会发生变化,而是在规划之间发生变化(因为Solver会暂停应用问题事实的变化)。
要创建一个良好的领域模型,请阅读领域建模指南。
在OptaPlanner中,所有的问题事实和规划实体都是普通的JavaBean(POJO)。可以从数据库、XML文件、数据仓库、REST服务、noSQL云等加载它们(参见集成):这并不重要。
3.2. 问题事实
问题事实是任何具有getter方法且在规划过程中不会发生变化的JavaBean(POJO)。例如,在N皇后问题中,列和行就是问题事实:
public class Column {
private int index;
// ... getters
}
public class Row {
private int index;
// ... getters
}
问题事实当然可以引用其他问题事实:
public class Course {
private String code;
private Teacher teacher; // 其他问题事实
private int lectureSize;
private int minWorkingDaySize;
private List<Curriculum> curriculumList; // 其他问题事实
private int studentSize;
// ... getters
}
问题事实类不需要任何OptaPlanner特定的代码。例如,您可以重用您的领域类,这些类可能具有JPA注解。
一般来说,设计良好的领域类会导致更简单、更高效的评分约束。因此,当处理混乱(去规范化)的遗留系统时,有时将混乱的领域模型转换为OptaPlanner特定模型可能是值得的。例如:如果您的领域模型对同一位教师在两个不同部门上课有两个Teacher实例,那么编写一个约束该教师空闲时间的正确评分约束在原始模型上会比在调整后的模型上更困难。
或者,有时您还可以引入一个缓存的问题事实,仅为了丰富规划过程中的领域模型。
3.3. 规划实体
3.3.1. 规划实体注解
规划实体是在求解过程中发生变化的JavaBean(POJO),例如一个皇后在不同的行之间移动。一个规划问题有多个规划实体,例如对于单个N皇后问题,每个皇后都是一个规划实体。但通常只有一个规划实体类,例如皇后类。
规划实体类需要用@PlanningEntity注解进行注解。
每个规划实体类都有一个或多个规划变量(可以是真实的变量或影子变量)。它还应该具有一个或多个定义属性。例如,在N皇后问题中,皇后由其列定义,并且具有规划变量行。这意味着皇后的列在求解过程中不会改变,而行会改变。
@PlanningEntity
public class Queen {
private Column column;
// Planning variables: changes during planning, between score calculations.
private Row row;
// ... getters and setters
}
规划实体类可以有多个规划变量。例如,Lecture由其Course和课程中的索引定义(因为一个课程有多个讲座)。每个Lecture需要安排到Period和Room中,所以它有两个规划变量(period和room)。例如:数学课程每周有八节课,其中第一节课在星期一早上08:00在212教室。
@PlanningEntity
public class Lecture {
private Course course;
private int lectureIndexInCourse;
// Planning variables: changes during planning, between score calculations.
private Period period;
private Room room;
// ...
}
Solver配置需要声明每个规划实体类:
<solver xmlns="https://www.optaplanner.org/xsd/solver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd">
...
<entityClass>org.optaplanner.examples.nqueens.domain.Queen</entityClass>
...
</solver>
一些用例可能有多个规划实体类。例如:将货物和火车路由到铁路网络弧中,其中每个货物可以在其行程中使用多个火车,每个火车可以在每个弧中运载多个货物。拥有多个规划实体类直接增加了您的用例的实现复杂性。
不要创建不必要的规划实体类。这会导致Move实现困难和评分计算较慢。
例如,不要创建一个规划实体类来保存教师的总空闲时间,这需要在Lecture规划实体发生变化时保持更新。相反,在评分约束中计算空闲时间(或作为影子变量)并将每个教师的结果放入逻辑插入的分数对象中。
如果还需要考虑历史数据,则创建一个问题事实来保存规划窗口之前(但不包括)的历史分配总数(以便在规划实体发生变化时不改变),并让评分约束考虑到它。
规划实体的hashCode()实现必须保持不变。因此,entity hashCode()不能依赖于任何规划变量。使用自动生成的hashCode()作为实体的数据结构(如Java记录或Kotlin数据类)时要特别注意。
3.3.2. 规划实体难度
某些优化算法在了解哪些规划实体更难规划时效率更高。例如:在装箱问题中,更大的物品更难安排;在课程调度中,学生人数更多的讲座更难安排;在N皇后问题中,中间的皇后更难放置在棋盘上。
不要尝试使用规划实体难度来实现业务约束。它不会影响评分函数:如果我们有无限的求解时间,返回的解决方案将是相同的。
要获得在调度中较早安排某些实体的时间表,请添加一个评分约束,以更改评分函数,使其更喜欢这样的解决方案。只有在可以使求解器更高效的情况下才考虑添加规划实体难度。
为了允许启发式算法利用领域特定信息,请在@PlanningEntity注解中设置difficultyComparatorClass:
@PlanningEntity(difficultyComparatorClass = CloudProcessDifficultyComparator.class)
public class CloudProcess {
// ...
}
public class CloudProcessDifficultyComparator implements Comparator<CloudProcess> {
public int compare(CloudProcess a, CloudProcess b) {
return new CompareToBuilder()
.append(a.getRequiredMultiplicand(), b.getRequiredMultiplicand())
.append(a.getId(), b.getId())
.toComparison();
}
}
或者,您还可以在@PlanningEntity注解中设置difficultyWeightFactoryClass,这样您也可以从解决方案中访问其他问题事实:
@PlanningEntity(difficultyWeightFactoryClass = QueenDifficultyWeightFactory.class