使用红帽在 Red Hat Process Automation Manager 中构建 OptaPlanner 的解决者

Red Hat Process Automation Manager 7.12

摘要

本文档论述了如何使用 Red Hat Process Automation Manager 中构建 OptaPlanner 的 OptaPlanner 来获取规划问题的最佳解决方案。

前言

作为业务决策和流程的开发人员,您可以使用红帽构建的 OptaPlanner 来开发用于规划问题的最佳解决方案。OptaPlanner 是 Red Hat Process Automation Manager 的内置组件。您可以在 Red Hat Process Automation Manager 中使用 solvers 作为服务的一部分,以优化具有特定限制的有限资源。

使开源包含更多

红帽致力于替换我们的代码、文档和 Web 属性中存在问题的语言。我们从这四个术语开始:master、slave、黑名单和白名单。由于此项工作十分艰巨,这些更改将在即将推出的几个发行版本中逐步实施。详情请查看 CTO Chris Wright 信息

部分 I. 将 OptaPlanner 项目的红帽构建升级到 OptaPlanner 8

如果您有使用 OptaPlanner 7 或更早的 pubic API 创建的 OptaPlanner 项目,并且您希望将项目代码升级到 OptaPlanner 8,请查看本指南中的信息。本指南还包括对位于公开 API 外的实施类的更改。

OptaPlanner 公共 API 是 OptaPlanner 源代码的子集,可让您通过 Java 代码与 OptaPlanner 交互。因此,您可以在同一主发行版本中升级到更高的 OptaPlanner 版本,OptaPlanner 遵循 语义版本。这意味着,您可以在不破坏使用 OptaPlanner 公共 API 的代码的情况下,从 OptaPlanner 7.44 升级到 OptaPlanner 7.48。OptaPlanner 公共 API 类在主 OptaPlanner 版本的版本中兼容。但是,当红帽发布新的主版本时,会破坏的更改有时会引入公共 API。

OptaPlanner 8 是一个新的主版本,对公共 API 的一些更改与早期版本的 OptaPlanner 不兼容。OptaPlanner 8 将在接下来的数年内是 8.x 系列的基础。对于此项目的长期优势,对与此版本所需的较早版本不兼容的公共 API 的更改。

表 1. Red Hat Process Automation Manager 和 Red Hat build of OptaPlanner 版本

Process Automation ManagerOptaPlanner

7.7

7.33

7.8

7.39

7.9

7.44

7.10

7.48

7.11

8.5

每个升级备注都有一个标签,指示您的代码会受到这一更改的影响。下表描述了每个标签:

表 2. 升级影响标签

标签影响

可能会影响您的代码。

除非您对代码进行了定制,否则不太可能影响您的代码,特别是在您遵循这些示例时。

与早期版本的 OptaPlanner 不兼容的任何更改都会使用 Public API 标签标注。

第 1 章 与 OptaPlanner 7.x 或更早版本不兼容的更改

本节中列出的更改与 OptaPlanner 7.x 或更早版本的 OptaPlanner 7.x 不兼容。

需要 Java 11 或更高版本

公共 API

如果您使用 JRE 或 JDK 8,请升级到 JDK 11 或更高版本。

  • 在 Linux 上,从 Linux 软件存储库获取 OpenJDK。在 Fedora 和 Red Hat Enterprise Linux 中输入以下命令:

    sudo dnf install java-11-openjdk-devel
  • 在 Windows 和 macOS 上,从 AdoptOpenJDK 网站下载 OpenJDK。

SolverFactoryPlannerBenchmarkFactory 不再支持 KIE 容器

公共 API

因为 OptaPlanner 现在与 Kogito 保持一致,所以 KIE 容器概念不再适用。因此,SolverFactory 不再允许您从 KIE 容器创建 Solver 实例。这也适用于 PlannerBenchmarkFactory 和基准。

已删除 OSGi 元数据

公共 API

由于 OSGi 的有限使用情况及其维护负担,OptaPlanner 8.x 系列中的 OptaPlanner JAR 文件不再将 OSGi 元数据包含在 META-INF/MANIFEST.MF 文件中。

禁止使用 Java 序列化

,公共 API

在 OptaPlanner 8 中,大多数使用 Serializable 标记接口已从公共 API 中删除。考虑使用 JSON 或另一个格式进行序列化。

SolverFactory.getScoreDirectorFactory () 替换为 ScoreManager

公共 API

在 OptaPlanner 版本 7 中,需要使用 ScoreDirectorFactory 来解释分数。在 OptaPlanner 版本 8 中,新功能被添加到 ScoreManager 中,因此不再有理由创建 ScoreDirector 的新实例。

OptaPlanner 7 中的 *.java 文件示例:

ScoreDirectorFactory<CloudBalance> scoreDirectorFactory = solverFactory.getScoreDirectorFactory();
try (ScoreDirector<CloudBalance> scoreDirector = scoreDirectorFactory.buildScoreDirector()) {
    scoreDirector.setWorkingSolution(solution);
    Score score = scoreDirector.calculateScore();
}

OptaPlanner 8 中的 *.java 文件示例:

ScoreManager<CloudBalance> scoreManager = ScoreManager.create(solverFactory);
Score score = scoreManager.updateScore(solution);

允许您检索 ScoreDirectorScoreDirectorFactory 实例的方法已从公共 API 中删除,而无需替换。ScoreDirector 接口的更小版本被提升到公共 API,以将 ProblemFactChange 接口提升到公共 API。

SolverFactory:getSolverConfig () 删除

,公共 API

SolverFactory.getSolverConfig () 方法已弃用,并替换为 SolverFactory.create (SolverConfig) 方法。SolverConfig 实例现在在 SolverFactory 实例实例化前实例化,这会更自然。之前的顺序已被删除。

OptaPlanner 7 中的 *.java 文件示例:

SolverFactory<MySolution> solverFactory = SolverFactory.createFromXmlResource(".../mySolverConfig.xml");
SolverConfig solverConfig = solverFactory.getSolverConfig();
...
Solver<MySolution> solver = solverFactory.buildSolver();

OptaPlanner 8 中的 *.java 文件示例:

SolverConfig solverConfig = SolverConfig.createFromXmlResource(".../mySolverConfig.xml");
...
SolverFactory<MySolution> solverFactory = SolverFactory.create(solverConfig);
Solver<MySolution> solver = solverFactory.buildSolver();

如果您还传递 ClassLoader,请将其传递给 SolverConfig.createFromXmlResource ()SolverFactory.create ()

SolverConfig:buildSolver () 删除

,公共 API

SolverConfig.buildSolver () 方法是一个不属于公共 API 中的内部方法。使用 SolverFactory.buildSolver () 方法替代。

OptaPlanner 7 中的 *.java 文件示例:

SolverConfig solverConfig = SolverConfig.createFromXmlResource(".../mySolverConfig.xml");
...
Solver<MySolution> solver = solverConfig.buildSolver();

OptaPlanner 8 中的 *.java 文件示例:

SolverConfig solverConfig = SolverConfig.createFromXmlResource(".../mySolverConfig.xml");
...
SolverFactory<MySolution> solverFactory = SolverFactory.create(solverConfig);
Solver<MySolution> solver = solverFactory.buildSolver();

PlannerBenchmarkConfig:buildPlannerBenchmark () 已删除

,公共 API

PlannerBenchmarkConfig.buildPlannerBenchmark () 方法是一个不属于公共 API 中的方法。使用 PlannerBenchmarkFactory.buildPlannerBenchmark () 方法替代。

OptaPlanner 7 中的 *.java 文件示例:

PlannerBenchmarkConfig benchmarkConfig = PlannerBenchmarkConfig.createFromXmlResource(
        ".../cloudBalancingBenchmarkConfig.xml");
...
PlannerBenchmark benchmark = benchmarkFactory.buildPlannerBenchmark();

OptaPlanner 8 中的 *.java 文件示例:

PlannerBenchmarkConfig benchmarkConfig = PlannerBenchmarkConfig.createFromXmlResource(
        ".../cloudBalancingBenchmarkConfig.xml");
...
PlannerBenchmarkFactory benchmarkFactory = PlannerBenchmarkFactory.create(benchmarkConfig);
PlannerBenchmark benchmark = benchmarkFactory.buildPlannerBenchmark();

SolverFactory:cloneSolverFactory () removed

,公共 API

SolverFactory.cloneSolverFactory () 方法已弃用,并替换为新的 SolverConfig (SolverConfig) 复制构造器,并且 SolverFactory.cloneSolverFactory () 方法已被删除。

OptaPlanner 7 中的 *.java 文件示例:

private SolverFactory<MySolution> base;

public void userRequest(..., long userInput) {
    SolverFactory<MySolution> solverFactory = base.cloneSolverFactory();
    solverFactory.getSolverConfig()
            .getTerminationConfig()
            .setMinutesSpentLimit(userInput);
    Solver<MySolution> solver = solverFactory.buildSolver();
    ...
}

OptaPlanner 8 中的 *.java 文件示例:

private SolverConfig base;

public void userRequest(..., long userInput) {
    SolverConfig solverConfig = new SolverConfig(base); // Copy it
    solverConfig.getTerminationConfig()
            .setMinutesSpentLimit(userInput);
    SolverFactory<MySolution> solverFactory = SolverFactory.create(solverConfig);
    Solver<MySolution> solver = solverFactory.buildSolver();
    ...
}

SolverFactory:createEmpty () 已删除

,公共 API

SolverFactory.createEmpty () 方法已弃用,并 替换为新的 SolverConfig () 方法。SolverFactory.createEmpty () 方法已被删除。

OptaPlanner 7 中的 *.java 文件示例:

SolverFactory<MySolution> solverFactory = SolverFactory.createEmpty();
SolverConfig solverConfig = solverFactory.getSolverConfig()
...
Solver<MySolution> solver = solverFactory.buildSolver();

OptaPlanner 8 中的 *.java 文件示例:

SolverConfig solverConfig = new SolverConfig();
...
SolverFactory<MySolution> solverFactory = SolverFactory.create(solverConfig);
Solver<MySolution> solver = solverFactory.buildSolver();

XML <solver/> root 元素现在属于 http://www.optaplanner.org/xsd/solver 命名空间

公共 API

OptaPlanner 现在为 solver 配置提供了一个 XML 模式定义。虽然 OptaPlanner 保留与现有 XML 配置的早期版本的兼容性,强烈建议迁移到 XSD,因为 OptaPlanner 可能以后只支持有效的配置 XML。

OptaPlanner 7 中的 *SolverConfig.xml 文件中的示例:

<?xml version="1.0" encoding="UTF-8"?>
<solver>
  ...
</solver>

OptaPlanner 8 中的 *SolverConfig.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">
  ...
</solver>

使用 XSD 可能需要对配置的一些 XML 元素重新排序。在 IDE 中使用代码完成来迁移到有效的 XML。

move selector 配置中的 property subPillarEnabled 已被删除

,公共 API

PillarSwapMoveSelector 和 PillarChangeMoveSelector 和 PillarChangeMoveSelector 中的 subPillarEnabled 属性已弃用,并替换为一个新的属性 subPillarTypesubPillarEnabled 属性已被删除。

OptaPlanner 7 中的 *SolverConfig.xml*BenchmarkConfig.xml 文件中的示例:

      <pillar...MoveSelector>
        ...
        <pillarSelector>
          <subPillarEnabled>false</subPillarEnabled>
          ...
        </pillarSelector>
        ...
      </pillar...MoveSelector>

OptaPlanner 8 中的 *SolverConfig.xml*BenchmarkConfig.xml 文件中的示例:

      <pillar...MoveSelector>
        <subPillarType>NONE</subPillarType>
        <pillarSelector>
          ...
        </pillarSelector>
        ...
      </pillar...MoveSelector>

solver : getScoreDirectorFactory () removed

公共 API

getScoreDirectorFactory () 方法已弃用,现在已从 Solver 和 Solver Factory 类中删除。

您不再需要仅创建 Solver 实例来计算或解释 UI 中的分数。改为使用 ScoreManager API。

OptaPlanner 7 中的 *.java 文件示例:

SolverFactory<VehicleRoutingSolution> solverFactory = SolverFactory.createFromXmlResource(...);
Solver<VehicleRoutingSolution> solver = solverFactory.buildSolver();
uiScoreDirectorFactory = solver.getScoreDirectorFactory();
...

OptaPlanner 8 中的 *.java 文件示例:

SolverFactory<VehicleRoutingSolution> solverFactory = SolverFactory.createFromXmlResource(...);
ScoreManager<VehicleRoutingSolution> scoreManager = ScoreManager.create(solverFactory);
...

不应再使用 ScoreDirectorFactory,因为它一直位于公共 API 之外,其所有功能都在公共 API 的各个部分中公开。

solver.explainBestScore () 已被删除

公共 API

Solver 接口上的 explainBestScore () 方法已在 7.x 中弃用,现已被删除。您可以通过新的 ScoreManager API 获取相同的信息。

红帽建议您不要以任何方式解析此方法调用的结果。

OptaPlanner 7 中的 *.java 文件示例:

solver = ...;
scoreExplanation = solver.explainBestScore();

OptaPlanner 8 中的 *.java 文件示例:

MySolution solution = ...;
ScoreManager<MySolution> scoreManager = ...;
scoreExplanation = scoreManager.explainScore(solution);

Solver 接口方法 getBestSolution (), getBestScore (), 和 getTimeMillisSpent () 已被删除

,公共 API

Solver 接口中的多个方法在 7.x 中已弃用,并已被删除。您可以通过 Solver.add EventListener (…​) 注册 EventListener 来获取相同的信息。

OptaPlanner 7 中的 *.java 文件示例:

solver = ...;
solution = solver.getBestSolution();
score = solver.getBestScore();
timeMillisSpent = solver.getTimeMillisSpent();

OptaPlanner 8 中的 *.java 文件示例:

solver = ...;
solver.addEventListener(event -> {
    solution = event.getNewBestSolution();
    score = event.getNewBestScore();
    timeMillisSpent = event.getTimeMillisSpent();
});

注解扫描已被删除

公共 API

solver 配置中的 & lt;scanAnnotatedClasses /> 指令在 7.x 中弃用,现已被删除。

OptaPlanner 7 中的 *.xml 文件示例:

<solver>
    ...
    <scanAnnotatedClasses/>
    ...
</solver>

OptaPlanner 8 中的 *.xml 文件示例:

<solver>
    ...
    <solutionClass>...</solutionClass>
    <entityClass>...</entityClass>
    ...
</solver>

@PlanningFactProperty@PlanningFactCollectionProperty的新软件包

公共 API

@PlanningFactProperty@PlanningFactCollectionProperty 注释现在与其他类似注释(如 @PlanningSolution )共享相同的软件包。旧注解在 7.x 中弃用并被删除。

OptaPlanner 7 中的 *.java 文件示例:

import org.optaplanner.core.api.domain.solution.drools.ProblemFactCollectionProperty;
import org.optaplanner.core.api.domain.solution.drools.ProblemFactProperty;

OptaPlanner 8 中的 *.java 文件示例:

import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
import org.optaplanner.core.api.domain.solution.ProblemFactProperty;

filterClassList 替换为单个过滤器类

,公共 API

EntitySelectorValueSelectorMoveSelector 的配置现在在配置 API 和 solver 配置 XML 中都有一个过滤器类。

在实践中,您不需要多个选择过滤器类,您可以将它们替换为实施所有它们逻辑的单一选择过滤器类。现在,传递一个选择类需要较少的样板代码。

OptaPlanner 7 中的 *.java 文件示例:

ValueSelectorConfig valueSelectorConfig = new ValueSelectorConfig();
valueSelectorConfig.setFilterClassList(Collections.singletonList(MySelectionFilterClass.class));

OptaPlanner 8 中的 *.java 文件示例:

ValueSelectorConfig valueSelectorConfig = new ValueSelectorConfig();
valueSelectorConfig.setFilterClass(MySelectionFilterClass.class);

使用单个选择过滤器类替换多个选择过滤器类

OptaPlanner 7 中的 *.xml 文件示例:

<swapMoveSelector>
  <entitySelector>
    <filterClass>com.example.FilterA</filterClass>
    <filterClass>com.example.FilterB</filterClass>
  </entitySelector>
</swapMoveSelector>

OptaPlanner 7 中的 *.java 文件示例:

package com.example;
...
public class FilterA implements SelectionFilter<MySolution, MyPlanningEntity> {

    @Override
    public boolean accept(ScoreDirector<MySolution> scoreDirector, MyPlanningEntity selection) {
        return selection.getValue() < 500;
    }
}
package com.example;
...
public class FilterB implements SelectionFilter<MySolution, MyPlanningEntity> {

    @Override
    public boolean accept(ScoreDirector<MySolution> scoreDirector, MyPlanningEntity selection) {
        return selection.getOrder() == Order.ASC;
    }
}

OptaPlanner 8 中的 *.xml 文件示例:

<swapMoveSelector>
  <entitySelector>
    <filterClass>com.example.SingleEntityFilter</filterClass>
  </entitySelector>
</swapMoveSelector>

OptaPlanner 8 中的 *.java 文件示例:

package com.example;
...
public class SingleEntityFilter implements SelectionFilter<MySolution, MyPlanningEntity> {

    @Override
    public boolean accept(ScoreDirector<MySolution> scoreDirector, MyPlanningEntity selection) {
        return selection.getValue() < 500 && selection.getOrder() == Order.ASC;
    }
}

AcceptorConfig 被重命名为 LocalSearchAcceptorConfig

这只会影响配置 API。解决器配置 XML 文件保持不变。

实现与其他特定于本地搜索的配置类的命名一致性。

OptaPlanner 7 中的 *.java 文件示例:

LocalSearchPhaseConfig localSearchPhaseConfig = new LocalSearchPhaseConfig()
        .withAcceptorConfig(new AcceptorConfig().withEntityTabuSize(5));

OptaPlanner 8 中的 *.java 文件示例:

LocalSearchPhaseConfig localSearchPhaseConfig = new LocalSearchPhaseConfig()
        .withAcceptorConfig(new LocalSearchAcceptorConfig().withEntityTabuSize(5));

自定义属性 XML 配置格式更改

,公共 API

此问题仅影响解析器配置 XML,特别是 < scoreDirectorFactory/> , < moveIteratorFactory/> , &lt; moveListFactory/> , & lt ;partitionedSearch/> 和 & lt;customPhase/>

这一更改是为了在构建时强制实施配置 XML 的结构。

OptaPlanner 7 中的 *.xml 文件示例:

<partitionedSearch>
  <solutionPartitionerClass>com.example.MySolutionPartitioner</solutionPartitionerClass>
  <solutionPartitionerCustomProperties>
    <partCount>4</partCount> <!-- a custom property -->
    <minimumProcessListSize>300</minimumProcessListSize> <!-- a custom property -->
  </solutionPartitionerCustomProperties>
</partitionedSearch>

OptaPlanner 8 中的 *.xml 文件示例:

<partitionedSearch>
  <solutionPartitionerClass>com.example.MySolutionPartitioner</solutionPartitionerClass>
  <solutionPartitionerCustomProperties>
    <property name="partCount" value="4"/> <!-- a custom property -->
    <property name="minimumProcessListSize" value="300"/> <!-- a custom property -->
  </solutionPartitionerCustomProperties>
</partitionedSearch>

<variableNameInclude/& gt; 元素现在被 < variableNameIncludes/> 元素包装

,公共 API

这个版本只影响 solver 配置 XML,特别是 < swapMoveSelector/> 和 & lt ;pillarSwapMoveSelector/> 元素。

这一更改是为了在构建时强制实施配置 XML 的结构。

OptaPlanner 7 中的 *.xml 文件示例:

<swapMoveSelector>
  <variableNameInclude>variableA</variableNameInclude>
  <variableNameInclude>variableB</variableNameInclude>
</swapMoveSelector>

OptaPlanner 8 中的 *.xml 文件示例:

<swapMoveSelector>
  <variableNameIncludes>
    <variableNameInclude>variableA</variableNameInclude>
    <variableNameInclude>variableB</variableNameInclude>
  </variableNameIncludes>
</swapMoveSelector>

删除了 解决方案 接口

,公共 API

Solution 接口已弃用并删除。AbstractSolution 接口(仅被 Business Central 使用)也已被删除。

删除 Solution 接口,将 getScore () 方法标上 @PlanningScore,并将 getProblemFacts () 方法替换为每个问题事实 getter (或字段)上的 @ProblemFactCollectionProperty 注释。

OptaPlanner 7 中的 *.java 文件示例:

@PlanningSolution
public class CloudBalance implements Solution<HardSoftScore> {

    private List<CloudComputer> computerList;
    ...

    private HardSoftScore score;

    @ValueRangeProvider(id = "computerRange")
    public List<CloudComputer> getComputerList() {...}

    public HardSoftScore getScore() {...}
    public void setScore(HardSoftScore score) {...}

    public Collection<? extends Object> getProblemFacts() {
        List<Object> facts = new ArrayList<Object>();
        facts.addAll(computerList);
        ...
        return facts;
    }

}

OptaPlanner 8 中的 *.java 文件示例:

@PlanningSolution
public class CloudBalance {

    private List<CloudComputer> computerList;
    ...

    private HardSoftScore score;

    @ValueRangeProvider(id = "computerRange")
    @ProblemFactCollectionProperty
    public List<CloudComputer> getComputerList() {...}

    @PlanningScore
    public HardSoftScore getScore() {...}
    public void setScore(HardSoftScore score) {...}

}

对于没有包括在集合中的单个问题事实,请使用 @ProblemFactProperty 注释,如下例所示,同时存在字段注释:

OptaPlanner 7 中的 *.java 文件示例:

@PlanningSolution
public class CloudBalance implements Solution<HardSoftScore> {

    private CloudParametrization parametrization;
    private List<CloudBuilding> buildingList;
    @ValueRangeProvider(id = "computerRange")
    private List<CloudComputer> computerList;
    ...

    public Collection<? extends Object> getProblemFacts() {
        List<Object> facts = new ArrayList<Object>();
        facts.add(parametrization); // not a Collection
        facts.addAll(buildingList);
        facts.addAll(computerList);
        ...
        return facts;
    }

}

OptaPlanner 8 中的 *.java 文件示例:

@PlanningSolution
public class CloudBalance {

    @ProblemFactProperty
    private CloudParametrization parametrization;
    @ProblemFactCollectionProperty
    private List<CloudBuilding> buildingList;
    @ValueRangeProvider(id = "computerRange")
    @ProblemFactCollectionProperty
    private List<CloudComputer> computerList;
    ...

}

不要对具有 @PlanningEntityCollectionProperty 注释的 getter (或字段)添加 @ProblemFactCollectionProperty 注释。

BestSolutionChangedEvent:isNewBestSolutionInitialized () 已删除

,公共 API

BestSolutionChangedEvent.isNewBestSolutionInitialized () 方法已弃用,并替换为 BestSolutionChangedEvent.getNewBestSolution ().getScore ().isSolutionInitialized () 方法。BestSolutionChangedEvent.isNewBestSolutionInitialized () 方法已被删除。

OptaPlanner 7 中的 *.java 文件示例:

    public void bestSolutionChanged(BestSolutionChangedEvent<CloudBalance> event) {
        if (event.isEveryProblemFactChangeProcessed()
                && event.isNewBestSolutionInitialized()) {
            ...
        }
    }

OptaPlanner 8 中的 *.java 文件示例:

    public void bestSolutionChanged(BestSolutionChangedEvent<CloudBalance> event) {
        if (event.isEveryProblemFactChangeProcessed()
                && event.getNewBestSolution().getScore().isSolutionInitialized()) {
            ...
        }
    }

如果您检查 isFeasible (),它将检查是否初始化了解决方案。

OptaPlanner 8 中的 *.java 文件示例:

    public void bestSolutionChanged(BestSolutionChangedEvent<CloudBalance> event) {
        if (event.isEveryProblemFactChangeProcessed()
                // isFeasible() checks isSolutionInitialized() too
                && event.getNewBestSolution().getScore().isFeasible()) {
            ...
        }
    }

<valueSelector > : variableName 现在是一个属性

,公共 API

当电源调整移动选择器(如 < changeMoveSelector> )时,在带有多个规划变量的用例中,&lt ;variableName& gt; XML 元素已被一个 variableName="…​" XML 属性替代。这一更改可减少解决者配置详细程度。在整个 7.x 系列中被弃用后,旧方法现已被删除。

OptaPlanner 7 中的 *SolverConfig.xml*BenchmarkConfig.xml 文件中的示例:

  <valueSelector>
    <variableName>room</variableName>
  </valueSelector>

OptaPlanner 8 中的 *SolverConfig.xml*BenchmarkConfig.xml 文件中的示例:

  <valueSelector variableName="room"/>

分区的搜索:已删除 threadFactoryClass

,公共 API

因为 <solver& gt; 有一些时间支持 <threadFactoryClass > 元素,所以 & lt; partitionedSearch> 下的 <threadFactoryClass & gt; 元素已被删除。

OptaPlanner 7 中的 *SolverConfig.xml*BenchmarkConfig.xml 文件中的示例:

  <solver>
    ...
    <partitionedSearch>
      <threadFactoryClass>...MyAppServerThreadFactory</threadFactoryClass>
      ...
    </partitionedSearch>
  </solver>

OptaPlanner 8 中的 *SolverConfig.xml*BenchmarkConfig.xml 文件中的示例:

  <solver>
    <threadFactoryClass>...MyAppServerThreadFactory</threadFactoryClass>
    ...
    <partitionedSearch>
      ...
    </partitionedSearch>
  </solver>

SimpleDoubleScoreHardSoftDoubleScore removed

,公共 API

不建议使用基于双分的分数类型,因为它们可能导致分数损坏。它们已被删除。

OptaPlanner 7 中的 *.java 文件示例:

@PlanningSolution
public class MyPlanningSolution {

    private SimpleDoubleScore score;

    ...

}

OptaPlanner 8 中的 *.java 文件示例:

@PlanningSolution
public class MyPlanningSolution {

    private SimpleLongScore score;

    ...

}

Score.toInitializedScore() removed

,公共 API

Score.toInitializedScore () 方法已弃用,并使用 7.x 中的 Score.withInitScore (int) 方法替代,现已被删除。

OptaPlanner 7 中的 *.java 文件示例:

score = score.toInitializedScore();

OptaPlanner 8 中的 *.java 文件示例:

score = score.withInitScore(0);

已删除各种原因

,公共 API

以下 Comparator 实现在 7.x 中已弃用并删除:

  • org.optaplanner.core.api.score.comparator.NaturalScoreComparator
  • org.optaplanner.core.api.score.constraint.ConstraintMatchScoreComparator
  • org.optaplanner.core.api.score.constraint.ConstraintMatchTotalScoreComparator
  • org.optaplanner.core.api.score.constraint.IndictmentScoreComparator

OptaPlanner 7 中的 *.java 文件示例:

NaturalScoreComparator comparator = new NaturalScoreComparator();
ConstraintMatchScoreComparator comparator2 = new ConstraintMatchScoreComparator();

OptaPlanner 8 中的 *.java 文件示例:

Comparator<Score> comparator = Comparable::compareTo;
Comparator<ConstraintMatch> comparator2 = Comparator.comparing(ConstraintMatch::getScore);

删除 FeasibilityScore

,公共 API

FeasibilityScore 接口在 7.x 中已弃用,它唯一方法为 isFeasible () 移到 Score supertype。接口现已被删除。

您应该通过其 ultimate 类型引用 Score,例如 HardSoftScore 而不是 Score

@PlanningEntity.movableEntitySelectionFilter removed

,公共 API

@PlanningEntity 注解上的 movableEntitySelectionFilter 字段已在 7.x 中弃用,一个新的字段 pinningFilter 已被引入了一个名称,它显示 @PlanningPin 注释的相关名称。此过滤器实施一个新的 PinningFilter 接口,如果实体被固定,则返回 true,如果可移动,则为 false。因此,与旧过滤器相比,这个新过滤器的逻辑颠倒。

您应该通过提供新过滤器而不是旧过滤器来更新 @PlanningEntity 注释。旧的过滤器现已被删除。

OptaPlanner 7 中的 *.java 文件示例:

@PlanningEntity(movableEntitySelectionFilter = MyMovableEntitySelectionFilter.class)

OptaPlanner 8 中的 *.java 文件示例:

@PlanningEntity(pinningFilter = MyPinningFilter.class)

@PlanningVariable.reinitializeVariableEntityFilter removed

,公共 API

@PlanningVariable 注解上的 reinitializeVariableEntityFilter 字段在 7.x 中弃用,现在已被删除。

*ScoreHolder 类转换为接口

,公共 API

在 OptaPlanner 7 中,ScoreHolder 类专门用于 Drools 分数计算,公开了很多公共方法(如果使用),允许用户意外损坏或对分数造成负面影响。

在 OptaPlanner 8 中,这些方法已被删除,并将类转换为接口。大多数用户都不使用删除的方法,并可能会有害的方法。

但是,如果您确实使用这些方法,您将在分数说明和约束配置区域在公共 API 中找到合适的替换。

ValueRangeFactory 类现在最终

ValueRangeFactory 类是一个仅具有静态方法的工厂类。因此,您不需要扩展这个类,因此最终已成为 最终

OptaPlanner 7 中的 *.java 文件示例:

class MyValueRangeFactory extends ValueRangeFactory {
    ...
}

OptaPlanner 8 中的 *.java 文件示例:

class MyValueRangeFactory {
    ...
}

ConstraintMatchTotalIndictment 现在是接口

,公共 API

ConstraintMatchTotalIndictment 类已转换为接口。因此,它们的实现被从公共 API 中移出,以及允许它们修改其状态的方法。这些方法从不用于公共 API,因此没有替换它们。

如果您选择实施 ConstraintMatchAwareIncrementalScoreCalculator,您可能仍然需要实例:

ConstraintMatchTotal maximumCapacityMatchTotal = new ConstraintMatchTotal(...);

OptaPlanner 8 中的 *.java 文件示例:

ConstraintMatchTotal maximumCapacityMatchTotal = new DefaultConstraintMatchTotal(...);

ScoreManager: 添加了通用类型 Score

公共 API

ScoreManagerScoreExplanation API 现在具有通用类型 分数,以避免在代码中停机,例如从 ScoreHardSoftScore

OptaPlanner 7 中的 *.java 文件示例:

    @Inject // or @Autowired
    ScoreManager<TimeTable> scoreManager;

OptaPlanner 8 中的 *.java 文件示例:

    @Inject // or @Autowired
    ScoreManager<TimeTable, HardSoftScore> scoreManager;

OptaPlanner 7 中的 *.java 文件示例:

    ScoreExplanation<TimeTable> explanation = scoreManager.explainScore(timeTable);
    HardSoftScore score = (HardSoftScore) explanation.getScore();

OptaPlanner 8 中的 *.java 文件示例:

    ScoreExplanation<TimeTable, HardSoftScore> explanation = scoreManager.explainScore(timeTable);
    HardSoftScore score = explanation.getScore();

ConstraintMatchTotal,ConstraintMatch, 和 Indictment: generic type Score added

ScoreManagerScoreExplanation 类似,ConstraintMatchTotalConstraintMatchIndictment API 现在有一个通用类型 Score,以避免在代码中停机,例如从 ScoreHardSoftScore

OptaPlanner 7 中的 *.java 文件示例:

    ScoreExplanation<TimeTable> explanation = scoreManager.explainScore(timeTable);
    Map<String, ConstraintMatchTotal> constraintMatchTotalMap = scoreExplanation.getConstraintMatchTotalMap();
    ConstraintMatchTotal constraintMatchTotal = constraintMatchTotalMap.get(contraintId);
    HardSoftScore totalScore = (HardSoftScore) constraintMatchTotal.getScore();

OptaPlanner 8 中的 *.java 文件示例:

    ScoreExplanation<TimeTable, HardSoftScore> explanation = scoreManager.explainScore(timeTable);
    Map<String, ConstraintMatchTotal<HardSoftScore>> constraintMatchTotalMap = scoreExplanation.getConstraintMatchTotalMap();
    ConstraintMatchTotal<HardSoftScore> constraintMatchTotal = constraintMatchTotalMap.get(contraintId);
    HardSoftScore totalScore = constraintMatchTotal.getScore();

OptaPlanner 7 中的 *.java 文件示例:

    ScoreExplanation<TimeTable> explanation = scoreManager.explainScore(timeTable);
    Map<Object, Indictment> indictmentMap = scoreExplanation.getIndictmentMap();
    Indictment indictment = indictmentMap.get(lesson);
    HardSoftScore totalScore = (HardSoftScore) indictment.getScore();

OptaPlanner 8 中的 *.java 文件示例:

    ScoreExplanation<TimeTable, HardSoftScore> explanation = scoreManager.explainScore(timeTable);
    Map<Object, Indictment<HardSoftScore>> indictmentMap = scoreExplanation.getIndictmentMap();
    Indictment<HardSoftScore> indictment = indictmentMap.get(lesson);
    HardSoftScore totalScore = indictment.getScore();

ConstraintMatchAwareIncrementalScoreCalculator: 通用类型 Score 添加了

现在,接口 ConstraintMatchAwareIncrementalScoreCalculator 也是 Score 的通用类型参数,以避免原始类型使用 ConstraintMatchTotalIndictment

OptaPlanner 7 中的 *.java 文件示例:

public class MachineReassignmentIncrementalScoreCalculator
        implements ConstraintMatchAwareIncrementalScoreCalculator<MachineReassignment> {

    @Override
    public Collection<ConstraintMatchTotal> getConstraintMatchTotals() {
        ...
    }


    @Override
    public Map<Object, Indictment> getIndictmentMap() {
        ...
    }

}

OptaPlanner 8 中的 *.java 文件示例:

public class MachineReassignmentIncrementalScoreCalculator
        implements ConstraintMatchAwareIncrementalScoreCalculator<MachineReassignment, HardSoftLongScore> {

    @Override
    public Collection<ConstraintMatchTotal<HardSoftLongScore>> getConstraintMatchTotals() {
        ...
    }


    @Override
    public Map<Object, Indictment<HardSoftLongScore>> getIndictmentMap() {
        ...
    }

}

AbstractCustomPhaseCommand 已被删除

,公共 API

删除了抽象类 AbstractCustomPhaseCommand。任何扩展它的类都应直接实施 CustomPhaseCommand 接口。

OptaPlanner 7 中的 *.java 文件示例:

public class DinnerPartySolutionInitializer extends AbstractCustomPhaseCommand<DinnerParty> {

    @Override
    public void changeWorkingSolution(ScoreDirector<DinnerParty> scoreDirector) {
        ...
    }

}

OptaPlanner 8 中的 *.java 文件示例:

public class DinnerPartySolutionInitializer implements CustomPhaseCommand<DinnerParty> {

    @Override
    public void changeWorkingSolution(ScoreDirector<DinnerParty> scoreDirector) {
        ...
    }

}

分数计算器被移到公共 API

接口 EasyScoreCalculator,IncrementalScoreCalculator, and ConstraintMatchAwareIncrementalScoreCalculator 已移到公共 API 中的新软件包。其已弃用的 counterparts 已被删除。弃用的类 org.optaplanner.core.impl.score.director.incremental.AbstractIncrementalScoreCalculator 也已被删除。将已删除接口和类的使用替换为公共 API 中的对应部分。

OptaPlanner 7 中的 EasyScoreCalculator.java 文件示例:

  ...
  import org.optaplanner.core.impl.score.director.easy.EasyScoreCalculator;
  ...

  public class CloudBalancingEasyScoreCalculator implements EasyScoreCalculator<CloudBalance> {
    ...
  }

OptaPlanner 8 中的 EasyScoreCalculator.java 文件示例:

  ...
  import org.optaplanner.core.api.score.calculator.EasyScoreCalculator;
  ...

  public class CloudBalancingEasyScoreCalculator implements EasyScoreCalculator<CloudBalance, HardSoftScore> {
    ...
  }

OptaPlanner 7 中的 IncrementalScoreCalculator.java 文件示例:

  ...
  import org.optaplanner.core.impl.score.director.incremental.AbstractIncrementalScoreCalculator;
  ...

  public class CloudBalancingIncrementalScoreCalculator extends AbstractIncrementalScoreCalculator<CloudBalance> {
    ...
  }

OptaPlanner 8 中的 IncrementalScoreCalculator.java 文件示例:

  ...
  import org.optaplanner.core.api.score.calculator.IncrementalScoreCalculator;
  ...

  public class CloudBalancingIncrementalScoreCalculator implements IncrementalScoreCalculator<CloudBalance, HardSoftScore> {
    ...
  }

OptaPlanner 7 中的 ConstraintMatchAwareIncrementalScoreCalculator.java 文件示例:

  ...
  import org.optaplanner.core.impl.score.director.incremental.AbstractIncrementalScoreCalculator;
  import org.optaplanner.core.impl.score.director.incremental.ConstraintMatchAwareIncrementalScoreCalculator;
  ...

  public class CheapTimeConstraintMatchAwareIncrementalScoreCalculator
        extends AbstractIncrementalScoreCalculator<CheapTimeSolution>
        implements ConstraintMatchAwareIncrementalScoreCalculator<CheapTimeSolution> {
    ...
  }

OptaPlanner 8 中的 ConstraintMatchAwareIncrementalScoreCalculator.java 文件示例:

  ...
  import org.optaplanner.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator;
  ...

  public class CheapTimeConstraintMatchAwareIncrementalScoreCalculator
        implements ConstraintMatchAwareIncrementalScoreCalculator<CheapTimeSolution, HardMediumSoftLongScore> {
    ...
  }

PlannerBenchmarkFactory:createFromSolverFactory () removed

公共 API

PlannerBenchmarkFactory.createFromSolverFactory () 方法已弃用,并替换为 PlannerBenchmarkFactory.createFromSolverConfigXmlResource (String) 方法。PlannerBenchmarkFactory.createFromSolverFactory () 方法已被删除。

OptaPlanner 7 中的 *.java 文件示例:

SolverFactory<CloudBalance> solverFactory = SolverFactory.createFromXmlResource(
        ".../cloudBalancingSolverConfig.xml");
PlannerBenchmarkFactory benchmarkFactory = PlannerBenchmarkFactory.createFromSolverFactory(solverFactory);

OptaPlanner 8 中的 *.java 文件示例:

PlannerBenchmarkFactory benchmarkFactory = PlannerBenchmarkFactory.createFromSolverConfigXmlResource(
        ".../cloudBalancingSolverConfig.xml");

如果您以编程方式调整了 solver 配置,您可以使用 PlannerBenchmarkConfig.createFromSolverConfig (SolverConfig),然后 PlannerBenchmarkFactory.create (PlannerBenchmarkConfig)

PlannerBenchmarkFactory:getPlannerBenchmarkConfig () 已删除

,公共 API

PlannerBenchmarkFactory.getPlannerBenchmarkConfig () 方法已弃用,并替换为 PlannerBenchmarkFactory.create (PlannerBenchmarkConfig) 方法。PlannerBenchmarkConfig 实例现在在 PlannerBenchmarkFactory 实例实例化前实例化。该顺序更为逻辑。PlannerBenchmarkFactory.getPlannerBenchmarkConfig () 已被删除。

OptaPlanner 7 中的 *.java 文件示例:

PlannerBenchmarkFactory benchmarkFactory = PlannerBenchmarkFactory.createFromXmlResource(
        ".../cloudBalancingBenchmarkConfig.xml");
PlannerBenchmarkConfig benchmarkConfig = benchmarkFactory.getPlannerBenchmarkConfig();
...
PlannerBenchmark benchmark = benchmarkFactory.buildPlannerBenchmark();

OptaPlanner 8 中的 *.java 文件示例:

PlannerBenchmarkConfig benchmarkConfig = PlannerBenchmarkConfig.createFromXmlResource(
        ".../cloudBalancingBenchmarkConfig.xml");
...
PlannerBenchmarkFactory benchmarkFactory = PlannerBenchmarkFactory.create(benchmarkConfig);
PlannerBenchmark benchmark = benchmarkFactory.buildPlannerBenchmark();

XML <plannerBenchmark/> root 元素现在属于 http://www.optaplanner.org/xsd/benchmark 命名空间

,公共 API

OptaPlanner 现在为基准配置提供 XML 架构定义(XSD)。虽然 OptaPlanner 与早期版本的现有 XML 配置保持兼容,但强烈建议迁移到 XSD,因为 OptaPlanner 可能以后只支持有效的配置 XML。

OptaPlanner 7 中的 *BenchmarkConfig.xml 文件示例:

<?xml version="1.0" encoding="UTF-8"?>
<plannerBenchmark>
  ...
</plannerBenchmark>

OptaPlanner 8 中的 *BenchmarkConfig.xml 文件示例:

<?xml version="1.0" encoding="UTF-8"?>
<plannerBenchmark xmlns="https://www.optaplanner.org/xsd/benchmark" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://www.optaplanner.org/xsd/benchmark https://www.optaplanner.org/xsd/benchmark/benchmark.xsd">
  ...
</plannerBenchmark>

使用 XSD 可能需要对配置的一些 XML 元素重新排序。在 IDE 中使用代码完成来迁移到有效的 XML。

ProblemBenchmarksConfig:xStreamAnnotatedClass removed

公共 API

{mAnnotatedClas} 已从 < problemBenchmarks/&gt; 配置以及对应的 getXStreamAnnotatedClassList ()setXStreamAnnotatedClassList () 方法中删除。

OptaPlanner 7 中的 *.java 文件示例:

ProblemBenchmarksConfig problemBenchmarksConfig = new ProblemBenchmarksConfig();
problemBenchmarksConfig.setXStreamAnnotatedClassList(MySolution.class);

OptaPlanner 8 中的 *.java 文件示例:

package com.example;
...
public class MySolutionFileIO extends XStreamSolutionFileIO<MySolution> {
    public MySolutionFileIO() {
        super(MySolution.class);
    }
}

...

ProblemBenchmarksConfig problemBenchmarksConfig = new ProblemBenchmarksConfig();
problemBenchmarksConfig.setSolutionFileIOClass(MySolutionFileIO.class);

OptaPlanner 7 中的 *BenchmarkConfig.xml 文件示例:

<plannerBenchmark>
...
  <solverBenchmark>
    <problemBenchmarks>
      <xStreamAnnotatedClass>com.example.MySolution</xStreamAnnotatedClass>
      ...
    </problemBenchmarks>
    ...
  </solverBenchmark>
...
</plannerBenchmark>

OptaPlanner 8 中的 *BenchmarkConfig.xml 文件示例:

<plannerBenchmark>
...
  <solverBenchmark>
    <problemBenchmarks>
      <!-- See the "After in *.java" section to create the MySolutionFileIO. -->
      <solutionFileIOClass>com.example.MySolutionFileIO</solutionFileIOClass>
      ...
    </problemBenchmarks>
    ...
  </solverBenchmark>
...
</plannerBenchmark>

BenchmarkAggregatorFrame.createAndDisplay (PlannerBenchmarkFactory) 已删除

BenchmarkAggregatorFrame.createAndDisplay (PlannerBenchmarkFactory) 方法已弃用,并替换为 BenchmarkAggregatorFrame.createAndDisplayFromXmlResource (String) 方法。BenchmarkAggregatorFrame.createAndDisplay (PlannerBenchmarkFactory) 方法已被删除。

OptaPlanner 7 中的 *.java 文件示例:

PlannerBenchmarkFactory benchmarkFactory = PlannerBenchmarkFactory.createFromXmlResource(
        ".../cloudBalancingBenchmarkConfig.xml");
BenchmarkAggregatorFrame.createAndDisplay(benchmarkFactory);

OptaPlanner 8 中的 *.java 文件示例:

BenchmarkAggregatorFrame.createAndDisplayFromXmlResource(
        ".../cloudBalancingBenchmarkConfig.xml");

如果您以编程方式调整基准配置,您可以使用 BenchmarkAggregatorFrame.createAndDisplay (PlannerBenchmarkConfig)

删除了配置中的 JavaScript 表达式支持

solver 配置和基准配置的各种元素不再支持嵌套 JavaScript 表达式。您必须将它们替换为自动配置,或替换为整数常量。

OptaPlanner 7 中的 solverConfig.xml 文件中的示例:

    <solver>
        ...
        <moveThreadCount>availableProcessorCount - 1</moveThreadCount>
        ...
    </solver>

OptaPlanner 8 中的 'solverConfig.xml'file 的示例:

    <solver>
        ...
        <moveThreadCount>1</moveThreadCount> <!-- Alternatively, use "AUTO" or omit entirely. -->
        ...
    </solver>

OptaPlanner 7 中的 benchmarkConfig.xml 文件示例:

    <plannerBenchmark>
      ...
      <parallelBenchmarkCount>availableProcessorCount - 1</parallelBenchmarkCount>
      ...
    </plannerBenchmark>

OptaPlanner 8 中的 benchmarkConfig.xml 文件示例:

    <plannerBenchmark>
      ...
      <parallelBenchmarkCount>1</parallelBenchmarkCount> <!-- Alternatively, use "AUTO" or omit entirely. -->
      ...
    </plannerBenchmark>

删除了已弃用的变量监听程序

公共 API

软件包 org.optaplanner.core.impl.domain.variable.listener 中的已弃用的接口 VariableListener 已被删除,以及已弃用的接口 StatefulVariableListener 以及同一软件包中已弃用的类 VariableListenerAdapter。使用 org.optaplanner.core.api.domain.variable 软件包中的 VariableListener 接口。

OptaPlanner 7 中的 VariableListener.java 文件示例:

  ...
  import org.optaplanner.core.impl.domain.variable.listener.VariableListenerAdapter;
  ...

  public class MyVariableListener extends VariableListenerAdapter<Object> {

    ...

    @Override
    void afterEntityRemoved(ScoreDirector scoreDirector, Object entity);
      ...
    }

    ...
  }

OptaPlanner 8 中的 VariableListener.java 文件示例:

  ...
  import org.optaplanner.core.api.domain.variable.VariableListener;
  ...

  public class MyVariableListener extends VariableListener<MySolution, Object> {

    ...

    @Override
    void afterEntityRemoved(ScoreDirector<MySolution> scoreDirector, Object entity);
      ...
    }

    ...
  }

OptaPlanner 7 中的 StatefulVariableListener.java 文件示例:

  ...
  import org.optaplanner.core.impl.domain.variable.listener.StatefulVariableListener;
  ...

  public class MyStatefulVariableListener implements StatefulVariableListener<Object> {

    ...

    @Override
    public void clearWorkingSolution(ScoreDirector scoreDirector) {
      ...
    }

    ...
  }

OptaPlanner 8 中的 StatefulVariableListener.java 文件示例:

  ...
  import org.optaplanner.core.api.domain.variable.VariableListener;
  ...

  public class MyStatefulVariableListener implements VariableListener<MySolution, Object> {

    ...

    @Override
    public void close() {
      ...
    }

    ...
  }

第 2 章 OptaPlanner 8.2.0 和 OptaPlanner 8.3.0 之间的变化

本节中列出的更改是在 OptaPlanner 8.2.0 和 OptaPlanner 8.3.0 之间进行的。

ConstraintMatch.compareTo ()equals ()不一致

ConstraintMatch 中的 equals () 覆盖已被删除。因此,两个不同的 ConstraintMatch 实例永远不会被视为相等。这与 compareTo () 方法相对,如果所有字段值相等,则继续将两个实例视为相等。

部分 II. 红帽构建的 OptaPlanner 入门

作为业务规则开发人员,您可以使用红帽构建的 OptaPlanner 来查找根据一组有限资源和特定限制规划问题的最佳解决方案。

使用本文档通过 OptaPlanner 开始开发解决者。

第 3 章 红帽构建的 OptaPlanner 简介

OptaPlanner 是一个轻量级、可嵌入的规划引擎,可优化计划问题。它有助于一般的 Java 编程人员有效地解决规划问题,并将优化风格和元化与高效率的分数计算相结合。

例如,OptaPlanner 帮助解决各种用例:

  • 员工/基本文章 :它有助于为国业创建时间,并跟踪病人护理管理。
  • 教育 时间表:它有助于安排课程、课程、考试和会议演示。
  • Shop Schedules :跟踪回车装配行、机器队列规划和工作任务规划。
  • 剪切 Stock :通过减少纸张和窃取等资源的消耗来最小化浪费。

每个机构都面临规划问题;也就是说,它们为产品和服务提供有限的受限资源(员工、资产、时间和金钱)。

OptaPlanner 是 Apache 软件许可证 2.0 下的开源软件。这是 100% 的纯 Java,在大多数 Java 虚拟机(JVM)上运行。

3.1. 规划问题

规划问题 具有最佳目标,基于有限的资源和特定限制。最佳目标可以是任意数量的内容,例如:

  • 最大化利润 - 最佳目标使利润达到最高可能。
  • 最小化的经济占用空间 - 最佳目标对环境的影响最少。
  • 最大化员工或客户满意度 - 最佳目标优先选择员工或客户的需求。

实现这些目标的能力取决于可用资源的数量。例如,可能会限制以下资源:

  • 人员数量
  • 时间量
  • 预算
  • 物理资产,如 machinery、vehicles、计算机、构建

您还必须考虑与这些资源相关的特定限制,如个人工作的小时数、他们使用某些机器或设备组件之间的兼容性的能力。

红帽构建的 OptaPlanner 帮助 Java 编程人员有效地解决约束问题。它结合了优化 heuristics 和 metaheuristics 与有效的分数计算。

3.2. 计划问题中的 NP-completeness

提供的用例 可能是 NP-complete 或 NP-hard,这意味着适用以下语句:

  • 可轻松验证特定解决方案以便获得合理的时间。
  • 合理的时间无法找到问题的最佳解决方案。

这意味着,解决您的问题可能比您期望的更难,因为两种常见的技术不可靠:

  • 括号强制算法(甚至更高级变体)用时过长。
  • 例如,一个快速算法(例如,在组合问题 中)放在最大的项目中,首先返回一个远于 最佳的解决方案。

通过使用高级优化算法,OptaPlanner 在这类规划问题合理时间内找到了很好的解决方案。

3.3. 规划问题的解决方案

计划问题有很多解决方案。

几种解决方案类别有:

可能的解决方案
可能的解决方案是任何解决方案,无论它是否破坏了任意数量的限制。规划问题通常具有大量可能的解决方案。其中许多解决方案都不可用。
可行的解决方案
可行的解决方案是不会破坏任何(负)硬约束的解决方案。可行的解决方案数量与可能的解决方案数量相对。有时没有可行的解决方案。每种可行的解决方案都是可能的解决方案。
最佳解决方案
最佳解决方案是具有最高分数的解决方案。规划问题通常具有几个最佳解决方案。它们始终至少有一个最佳解决方案,即使是没有可行的解决方案,且最佳解决方案不可行。
最佳解决方案发现
最佳解决方案是实施在指定时间内获得的最高分数的解决方案。找到的最佳解决方案可能可行,考虑到足够的时间,这是最佳解决方案。

Counterintuitive,可能的解决方案数量是很大(如果正确计算),即使数据集小也是如此。

planner-engine 分发文件夹中提供的示例中,大多数实例都有大量可能的解决方案。由于无法保证找到最佳解决方案,任何实施都强制评估所有可能解决方案的子集。

OptaPlanner 支持多种优化算法,通过非常广泛的可能的解决方案来高效地偏离。

根据用例,一些优化算法的性能优于其他算法,但无法提前了解。使用 OptaPlanner,您可以通过更改 XML 或代码的几行中的 solver 配置来切换优化算法。

3.4. 对规划问题的约束

通常,计划问题至少有两个级别的限制:

  • (负)硬约束 不能中断。

    例如,一个教员无法同时教授两个不同的课程。

  • 如果可以避免,不应破坏 (负)软约束

    例如,Teacher A 不喜欢在周五之后教授。

有些问题也具有正限制:

  • 应尽可能 实现正软约束(或 获得者)。

    例如,Teacher B 喜欢在周早上教授。

某些基本问题只有硬限制。有些问题有三个或更多个约束,如 hard、medium 和 soft 约束。

这些限制定义了规划问题 的分数计算 (也称为适合 函数)。规划问题的每个解决方案均以分数的形式进行评分。使用 OptaPlanner 时,分数限制使用面向对象的语言(如 Java)或 Drools 规则编写。

这种代码非常灵活且可扩展的。

3.5. 红帽构建的 OptaPlanner 提供的示例

Red Hat Process Automation Manager 提供了几个红帽构建的 OptaPlanner 示例。您可以查看示例的代码,并根据需要进行修改,以满足您的需要。

注意

红帽不提供对 Red Hat Process Automation Manager 发行版中包含的示例代码的支持。

某些 OptaPlanner 示例解决了在紧张中呈现的问题。下表中的就列列出了 contests。它还将示例识别为 现实 或非现实真实的 contest 是符合以下标准的官方独立调查:

  • 明确定义的实际用例
  • 实时限制
  • 多个现实数据集
  • 在特定硬件上的特定时间限制中可重复生成结果
  • 积极参与来自院校和/或企业运营研究社区的严格参与。

real real contests 提供 OptaPlanner 与竞争软件和学术研究的目标比较。

表 3.1. 示例概述

示例Domain大小候选人目录名称

N queens

1 个实体类

(1 变量)

实体 rhacm 256

值 mvapich 256

搜索空间 rhacm 10^616

无点(可热点)

nqueens

负载均衡

1 个实体类

(1 变量)

实体 hawkular 2400

值 mvapich 800

搜索空间 rhacm 10^6967

否(由我们定义)

Cloudbalancing

traveling salesman

1 个实体类

(1 个串联变量)

实体 rhacm 980

值 mvapich 980

搜索空间 rhacm 10^2504

unrealistic TSP web

tsp

Tennis club 调度

1 个实体类

(1 变量)

实体 hawkular 72

值 mvapich 7

搜索空间 rhacm 10^60

否(由我们定义)

tennis

满足调度

1 个实体类

(2 变量)

实体 hawkular 10

值 mvapich 320 和 mvapich 5

搜索空间 rhacm 10^320

否(由我们定义)

会议调度

课程时间组

1 个实体类

(2 变量)

实体 rhacm 434

值 mvapich 25 和 mvapich 20

搜索空间 rhacm 10^1171

真实的 ITC 2007 跟踪 3

curriculumCourse

机器重新分配

1 个实体类

(1 变量)

实体 hawkular 50000

值 mvapich 5000

搜索空间 rhacm 10^184948

2012 年现实 ROADEF

machineReassignment

vehicle 路由

1 个实体类

(1 个串联变量)

1 个影子实体类

(1 个自动影子变量)

实体 rhacm 2740

值 mvapich 2795

搜索空间 rhacm 10^8380

不切实际的 VRP 网络

vehiclerouting

带有时间窗的载体 路由

所有 Vehicle 路由

(1 shadow 变量)

实体 rhacm 2740

值 mvapich 2795

搜索空间 rhacm 10^8380

不切实际的 VRP 网络

vehiclerouting

项目作业调度

1 个实体类

(2 变量)

(1 shadow 变量)

实体 rhacm 640

Value ? 和 hawkular ?

搜索空格 DAEMON ?

2013 年几乎真实的 MISTA

projectjobscheduling

任务分配

1 个实体类

(1 个串联变量)

(1 shadow 变量)

1 个影子实体类

(1 个自动影子变量)

实体 mvapich 500

值 mvapich 520

搜索空间 rhacm 10^1168

没有由我们定义

任务分配

考试时间组

2 个实体类(相同层次结构)

(2 变量)

实体 hawkular 1096

值 mvapich 80 和 mvapich 49

搜索空间 rhacm 10^3374

2007 年真实的 ITC 研究 1

检查

Nurse rostering

1 个实体类

(1 变量)

实体 rhacm 752

值 mvapich 50

搜索空间 rhacm 10^1277

2010 年现实的 INRC

nurserostering

traveling tournament

1 个实体类

(1 变量)

实体 hawkular 1560

值 mvapich 78

搜索空间 rhacm 10^2301

不切实际的 TTP

travelingtournament

更便宜的时间调度

1 个实体类

(2 变量)

实体 mvapich 500

值 mvapich 100 和 rhacm 288

搜索空间 rhacm 10^20078

几乎真实的 ICON Energy

cheaptimescheduling

投资

1 个实体类

(1 变量)

实体 rhacm 11

Value = 1000

搜索空间 rhacm 10^4

没有由我们定义

投资

会议调度

1 个实体类

(2 变量)

实体 rhacm 216

值 mvapich 18 和 mvapich 20

搜索空间 rhacm 10^552

没有由我们定义

conferencescheduling

rock Tur

1 个实体类

(1 个串联变量)

(4 个影子变量)

1 个影子实体类

(1 个自动影子变量)

实体 hawkular 47

值 mvapich 48

搜索空间 rhacm 10^59

没有由我们定义

rocktour

flight crew 调度

1 个实体类

(1 变量)

1 个影子实体类

(1 个自动影子变量)

实体 DAEMON 4375

值 mvapich 750

搜索空间 rhacm 10^12578

没有由我们定义

flightcrewscheduling

3.6. N queens

n sized chessboard 上放置 N 个queens 数量,因此没有两个 queens 相互攻击。最常见的 n queens puzzle 是八个 queens puzzle,n = 8

nQueensScreenshot

约束:

  • 使用 n 列和 n 行。
  • n queens 放置到 chessboard 上。
  • 没有两个 queens 相互攻击。queen 可以在同一个横向、垂直或诊断行中互相攻击。

本文档主要使用 4 queens puzzle 作为主要示例。

建议的解决方案可以是:

图 3.1. 错误解决方案,用于 4 queens puzzle

partiallySolvedNQueens04Explained

以上解决方案错误,因为 queens A1B0 可以相互攻击(从而可以相互攻击 B0D0)。删除 queen B0 会遵守"无两个 queens 可相互攻击"约束,但可能会破坏"place n queens"约束。

以下是正确的解决方案:

图 3.2. 用于 Four queens puzzle 的正确解决方案

solvedNQueens04

满足了所有限制,因此解决方案正确。

请注意,最多 n queens puzzles 具有多个正确的解决方案。我们将专注于为特定 n 查找单个正确的解决方案,而不是查找特定 n 可能的正确解决方案。

问题大小

4queens   has   4 queens with a search space of    256.
8queens   has   8 queens with a search space of   10^7.
16queens  has  16 queens with a search space of  10^19.
32queens  has  32 queens with a search space of  10^48.
64queens  has  64 queens with a search space of 10^115.
256queens has 256 queens with a search space of 10^616.

n queens 示例中的实现没有被优化,因为它作为初学者示例运行。然而,它可以轻松处理 64 个问题。随着一些更改,它已被显示,可以轻松地处理 5000 频率等。

3.6.1. N queens 的域模型

这个示例使用域模型解决四个问题。

  • 创建域模型

    良好的域模型将使您更轻松地理解和解决您的规划问题。

    这是 n queens 示例的域模型:

    public class Column {
    
        private int index;
    
        // ... getters and setters
    }
    public class Row {
    
        private int index;
    
        // ... getters and setters
    }
    public class Queen {
    
        private Column column;
        private Row row;
    
        public int getAscendingDiagonalIndex() {...}
        public int getDescendingDiagonalIndex() {...}
    
        // ... getters and setters
    }
  • 计算搜索空间.

    Queen 实例有一个 Column (例如: 0 是列 A, 1 is column B, …​) 和一个 Row (例如,0 一行,1 为行 1, …​)。

    可以按列和行计算升序诊断行和降序诊断行。

    列和行索引从 chessboard 的左上角启动。

    public class NQueens {
    
        private int n;
        private List<Column> columnList;
        private List<Row> rowList;
    
        private List<Queen> queenList;
    
        private SimpleScore score;
    
        // ... getters and setters
    }
  • 查找解决方案

    单个 NQueens 实例包含所有 Queen 实例的列表。它是提供给、解决并从 Solver 检索的解决方案实施。

请注意,在四个 queens 示例中,NQueens getN () 方法始终返回四个。

图 3.3. Four Queens 的解决方案

partiallySolvedNQueens04Explained

表 3.2. 域模型中的解决方案详情

 columnIndexrowIndexatingDiagonalIndex (columnIndex + rowIndex)descendingDiagonalIndex (columnIndex - rowIndex)

A1

0

1

1 (**)

-1

B0

1

0 (*)

1 (**)

1

C2

2

2

4

0

D0

3

0 (*)

3

3

当两个 queens 共享同一列时,行或诊断行,如 principal 和(**),他们可以相互攻击。

3.7. 负载均衡

有关此示例的详情,请参考 红帽构建的 OptaPlanner 快速启动指南

3.8. 旅行销售员(TSP - traveling Salesman 问题)

考虑到城市列表,请找到获得每个城市完全访问的销售人员的最短产品。

这个问题由 Wikipedia 定义。它是计算数学 中最广泛的问题之一。然而,在现实世界中,它通常只是规划问题的一部分,以及其他限制,如员工的转变限制。

问题大小

dj38     has  38 cities with a search space of   10^43.
europe40 has  40 cities with a search space of   10^46.
st70     has  70 cities with a search space of   10^98.
pcb442   has 442 cities with a search space of  10^976.
lu980    has 980 cities with a search space of 10^2504.

问题困难

尽管 TSP 的简单定义,但问题很难解决。因为它是一个 NP-hard 问题(如大多数规划问题),所以当问题数据集略有变化时,特定问题数据集的最佳解决方案可能会有较大变化:

tspOptimalSolutionVolatility

3.9. Tennis club 调度

每周,10nis club 均有四个团队互相扮演循环。为团队随机分配这四个点。

硬限制:

  • 冲突:团队每天只能执行一次。
  • 不可用:有些团队在某些日期不可用。

中型限制:

  • 公平分配:所有团队都应对(几乎)等于次数。

软限制:

  • evenly confrontation:每个团队应针对其他团队执行相同次数的团队。

问题大小

munich-7teams has 7 teams, 18 days, 12 unavailabilityPenalties and 72 teamAssignments with a search space of 10^60.

图 3.4. 域模型

tennisClassDiagram

3.10. 满足调度

为开始时间和房间分配每个会议。会议有不同的持续时间。

硬限制:

  • 房间冲突:两个会议不能同时使用同一个房间。
  • 需要的参与者:同时,一个人员不能同时拥有两个所需的会议。
  • 所需房间容量:会议不得位于无法满足所有会议的参与者的房间。
  • 在同一天开始和结束:不计划超过多天的会议。

中型限制:

  • 首选参与者:个人不能同时拥有两个首选的会议,也不能同时拥有首选的会议。

软限制:

  • 早于早于:尽快计划所有会议。
  • 会议间断:两个会议之间应至少一张时间休息。
  • 重叠会议:为了尽可能减少并行会议的数量,因此人们不必选择彼此的会议。
  • 首先分配更大的房间:如果有大房可用,则应该为这个空间分配任何会议,以便尽可能多地容纳尽可能多的人,即使他们未签约该会议。
  • 房间稳定性:如果个人连续两个会议,它们之间有两个或更少的时间中断,则它们之间可以更好地位于相同的房间。

问题大小

50meetings-160timegrains-5rooms  has  50 meetings, 160 timeGrains and 5 rooms with a search space of 10^145.
100meetings-320timegrains-5rooms has 100 meetings, 320 timeGrains and 5 rooms with a search space of 10^320.
200meetings-640timegrains-5rooms has 200 meetings, 640 timeGrains and 5 rooms with a search space of 10^701.
400meetings-1280timegrains-5rooms has 400 meetings, 1280 timeGrains and 5 rooms with a search space of 10^1522.
800meetings-2560timegrains-5rooms has 800 meetings, 2560 timeGrains and 5 rooms with a search space of 10^3285.

3.11. 课程时间表(ITC 2007 Track 3 - Curriculum course Scheduling)

将每个讲座安排成分机和房间。

硬限制:

  • 教师冲突:老师不得在同一期间有两门讲座。
  • 课程冲突:在同一期间,课程不能有两门课程。
  • 房间两个讲座:两个讲座不能在同一期间处于同一个房间。
  • 不可用期限(每个数据集指定):不能将特定的讲座分配给特定期间。

软限制:

  • 房间容量:房间容量不应小于其讲座中的学员数量。
  • 最小工作日:同一课程的指南应分布到最少的天数。
  • 课程紧凑性:属于相同课程的内容应相互相邻(连续的期间)。
  • 房间稳定性:同一课程的概念应当分配到同一个房间。

此问题由 2007 年国际时间稳定竞争 3 定义

问题大小

comp01 has 24 teachers,  14 curricula,  30 courses, 160 lectures, 30 periods,  6 rooms and   53 unavailable period constraints with a search space of  10^360.
comp02 has 71 teachers,  70 curricula,  82 courses, 283 lectures, 25 periods, 16 rooms and  513 unavailable period constraints with a search space of  10^736.
comp03 has 61 teachers,  68 curricula,  72 courses, 251 lectures, 25 periods, 16 rooms and  382 unavailable period constraints with a search space of  10^653.
comp04 has 70 teachers,  57 curricula,  79 courses, 286 lectures, 25 periods, 18 rooms and  396 unavailable period constraints with a search space of  10^758.
comp05 has 47 teachers, 139 curricula,  54 courses, 152 lectures, 36 periods,  9 rooms and  771 unavailable period constraints with a search space of  10^381.
comp06 has 87 teachers,  70 curricula, 108 courses, 361 lectures, 25 periods, 18 rooms and  632 unavailable period constraints with a search space of  10^957.
comp07 has 99 teachers,  77 curricula, 131 courses, 434 lectures, 25 periods, 20 rooms and  667 unavailable period constraints with a search space of 10^1171.
comp08 has 76 teachers,  61 curricula,  86 courses, 324 lectures, 25 periods, 18 rooms and  478 unavailable period constraints with a search space of  10^859.
comp09 has 68 teachers,  75 curricula,  76 courses, 279 lectures, 25 periods, 18 rooms and  405 unavailable period constraints with a search space of  10^740.
comp10 has 88 teachers,  67 curricula, 115 courses, 370 lectures, 25 periods, 18 rooms and  694 unavailable period constraints with a search space of  10^981.
comp11 has 24 teachers,  13 curricula,  30 courses, 162 lectures, 45 periods,  5 rooms and   94 unavailable period constraints with a search space of  10^381.
comp12 has 74 teachers, 150 curricula,  88 courses, 218 lectures, 36 periods, 11 rooms and 1368 unavailable period constraints with a search space of  10^566.
comp13 has 77 teachers,  66 curricula,  82 courses, 308 lectures, 25 periods, 19 rooms and  468 unavailable period constraints with a search space of  10^824.
comp14 has 68 teachers,  60 curricula,  85 courses, 275 lectures, 25 periods, 17 rooms and  486 unavailable period constraints with a search space of  10^722.

图 3.5. 域模型

curriculumCourseClassDiagram

3.12. Machine reassignment (Google ROADEF 2012)

为机器分配每个进程。所有进程已经有原始(未优化)分配。每个进程需要每个资源(如 CPU 或 RAM)的数量。这是 Cloud Balancing 示例的一个更加复杂的版本。

硬限制:

  • 最大容量:不得超过每台机器的最大资源容量。
  • 冲突:同一服务的进程必须在不同的机器上运行。
  • 分散 :同一服务的进程必须在不同的位置分散。
  • 依赖项:服务的进程取决于另一个服务的进程,必须在其他服务的进程的邻居中运行。
  • 临时使用:有些资源是临时的,计算原始机器作为新分配的机器的最大容量。

软限制:

  • load :不应超过每台机器的每个资源的安全容量。
  • Balance:通过平衡每台计算机上的可用资源,为未来分配留出空间。
  • 进程移动成本:流程具有移动成本。
  • 服务移动成本:服务具有移动成本。
  • 机器移动成本:将流程从机器 A 移到机器 B 具有另一个 A-B 特定移动成本。

这个问题由 Google ROADEF/EURO Challenge 2012 定义。

cloudOptimizationIsLikeTetris

图 3.6. 价值定位

cloudOptimizationValueProposition

问题大小

model_a1_1 has  2 resources,  1 neighborhoods,   4 locations,    4 machines,    79 services,   100 processes and 1 balancePenalties with a search space of     10^60.
model_a1_2 has  4 resources,  2 neighborhoods,   4 locations,  100 machines,   980 services,  1000 processes and 0 balancePenalties with a search space of   10^2000.
model_a1_3 has  3 resources,  5 neighborhoods,  25 locations,  100 machines,   216 services,  1000 processes and 0 balancePenalties with a search space of   10^2000.
model_a1_4 has  3 resources, 50 neighborhoods,  50 locations,   50 machines,   142 services,  1000 processes and 1 balancePenalties with a search space of   10^1698.
model_a1_5 has  4 resources,  2 neighborhoods,   4 locations,   12 machines,   981 services,  1000 processes and 1 balancePenalties with a search space of   10^1079.
model_a2_1 has  3 resources,  1 neighborhoods,   1 locations,  100 machines,  1000 services,  1000 processes and 0 balancePenalties with a search space of   10^2000.
model_a2_2 has 12 resources,  5 neighborhoods,  25 locations,  100 machines,   170 services,  1000 processes and 0 balancePenalties with a search space of   10^2000.
model_a2_3 has 12 resources,  5 neighborhoods,  25 locations,  100 machines,   129 services,  1000 processes and 0 balancePenalties with a search space of   10^2000.
model_a2_4 has 12 resources,  5 neighborhoods,  25 locations,   50 machines,   180 services,  1000 processes and 1 balancePenalties with a search space of   10^1698.
model_a2_5 has 12 resources,  5 neighborhoods,  25 locations,   50 machines,   153 services,  1000 processes and 0 balancePenalties with a search space of   10^1698.
model_b_1  has 12 resources,  5 neighborhoods,  10 locations,  100 machines,  2512 services,  5000 processes and 0 balancePenalties with a search space of  10^10000.
model_b_2  has 12 resources,  5 neighborhoods,  10 locations,  100 machines,  2462 services,  5000 processes and 1 balancePenalties with a search space of  10^10000.
model_b_3  has  6 resources,  5 neighborhoods,  10 locations,  100 machines, 15025 services, 20000 processes and 0 balancePenalties with a search space of  10^40000.
model_b_4  has  6 resources,  5 neighborhoods,  50 locations,  500 machines,  1732 services, 20000 processes and 1 balancePenalties with a search space of  10^53979.
model_b_5  has  6 resources,  5 neighborhoods,  10 locations,  100 machines, 35082 services, 40000 processes and 0 balancePenalties with a search space of  10^80000.
model_b_6  has  6 resources,  5 neighborhoods,  50 locations,  200 machines, 14680 services, 40000 processes and 1 balancePenalties with a search space of  10^92041.
model_b_7  has  6 resources,  5 neighborhoods,  50 locations, 4000 machines, 15050 services, 40000 processes and 1 balancePenalties with a search space of 10^144082.
model_b_8  has  3 resources,  5 neighborhoods,  10 locations,  100 machines, 45030 services, 50000 processes and 0 balancePenalties with a search space of 10^100000.
model_b_9  has  3 resources,  5 neighborhoods, 100 locations, 1000 machines,  4609 services, 50000 processes and 1 balancePenalties with a search space of 10^150000.
model_b_10 has  3 resources,  5 neighborhoods, 100 locations, 5000 machines,  4896 services, 50000 processes and 1 balancePenalties with a search space of 10^184948.

图 3.7. 域模型

machineReassignmentClassDiagram

3.13. vehicle 路由

使用一组车辆,获取每个客户的对象并将其引入独立。每个 vehicle 都可以服务于多个客户,但其容量有限。

vehicleRoutingUseCase

除了基本大小写(CVRP)外,还有一个包含时间窗(CVRPTW)的变体。

硬限制:

  • 载体容量:其容量之后无法处理更多项目。
  • 时间窗口(仅在 CVRPTW 中):

    • 差旅时间:从一个地点到另一地点时间。
    • 客户服务持续时间:在服务持续时间内必须保持客户保持在客户时限。
    • 客户准备时间:客户准备时间之前可能到达,但必须在服务前等待就绪时间。
    • 客户到期时间:由于时间,在客户到期之前,Pevehicle 必须到达时间。

软限制:

  • 总计 distance :减少所有 vehicles 的总距离驱动(fuel 消耗)。

capacitated vehicle 路由问题(CVRP)及其定时序变体(CVRPTW)由 VRP Web 定义。

图 3.8. 价值定位

vehicleRoutingValueProposition

问题大小

CVRP 实例(无时间窗口):

belgium-n50-k10             has  1 depots, 10 vehicles and   49 customers with a search space of   10^74.
belgium-n100-k10            has  1 depots, 10 vehicles and   99 customers with a search space of  10^170.
belgium-n500-k20            has  1 depots, 20 vehicles and  499 customers with a search space of 10^1168.
belgium-n1000-k20           has  1 depots, 20 vehicles and  999 customers with a search space of 10^2607.
belgium-n2750-k55           has  1 depots, 55 vehicles and 2749 customers with a search space of 10^8380.
belgium-road-km-n50-k10     has  1 depots, 10 vehicles and   49 customers with a search space of   10^74.
belgium-road-km-n100-k10    has  1 depots, 10 vehicles and   99 customers with a search space of  10^170.
belgium-road-km-n500-k20    has  1 depots, 20 vehicles and  499 customers with a search space of 10^1168.
belgium-road-km-n1000-k20   has  1 depots, 20 vehicles and  999 customers with a search space of 10^2607.
belgium-road-km-n2750-k55   has  1 depots, 55 vehicles and 2749 customers with a search space of 10^8380.
belgium-road-time-n50-k10   has  1 depots, 10 vehicles and   49 customers with a search space of   10^74.
belgium-road-time-n100-k10  has  1 depots, 10 vehicles and   99 customers with a search space of  10^170.
belgium-road-time-n500-k20  has  1 depots, 20 vehicles and  499 customers with a search space of 10^1168.
belgium-road-time-n1000-k20 has  1 depots, 20 vehicles and  999 customers with a search space of 10^2607.
belgium-road-time-n2750-k55 has  1 depots, 55 vehicles and 2749 customers with a search space of 10^8380.
belgium-d2-n50-k10          has  2 depots, 10 vehicles and   48 customers with a search space of   10^74.
belgium-d3-n100-k10         has  3 depots, 10 vehicles and   97 customers with a search space of  10^170.
belgium-d5-n500-k20         has  5 depots, 20 vehicles and  495 customers with a search space of 10^1168.
belgium-d8-n1000-k20        has  8 depots, 20 vehicles and  992 customers with a search space of 10^2607.
belgium-d10-n2750-k55       has 10 depots, 55 vehicles and 2740 customers with a search space of 10^8380.

A-n32-k5  has 1 depots,  5 vehicles and  31 customers with a search space of  10^40.
A-n33-k5  has 1 depots,  5 vehicles and  32 customers with a search space of  10^41.
A-n33-k6  has 1 depots,  6 vehicles and  32 customers with a search space of  10^42.
A-n34-k5  has 1 depots,  5 vehicles and  33 customers with a search space of  10^43.
A-n36-k5  has 1 depots,  5 vehicles and  35 customers with a search space of  10^46.
A-n37-k5  has 1 depots,  5 vehicles and  36 customers with a search space of  10^48.
A-n37-k6  has 1 depots,  6 vehicles and  36 customers with a search space of  10^49.
A-n38-k5  has 1 depots,  5 vehicles and  37 customers with a search space of  10^49.
A-n39-k5  has 1 depots,  5 vehicles and  38 customers with a search space of  10^51.
A-n39-k6  has 1 depots,  6 vehicles and  38 customers with a search space of  10^52.
A-n44-k7  has 1 depots,  7 vehicles and  43 customers with a search space of  10^61.
A-n45-k6  has 1 depots,  6 vehicles and  44 customers with a search space of  10^62.
A-n45-k7  has 1 depots,  7 vehicles and  44 customers with a search space of  10^63.
A-n46-k7  has 1 depots,  7 vehicles and  45 customers with a search space of  10^65.
A-n48-k7  has 1 depots,  7 vehicles and  47 customers with a search space of  10^68.
A-n53-k7  has 1 depots,  7 vehicles and  52 customers with a search space of  10^77.
A-n54-k7  has 1 depots,  7 vehicles and  53 customers with a search space of  10^79.
A-n55-k9  has 1 depots,  9 vehicles and  54 customers with a search space of  10^82.
A-n60-k9  has 1 depots,  9 vehicles and  59 customers with a search space of  10^91.
A-n61-k9  has 1 depots,  9 vehicles and  60 customers with a search space of  10^93.
A-n62-k8  has 1 depots,  8 vehicles and  61 customers with a search space of  10^94.
A-n63-k9  has 1 depots,  9 vehicles and  62 customers with a search space of  10^97.
A-n63-k10 has 1 depots, 10 vehicles and  62 customers with a search space of  10^98.
A-n64-k9  has 1 depots,  9 vehicles and  63 customers with a search space of  10^99.
A-n65-k9  has 1 depots,  9 vehicles and  64 customers with a search space of 10^101.
A-n69-k9  has 1 depots,  9 vehicles and  68 customers with a search space of 10^108.
A-n80-k10 has 1 depots, 10 vehicles and  79 customers with a search space of 10^130.
F-n45-k4  has 1 depots,  4 vehicles and  44 customers with a search space of  10^60.
F-n72-k4  has 1 depots,  4 vehicles and  71 customers with a search space of 10^108.
F-n135-k7 has 1 depots,  7 vehicles and 134 customers with a search space of 10^240.

CVRPTW 实例(有时间窗):

belgium-tw-d2-n50-k10    has  2 depots, 10 vehicles and   48 customers with a search space of   10^74.
belgium-tw-d3-n100-k10   has  3 depots, 10 vehicles and   97 customers with a search space of  10^170.
belgium-tw-d5-n500-k20   has  5 depots, 20 vehicles and  495 customers with a search space of 10^1168.
belgium-tw-d8-n1000-k20  has  8 depots, 20 vehicles and  992 customers with a search space of 10^2607.
belgium-tw-d10-n2750-k55 has 10 depots, 55 vehicles and 2740 customers with a search space of 10^8380.
belgium-tw-n50-k10       has  1 depots, 10 vehicles and   49 customers with a search space of   10^74.
belgium-tw-n100-k10      has  1 depots, 10 vehicles and   99 customers with a search space of  10^170.
belgium-tw-n500-k20      has  1 depots, 20 vehicles and  499 customers with a search space of 10^1168.
belgium-tw-n1000-k20     has  1 depots, 20 vehicles and  999 customers with a search space of 10^2607.
belgium-tw-n2750-k55     has  1 depots, 55 vehicles and 2749 customers with a search space of 10^8380.

Solomon_025_C101       has 1 depots,  25 vehicles and   25 customers with a search space of   10^40.
Solomon_025_C201       has 1 depots,  25 vehicles and   25 customers with a search space of   10^40.
Solomon_025_R101       has 1 depots,  25 vehicles and   25 customers with a search space of   10^40.
Solomon_025_R201       has 1 depots,  25 vehicles and   25 customers with a search space of   10^40.
Solomon_025_RC101      has 1 depots,  25 vehicles and   25 customers with a search space of   10^40.
Solomon_025_RC201      has 1 depots,  25 vehicles and   25 customers with a search space of   10^40.
Solomon_100_C101       has 1 depots,  25 vehicles and  100 customers with a search space of  10^185.
Solomon_100_C201       has 1 depots,  25 vehicles and  100 customers with a search space of  10^185.
Solomon_100_R101       has 1 depots,  25 vehicles and  100 customers with a search space of  10^185.
Solomon_100_R201       has 1 depots,  25 vehicles and  100 customers with a search space of  10^185.
Solomon_100_RC101      has 1 depots,  25 vehicles and  100 customers with a search space of  10^185.
Solomon_100_RC201      has 1 depots,  25 vehicles and  100 customers with a search space of  10^185.
Homberger_0200_C1_2_1  has 1 depots,  50 vehicles and  200 customers with a search space of  10^429.
Homberger_0200_C2_2_1  has 1 depots,  50 vehicles and  200 customers with a search space of  10^429.
Homberger_0200_R1_2_1  has 1 depots,  50 vehicles and  200 customers with a search space of  10^429.
Homberger_0200_R2_2_1  has 1 depots,  50 vehicles and  200 customers with a search space of  10^429.
Homberger_0200_RC1_2_1 has 1 depots,  50 vehicles and  200 customers with a search space of  10^429.
Homberger_0200_RC2_2_1 has 1 depots,  50 vehicles and  200 customers with a search space of  10^429.
Homberger_0400_C1_4_1  has 1 depots, 100 vehicles and  400 customers with a search space of  10^978.
Homberger_0400_C2_4_1  has 1 depots, 100 vehicles and  400 customers with a search space of  10^978.
Homberger_0400_R1_4_1  has 1 depots, 100 vehicles and  400 customers with a search space of  10^978.
Homberger_0400_R2_4_1  has 1 depots, 100 vehicles and  400 customers with a search space of  10^978.
Homberger_0400_RC1_4_1 has 1 depots, 100 vehicles and  400 customers with a search space of  10^978.
Homberger_0400_RC2_4_1 has 1 depots, 100 vehicles and  400 customers with a search space of  10^978.
Homberger_0600_C1_6_1  has 1 depots, 150 vehicles and  600 customers with a search space of 10^1571.
Homberger_0600_C2_6_1  has 1 depots, 150 vehicles and  600 customers with a search space of 10^1571.
Homberger_0600_R1_6_1  has 1 depots, 150 vehicles and  600 customers with a search space of 10^1571.
Homberger_0600_R2_6_1  has 1 depots, 150 vehicles and  600 customers with a search space of 10^1571.
Homberger_0600_RC1_6_1 has 1 depots, 150 vehicles and  600 customers with a search space of 10^1571.
Homberger_0600_RC2_6_1 has 1 depots, 150 vehicles and  600 customers with a search space of 10^1571.
Homberger_0800_C1_8_1  has 1 depots, 200 vehicles and  800 customers with a search space of 10^2195.
Homberger_0800_C2_8_1  has 1 depots, 200 vehicles and  800 customers with a search space of 10^2195.
Homberger_0800_R1_8_1  has 1 depots, 200 vehicles and  800 customers with a search space of 10^2195.
Homberger_0800_R2_8_1  has 1 depots, 200 vehicles and  800 customers with a search space of 10^2195.
Homberger_0800_RC1_8_1 has 1 depots, 200 vehicles and  800 customers with a search space of 10^2195.
Homberger_0800_RC2_8_1 has 1 depots, 200 vehicles and  800 customers with a search space of 10^2195.
Homberger_1000_C110_1  has 1 depots, 250 vehicles and 1000 customers with a search space of 10^2840.
Homberger_1000_C210_1  has 1 depots, 250 vehicles and 1000 customers with a search space of 10^2840.
Homberger_1000_R110_1  has 1 depots, 250 vehicles and 1000 customers with a search space of 10^2840.
Homberger_1000_R210_1  has 1 depots, 250 vehicles and 1000 customers with a search space of 10^2840.
Homberger_1000_RC110_1 has 1 depots, 250 vehicles and 1000 customers with a search space of 10^2840.
Homberger_1000_RC210_1 has 1 depots, 250 vehicles and 1000 customers with a search space of 10^2840.

3.13.1. Vehicle 路由的域模型

vehicleRoutingClassDiagram

带有时间窗域模型的载体路由大量使用影子变量功能。这样,它可以更加自然地表达其约束,因为 arrivalTimedepartureTime 等属性在域模型上直接可用。

road Distances Instead of Air Distances

在现实世界中,vehicles 无法直接跟随位置到位置:他们必须使用路线和高路。从业务角度来看,这很重要:

vehicleRoutingDistanceType

对于优化算法,这无关紧要,只要可以查找两个点之间的距离(最好是预先计算的)。路线成本甚至不需要距离。它还可以是出差时间、控制成本或其加权的功能。有多种技术可用于预先计算路线成本,如 GraphHopper (可能,离线 Java 引擎)、Open MapQuest (web service)和 Google Maps Client API (web 服务)。

integrationWithRealMaps

另外,还有多种技术可供开发人员呈现,如 LeafletGoogle Maps

vehicleRoutingLeafletAndGoogleMaps

甚至可以使用 GraphHopper 或 Google Map Directions 呈现实际的路线图路由,但由于路由在高方面重叠,因此查看独立顺序可能会变得困难:

vehicleRoutingGoogleMapsDirections

请特别注意两点之间的路线成本使用与 OptaPlanner 中使用的优化标准相同的优化标准。例如,G GraphHopper 默认返回最快的路由,而不是最短的路由。不要使用最快的 GPS 路由的 km (或 miles)来优化 OptaPlanner 中的最短往返:这会带来子优化的解决方案,如下所示:

roadDistanceTriangleInequality

最受欢迎的是,大多数用户都不希望最短的路由:他们希望获得最快的路由。他们更喜欢比普通路线的高路。他们更喜欢使用 dirt 路线的正常路。在现实世界中,最快速和最短的路由很少相同。

3.14. 项目作业调度

以时间和执行模式调度所有作业,以最小化项目延迟。每个作业都是一个项目的一部分。某个作业可以以不同的方式执行:每个执行模式都是一个执行模式,这意味着不同的持续时间,但也有不同的资源使用量。这是灵活的 作业跃点调度的一种形式

projectJobSchedulingUseCase

硬限制:

  • 作业优先级 :作业只能在所有 preesor 作业完成后启动。
  • 资源容量:不使用比可用的资源多。

    • 资源是本地(同一项目中作业之间共享)或全局资源(在所有作业间共享)
    • 资源是可续订的(每天可用的容量)或不可续订(所有天都提供容量)

中型限制:

  • 项目总延迟:最小化每个项目的持续时间(makespan)。

软限制:

  • 总计 makespan:最小化整个多项目调度的持续时间。

这个问题由 MISTA 2013 挑战 定义。

问题大小

Schedule A-1  has  2 projects,  24 jobs,   64 execution modes,  7 resources and  150 resource requirements.
Schedule A-2  has  2 projects,  44 jobs,  124 execution modes,  7 resources and  420 resource requirements.
Schedule A-3  has  2 projects,  64 jobs,  184 execution modes,  7 resources and  630 resource requirements.
Schedule A-4  has  5 projects,  60 jobs,  160 execution modes, 16 resources and  390 resource requirements.
Schedule A-5  has  5 projects, 110 jobs,  310 execution modes, 16 resources and  900 resource requirements.
Schedule A-6  has  5 projects, 160 jobs,  460 execution modes, 16 resources and 1440 resource requirements.
Schedule A-7  has 10 projects, 120 jobs,  320 execution modes, 22 resources and  900 resource requirements.
Schedule A-8  has 10 projects, 220 jobs,  620 execution modes, 22 resources and 1860 resource requirements.
Schedule A-9  has 10 projects, 320 jobs,  920 execution modes, 31 resources and 2880 resource requirements.
Schedule A-10 has 10 projects, 320 jobs,  920 execution modes, 31 resources and 2970 resource requirements.
Schedule B-1  has 10 projects, 120 jobs,  320 execution modes, 31 resources and  900 resource requirements.
Schedule B-2  has 10 projects, 220 jobs,  620 execution modes, 22 resources and 1740 resource requirements.
Schedule B-3  has 10 projects, 320 jobs,  920 execution modes, 31 resources and 3060 resource requirements.
Schedule B-4  has 15 projects, 180 jobs,  480 execution modes, 46 resources and 1530 resource requirements.
Schedule B-5  has 15 projects, 330 jobs,  930 execution modes, 46 resources and 2760 resource requirements.
Schedule B-6  has 15 projects, 480 jobs, 1380 execution modes, 46 resources and 4500 resource requirements.
Schedule B-7  has 20 projects, 240 jobs,  640 execution modes, 61 resources and 1710 resource requirements.
Schedule B-8  has 20 projects, 440 jobs, 1240 execution modes, 42 resources and 3180 resource requirements.
Schedule B-9  has 20 projects, 640 jobs, 1840 execution modes, 61 resources and 5940 resource requirements.
Schedule B-10 has 20 projects, 460 jobs, 1300 execution modes, 42 resources and 4260 resource requirements.

3.15. 任务分配

将每个任务分配到员工队列中的位置。每个任务都有一个持续时间,受员工的关联级别与任务的客户影响。

硬限制:

  • 技能:每个任务都需要一个或多个技能。员工必须具有所有这些技能。

软级别 0 约束:

  • 关键任务:首先完成关键任务,比主要和次要任务快。

软级别 1 约束:

  • 最小化 makespan:减少完成所有任务的时间。

    • 首先,首先从工作员工的最长时间开始,然后是员工的最长最长时间,以便创建公平和负载平衡。

软级别 2 约束:

  • 主要任务:尽快完成主要任务,比次要任务快。

软级别 3 约束:

  • 小任务:尽快完成次要任务。

图 3.9. 价值定位

taskAssigningValueProposition

问题大小

24tasks-8employees   has  24 tasks, 6 skills,  8 employees,   4 task types and  4 customers with a search space of   10^30.
50tasks-5employees   has  50 tasks, 5 skills,  5 employees,  10 task types and 10 customers with a search space of   10^69.
100tasks-5employees  has 100 tasks, 5 skills,  5 employees,  20 task types and 15 customers with a search space of  10^164.
500tasks-20employees has 500 tasks, 6 skills, 20 employees, 100 task types and 60 customers with a search space of 10^1168.

图 3.10. 域模型

taskAssigningClassDiagram

3.16. 考试时间稳定(ITC 2007 跟踪 1 - 检查)

将每个考试计划在一个时段和房间。多个考试在同一期间内共享相同的房间。

examinationTimetablingUseCase

硬限制:

  • 冲突:两个共享学员的考试不能在同一期间内发生。
  • 房间容量:房间容量必须随时可用。
  • 周期持续时间:其所有考试的期限必须有效。
  • 相关硬约束(每个数据集指定):

    • coincidence:两个指定的考试必须使用相同的期间(但可能还有其他房间)。
    • 排除:两个指定的考试不能使用相同的期间。
    • 之后:在另一指定考试期后,必须进行指定的考试。
  • 与房间相关的硬约束(每个数据集指定):

    • 排他:指定一个指定考试不必与任何其他考试共享其房间。

软限制(每个限制具有重要影响):

  • 同一学员不应连续两个考试。
  • 同一名学员不应在同一天拥有两个考试。
  • 布置:两门共享学员的考试是以下几个时期。
  • 混合持续时间:共享房间的两个考试不应具有不同的持续时间。
  • 前端负载:在计划前应提前安排大的考试。
  • 周期损失(每个数据集指定):一些句点在使用时会有损失。
  • 房间罚款(每个数据集指定):一些空间在使用时会有损失。

它使用大型测试数据集。

问题由 2007 年国际时间稳定注释 1 进行定义。Geoffrey De Smet 通过 OptaPlanner 的早期版本完成 4 个竞争。自那时起,我们做出了很多改进。

问题大小

exam_comp_set1 has  7883 students,  607 exams, 54 periods,  7 rooms,  12 period constraints and  0 room constraints with a search space of 10^1564.
exam_comp_set2 has 12484 students,  870 exams, 40 periods, 49 rooms,  12 period constraints and  2 room constraints with a search space of 10^2864.
exam_comp_set3 has 16365 students,  934 exams, 36 periods, 48 rooms, 168 period constraints and 15 room constraints with a search space of 10^3023.
exam_comp_set4 has  4421 students,  273 exams, 21 periods,  1 rooms,  40 period constraints and  0 room constraints with a search space of  10^360.
exam_comp_set5 has  8719 students, 1018 exams, 42 periods,  3 rooms,  27 period constraints and  0 room constraints with a search space of 10^2138.
exam_comp_set6 has  7909 students,  242 exams, 16 periods,  8 rooms,  22 period constraints and  0 room constraints with a search space of  10^509.
exam_comp_set7 has 13795 students, 1096 exams, 80 periods, 15 rooms,  28 period constraints and  0 room constraints with a search space of 10^3374.
exam_comp_set8 has  7718 students,  598 exams, 80 periods,  8 rooms,  20 period constraints and  1 room constraints with a search space of 10^1678.

3.16.1. 考试时间组的域模型

下图显示了主要的考试域类:

图 3.11. 检查域类图

examinationDomainDiagram

请注意,我们已将考试概念分成 考试 课程和主题课程。在解决时(这是计划实体类)的考试实例在它们的期间或房间属性发生变化时发生了变化。主题 PeriodRoom 实例在解决过程中不会改变(就像其它类一样是问题事实。)

3.17. Nurse rostering (2010 年)

对于每个变动,为进行这种转变分配一元化。

employeeShiftRosteringUseCase

硬限制:

  • 没有未分配的 转换(内置):需要将每个变换分配到员工。
  • 改变冲突 :员工每天只能有一个变化。

软限制:

  • 合同义务。企业经常违反这些,因此他们决定将这些定义为软限制,而不是硬限制。

    • 最小和最大分配量 :每个员工都需要与 x 相比变化的工作多,且少于 y 个变化(取决于其合同)。
    • 最小和最大连续工作日 :每个员工都需要在 x 和 y 天之间工作(取决于其合同)。
    • 最少和连续的免费天数 :每个员工都需要在 x 到 y 天之间自由使用(取决于其合同)。
    • 最小和最大连续工作周末 :每个员工都需要在 x 到 y weekends 之间在一行(取决于其合同)之间工作。
    • 完整周末 :每个员工都需要每天每天工作,或者根本不需要工作。
    • 每周端的相同转变类型 :同一名员工的相同周末转换都必须是相同的转换类型。
    • 不需要 的模式 :一行中不需要的切换的组合,例如转换后再进行早期转换,然后转换。
  • 员工希望:

    • 申请 日期:员工希望在一个特定日期工作。
    • 每天到期请求 :员工不希望在某个特定日期工作。
    • 行动请求 :员工希望分配给一个特定变化。
    • 转放请求 :员工不希望被分配给特定变化。
  • 备选技能 :分配给技能的员工应精通该变化所需的各种技能。

问题由 2010 年国际子公司定义

图 3.12. 价值定位

employeeRosteringValueProposition

问题大小

有三个数据集类型:

  • print: 必须以秒为单位解决。
  • Medium:必须在数分钟内解决。
  • 长:必须在几小时内解决。
toy1          has 1 skills, 3 shiftTypes, 2 patterns, 1 contracts,  6 employees,  7 shiftDates,  35 shiftAssignments and   0 requests with a search space of   10^27.
toy2          has 1 skills, 3 shiftTypes, 3 patterns, 2 contracts, 20 employees, 28 shiftDates, 180 shiftAssignments and 140 requests with a search space of  10^234.

sprint01      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint02      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint03      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint04      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint05      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint06      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint07      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint08      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint09      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint10      has 1 skills, 4 shiftTypes, 3 patterns, 4 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_hint01 has 1 skills, 4 shiftTypes, 8 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_hint02 has 1 skills, 4 shiftTypes, 0 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_hint03 has 1 skills, 4 shiftTypes, 8 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_late01 has 1 skills, 4 shiftTypes, 8 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_late02 has 1 skills, 3 shiftTypes, 4 patterns, 3 contracts, 10 employees, 28 shiftDates, 144 shiftAssignments and 139 requests with a search space of  10^144.
sprint_late03 has 1 skills, 4 shiftTypes, 8 patterns, 3 contracts, 10 employees, 28 shiftDates, 160 shiftAssignments and 150 requests with a search space of  10^160.
sprint_late04 has 1 skills, 4 shiftTypes, 8 patterns, 3 contracts, 10 employees, 28 shiftDates, 160 shiftAssignments and 150 requests with a search space of  10^160.
sprint_late05 has 1 skills, 4 shiftTypes, 8 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_late06 has 1 skills, 4 shiftTypes, 0 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_late07 has 1 skills, 4 shiftTypes, 0 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.
sprint_late08 has 1 skills, 4 shiftTypes, 0 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and   0 requests with a search space of  10^152.
sprint_late09 has 1 skills, 4 shiftTypes, 0 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and   0 requests with a search space of  10^152.
sprint_late10 has 1 skills, 4 shiftTypes, 0 patterns, 3 contracts, 10 employees, 28 shiftDates, 152 shiftAssignments and 150 requests with a search space of  10^152.

medium01      has 1 skills, 4 shiftTypes, 0 patterns, 4 contracts, 31 employees, 28 shiftDates, 608 shiftAssignments and 403 requests with a search space of  10^906.
medium02      has 1 skills, 4 shiftTypes, 0 patterns, 4 contracts, 31 employees, 28 shiftDates, 608 shiftAssignments and 403 requests with a search space of  10^906.
medium03      has 1 skills, 4 shiftTypes, 0 patterns, 4 contracts, 31 employees, 28 shiftDates, 608 shiftAssignments and 403 requests with a search space of  10^906.
medium04      has 1 skills, 4 shiftTypes, 0 patterns, 4 contracts, 31 employees, 28 shiftDates, 608 shiftAssignments and 403 requests with a search space of  10^906.
medium05      has 1 skills, 4 shiftTypes, 0 patterns, 4 contracts, 31 employees, 28 shiftDates, 608 shiftAssignments and 403 requests with a search space of  10^906.
medium_hint01 has 1 skills, 4 shiftTypes, 7 patterns, 4 contracts, 30 employees, 28 shiftDates, 428 shiftAssignments and 390 requests with a search space of  10^632.
medium_hint02 has 1 skills, 4 shiftTypes, 7 patterns, 3 contracts, 30 employees, 28 shiftDates, 428 shiftAssignments and 390 requests with a search space of  10^632.
medium_hint03 has 1 skills, 4 shiftTypes, 7 patterns, 4 contracts, 30 employees, 28 shiftDates, 428 shiftAssignments and 390 requests with a search space of  10^632.
medium_late01 has 1 skills, 4 shiftTypes, 7 patterns, 4 contracts, 30 employees, 28 shiftDates, 424 shiftAssignments and 390 requests with a search space of  10^626.
medium_late02 has 1 skills, 4 shiftTypes, 7 patterns, 3 contracts, 30 employees, 28 shiftDates, 428 shiftAssignments and 390 requests with a search space of  10^632.
medium_late03 has 1 skills, 4 shiftTypes, 0 patterns, 4 contracts, 30 employees, 28 shiftDates, 428 shiftAssignments and 390 requests with a search space of  10^632.
medium_late04 has 1 skills, 4 shiftTypes, 7 patterns, 3 contracts, 30 employees, 28 shiftDates, 416 shiftAssignments and 390 requests with a search space of  10^614.
medium_late05 has 2 skills, 5 shiftTypes, 7 patterns, 4 contracts, 30 employees, 28 shiftDates, 452 shiftAssignments and 390 requests with a search space of  10^667.

long01        has 2 skills, 5 shiftTypes, 3 patterns, 3 contracts, 49 employees, 28 shiftDates, 740 shiftAssignments and 735 requests with a search space of 10^1250.
long02        has 2 skills, 5 shiftTypes, 3 patterns, 3 contracts, 49 employees, 28 shiftDates, 740 shiftAssignments and 735 requests with a search space of 10^1250.
long03        has 2 skills, 5 shiftTypes, 3 patterns, 3 contracts, 49 employees, 28 shiftDates, 740 shiftAssignments and 735 requests with a search space of 10^1250.
long04        has 2 skills, 5 shiftTypes, 3 patterns, 3 contracts, 49 employees, 28 shiftDates, 740 shiftAssignments and 735 requests with a search space of 10^1250.
long05        has 2 skills, 5 shiftTypes, 3 patterns, 3 contracts, 49 employees, 28 shiftDates, 740 shiftAssignments and 735 requests with a search space of 10^1250.
long_hint01   has 2 skills, 5 shiftTypes, 9 patterns, 3 contracts, 50 employees, 28 shiftDates, 740 shiftAssignments and   0 requests with a search space of 10^1257.
long_hint02   has 2 skills, 5 shiftTypes, 7 patterns, 3 contracts, 50 employees, 28 shiftDates, 740 shiftAssignments and   0 requests with a search space of 10^1257.
long_hint03   has 2 skills, 5 shiftTypes, 7 patterns, 3 contracts, 50 employees, 28 shiftDates, 740 shiftAssignments and   0 requests with a search space of 10^1257.
long_late01   has 2 skills, 5 shiftTypes, 9 patterns, 3 contracts, 50 employees, 28 shiftDates, 752 shiftAssignments and   0 requests with a search space of 10^1277.
long_late02   has 2 skills, 5 shiftTypes, 9 patterns, 4 contracts, 50 employees, 28 shiftDates, 752 shiftAssignments and   0 requests with a search space of 10^1277.
long_late03   has 2 skills, 5 shiftTypes, 9 patterns, 3 contracts, 50 employees, 28 shiftDates, 752 shiftAssignments and   0 requests with a search space of 10^1277.
long_late04   has 2 skills, 5 shiftTypes, 9 patterns, 4 contracts, 50 employees, 28 shiftDates, 752 shiftAssignments and   0 requests with a search space of 10^1277.
long_late05   has 2 skills, 5 shiftTypes, 9 patterns, 3 contracts, 50 employees, 28 shiftDates, 740 shiftAssignments and   0 requests with a search space of 10^1257.

图 3.13. 域模型

nurseRosteringClassDiagram

3.18. 旅行排放问题(TTP)

计划在 n 个团队数量之间匹配。

travelingTournamentUseCase

硬限制:

  • 每个团队对其他团队都会执行两次:家家后,一旦离开一次。
  • 每个团队每次都有一次匹配。
  • 团队必须连续只有三个连续的主页或连续三个匹配项。
  • 无重复者:没有两个连续地匹配这两个两个不同团队。

软限制:

  • 最小化所有团队所旅行的总距离。

Michael Trick 的网站(包含全球记录) 上定义了问题。

问题大小

1-nl04     has  6 days,  4 teams and   12 matches with a search space of    10^5.
1-nl06     has 10 days,  6 teams and   30 matches with a search space of   10^19.
1-nl08     has 14 days,  8 teams and   56 matches with a search space of   10^43.
1-nl10     has 18 days, 10 teams and   90 matches with a search space of   10^79.
1-nl12     has 22 days, 12 teams and  132 matches with a search space of  10^126.
1-nl14     has 26 days, 14 teams and  182 matches with a search space of  10^186.
1-nl16     has 30 days, 16 teams and  240 matches with a search space of  10^259.
2-bra24    has 46 days, 24 teams and  552 matches with a search space of  10^692.
3-nfl16    has 30 days, 16 teams and  240 matches with a search space of  10^259.
3-nfl18    has 34 days, 18 teams and  306 matches with a search space of  10^346.
3-nfl20    has 38 days, 20 teams and  380 matches with a search space of  10^447.
3-nfl22    has 42 days, 22 teams and  462 matches with a search space of  10^562.
3-nfl24    has 46 days, 24 teams and  552 matches with a search space of  10^692.
3-nfl26    has 50 days, 26 teams and  650 matches with a search space of  10^838.
3-nfl28    has 54 days, 28 teams and  756 matches with a search space of  10^999.
3-nfl30    has 58 days, 30 teams and  870 matches with a search space of 10^1175.
3-nfl32    has 62 days, 32 teams and  992 matches with a search space of 10^1367.
4-super04  has  6 days,  4 teams and   12 matches with a search space of    10^5.
4-super06  has 10 days,  6 teams and   30 matches with a search space of   10^19.
4-super08  has 14 days,  8 teams and   56 matches with a search space of   10^43.
4-super10  has 18 days, 10 teams and   90 matches with a search space of   10^79.
4-super12  has 22 days, 12 teams and  132 matches with a search space of  10^126.
4-super14  has 26 days, 14 teams and  182 matches with a search space of  10^186.
5-galaxy04 has  6 days,  4 teams and   12 matches with a search space of    10^5.
5-galaxy06 has 10 days,  6 teams and   30 matches with a search space of   10^19.
5-galaxy08 has 14 days,  8 teams and   56 matches with a search space of   10^43.
5-galaxy10 has 18 days, 10 teams and   90 matches with a search space of   10^79.
5-galaxy12 has 22 days, 12 teams and  132 matches with a search space of  10^126.
5-galaxy14 has 26 days, 14 teams and  182 matches with a search space of  10^186.
5-galaxy16 has 30 days, 16 teams and  240 matches with a search space of  10^259.
5-galaxy18 has 34 days, 18 teams and  306 matches with a search space of  10^346.
5-galaxy20 has 38 days, 20 teams and  380 matches with a search space of  10^447.
5-galaxy22 has 42 days, 22 teams and  462 matches with a search space of  10^562.
5-galaxy24 has 46 days, 24 teams and  552 matches with a search space of  10^692.
5-galaxy26 has 50 days, 26 teams and  650 matches with a search space of  10^838.
5-galaxy28 has 54 days, 28 teams and  756 matches with a search space of  10^999.
5-galaxy30 has 58 days, 30 teams and  870 matches with a search space of 10^1175.
5-galaxy32 has 62 days, 32 teams and  992 matches with a search space of 10^1367.
5-galaxy34 has 66 days, 34 teams and 1122 matches with a search space of 10^1576.
5-galaxy36 has 70 days, 36 teams and 1260 matches with a search space of 10^1801.
5-galaxy38 has 74 days, 38 teams and 1406 matches with a search space of 10^2042.
5-galaxy40 has 78 days, 40 teams and 1560 matches with a search space of 10^2301.

3.19. 更便宜的时间调度

以时间和计算机上计划所有任务,以最大程度降低功耗。电源价格因时间不同。这是 作业跃点调度的一种形式

硬限制:

  • 开始时间限制:每个任务都必须在其最早启动和最新的开始限制之间启动。
  • 最大容量:不得超过每台机器的最大资源容量。
  • 启动和关闭:每台机器必须在为其分配任务的期间内处于活跃状态。在任务间,允许闲置以避免启动和关闭成本。

中型限制:

  • 电源成本:最小化整个计划的总功耗。

    • 机器电源成本:每个活动或空闲计算机都会消耗电源,这推断了电源成本(取决于该时间的电源价格)。
    • 任务电源成本:每个任务消耗电源,从而推断出了电源成本(取决于其时间的电源价格)。
    • 机器启动和关闭成本:每次机器启动或关机时都会产生额外的成本。

软限制(在原始问题定义中添加):

  • 早开始: 首选启动一个任务,而不是在以后启动。

问题由 ICON 挑战 定义。

问题大小

sample01   has 3 resources,   2 machines, 288 periods and   25 tasks with a search space of    10^53.
sample02   has 3 resources,   2 machines, 288 periods and   50 tasks with a search space of   10^114.
sample03   has 3 resources,   2 machines, 288 periods and  100 tasks with a search space of   10^226.
sample04   has 3 resources,   5 machines, 288 periods and  100 tasks with a search space of   10^266.
sample05   has 3 resources,   2 machines, 288 periods and  250 tasks with a search space of   10^584.
sample06   has 3 resources,   5 machines, 288 periods and  250 tasks with a search space of   10^673.
sample07   has 3 resources,   2 machines, 288 periods and 1000 tasks with a search space of  10^2388.
sample08   has 3 resources,   5 machines, 288 periods and 1000 tasks with a search space of  10^2748.
sample09   has 4 resources,  20 machines, 288 periods and 2000 tasks with a search space of  10^6668.
instance00 has 1 resources,  10 machines, 288 periods and  200 tasks with a search space of   10^595.
instance01 has 1 resources,  10 machines, 288 periods and  200 tasks with a search space of   10^599.
instance02 has 1 resources,  10 machines, 288 periods and  200 tasks with a search space of   10^599.
instance03 has 1 resources,  10 machines, 288 periods and  200 tasks with a search space of   10^591.
instance04 has 1 resources,  10 machines, 288 periods and  200 tasks with a search space of   10^590.
instance05 has 2 resources,  25 machines, 288 periods and  200 tasks with a search space of   10^667.
instance06 has 2 resources,  25 machines, 288 periods and  200 tasks with a search space of   10^660.
instance07 has 2 resources,  25 machines, 288 periods and  200 tasks with a search space of   10^662.
instance08 has 2 resources,  25 machines, 288 periods and  200 tasks with a search space of   10^651.
instance09 has 2 resources,  25 machines, 288 periods and  200 tasks with a search space of   10^659.
instance10 has 2 resources,  20 machines, 288 periods and  500 tasks with a search space of  10^1657.
instance11 has 2 resources,  20 machines, 288 periods and  500 tasks with a search space of  10^1644.
instance12 has 2 resources,  20 machines, 288 periods and  500 tasks with a search space of  10^1637.
instance13 has 2 resources,  20 machines, 288 periods and  500 tasks with a search space of  10^1659.
instance14 has 2 resources,  20 machines, 288 periods and  500 tasks with a search space of  10^1643.
instance15 has 3 resources,  40 machines, 288 periods and  500 tasks with a search space of  10^1782.
instance16 has 3 resources,  40 machines, 288 periods and  500 tasks with a search space of  10^1778.
instance17 has 3 resources,  40 machines, 288 periods and  500 tasks with a search space of  10^1764.
instance18 has 3 resources,  40 machines, 288 periods and  500 tasks with a search space of  10^1769.
instance19 has 3 resources,  40 machines, 288 periods and  500 tasks with a search space of  10^1778.
instance20 has 3 resources,  50 machines, 288 periods and 1000 tasks with a search space of  10^3689.
instance21 has 3 resources,  50 machines, 288 periods and 1000 tasks with a search space of  10^3678.
instance22 has 3 resources,  50 machines, 288 periods and 1000 tasks with a search space of  10^3706.
instance23 has 3 resources,  50 machines, 288 periods and 1000 tasks with a search space of  10^3676.
instance24 has 3 resources,  50 machines, 288 periods and 1000 tasks with a search space of  10^3681.
instance25 has 3 resources,  60 machines, 288 periods and 1000 tasks with a search space of  10^3774.
instance26 has 3 resources,  60 machines, 288 periods and 1000 tasks with a search space of  10^3737.
instance27 has 3 resources,  60 machines, 288 periods and 1000 tasks with a search space of  10^3744.
instance28 has 3 resources,  60 machines, 288 periods and 1000 tasks with a search space of  10^3731.
instance29 has 3 resources,  60 machines, 288 periods and 1000 tasks with a search space of  10^3746.
instance30 has 4 resources,  70 machines, 288 periods and 2000 tasks with a search space of  10^7718.
instance31 has 4 resources,  70 machines, 288 periods and 2000 tasks with a search space of  10^7740.
instance32 has 4 resources,  70 machines, 288 periods and 2000 tasks with a search space of  10^7686.
instance33 has 4 resources,  70 machines, 288 periods and 2000 tasks with a search space of  10^7672.
instance34 has 4 resources,  70 machines, 288 periods and 2000 tasks with a search space of  10^7695.
instance35 has 4 resources,  80 machines, 288 periods and 2000 tasks with a search space of  10^7807.
instance36 has 4 resources,  80 machines, 288 periods and 2000 tasks with a search space of  10^7814.
instance37 has 4 resources,  80 machines, 288 periods and 2000 tasks with a search space of  10^7764.
instance38 has 4 resources,  80 machines, 288 periods and 2000 tasks with a search space of  10^7736.
instance39 has 4 resources,  80 machines, 288 periods and 2000 tasks with a search space of  10^7783.
instance40 has 4 resources,  90 machines, 288 periods and 4000 tasks with a search space of 10^15976.
instance41 has 4 resources,  90 machines, 288 periods and 4000 tasks with a search space of 10^15935.
instance42 has 4 resources,  90 machines, 288 periods and 4000 tasks with a search space of 10^15887.
instance43 has 4 resources,  90 machines, 288 periods and 4000 tasks with a search space of 10^15896.
instance44 has 4 resources,  90 machines, 288 periods and 4000 tasks with a search space of 10^15885.
instance45 has 4 resources, 100 machines, 288 periods and 5000 tasks with a search space of 10^20173.
instance46 has 4 resources, 100 machines, 288 periods and 5000 tasks with a search space of 10^20132.
instance47 has 4 resources, 100 machines, 288 periods and 5000 tasks with a search space of 10^20126.
instance48 has 4 resources, 100 machines, 288 periods and 5000 tasks with a search space of 10^20110.
instance49 has 4 resources, 100 machines, 288 periods and 5000 tasks with a search space of 10^20078.

3.20. 投资资产类分配(Portfolio Optimization)

决定对每个资产类进行投资的相对数量。

硬限制:

  • 风险最大值:标准开发总数不能高于标准偏差的最大值。

  • 地区最大值:每个地区具有最大数量。
  • 扇区最大值:每个扇区具有最大数量。

软限制:

  • 最大化预期收益。

问题大小

de_smet_1 has 1 regions, 3 sectors and 11 asset classes with a search space of 10^4.
irrinki_1 has 2 regions, 3 sectors and 6 asset classes with a search space of 10^3.

大型数据集尚未创建或测试,但不应造成问题。良好的数据源是 此资产修正网站

3.21. 会议调度

为每个会议分配上机时间和房间。Timeslots 可能会重叠。从使用 libreoffice 或 Excel 编辑的 *.xlsx 文件中读取和写入。

硬限制:

  • talk of timeslot: 一个讨论的类型必须与 timeslot 的讨论类型匹配。
  • 房间不可用:在对话期间,必须有机房可用。
  • 房间冲突:两个对话在重叠期间无法使用同一空间。
  • speaker unavailable timeslots:每个对话的发言人都必须在对话期间可用。
  • speaker 冲突:两个对话在重叠时无法共享发言人。
  • 通用目的次数和房间标签:

    • speaker required timeslot tag:如果发言人具有所需的 timeslot 标签,则必须将其所有或她的对话分配给具有该标签的时间。
    • speaker prohibited timeslot 标签:如果发言人有禁止性差标签,则所有其或她的对话都不能分配给具有该标签的时间。
    • 对话所需的 timeslot 标签:如果对话具有所需的 timeslot 标签,则必须将其分配给具有该标签的时间。
    • prohibited timeslot 标签:如果通信有禁止性标签,则无法将其分配给具有该标签的 timeslot。
    • speaker required room tag:如果发言人具有所需的房间标签,则必须将所有或她的对话分配到具有该标签的房间。
    • speaker prohibited room标签:如果发言人有禁止的房间标签,则所有或她都无法分配给具有该标签的房间。
    • 对话所需的房间标签:如果对话具有所需的房间标签,则必须将其分配到具有该标签的房间。
    • Insecure room tag:如果对话有禁止的房间标签,则无法将其分配给具有该标签的房间。
  • talk mutually-exclusive-talks tag: Talks, share a such a tag not be scheduled in overlapping timests.
  • 对话前提条件:必须在所有先决条件对话后调度对话。

软限制:

  • 主题跟踪冲突:减少在重叠期间共享主题标签的对话数量。
  • 扇区冲突:减少在重叠期间共享相同扇区标签的讨论数量。
  • 内容受众级别流程违反情况:对于每一个内容标签,请计划在高级对话之前进行介绍。
  • 受众级别多元化:对于每倍,将讨论数量最大化,让不同受众水平最大化。
  • 语言多样性:对于每家来说,使用不同语言最大程度提高对话的数量。
  • 通用目的次数和房间标签:

    • speaker preferredlot 标签:如果发言人有首选 timeslot 标签,则所有他或她的所有对话都应分配给具有该标签的时间。
    • speaker undesired timeslot 标签:如果 speaker 具有不需要的 timeslot 标签,则其任何或她的任何对话都不应分配给具有该标签的时间。
    • 发生首选 timeslot 标签:如果通信具有首选差差标签,则应该将其分配给具有该标签的时间。
    • 排除不需要的 timeslot 标签:如果通信具有不需要的 timeslot 标签,则不应将其分配给具有该标签的 timeslot。
    • speaker preferred room tag:如果发言人有首选空间标签,则所有他的所有对话都应分配给具有该标签的房间。
    • speaker undesired room tag:如果发言人有不需要的房间标签,则不应向具有该标签的房间分配任何人。
    • 对话首选房间标签:如果对话具有首选房间标签,则应将其分配到具有该标签的房间。
    • 讨论不需要的房间标签:如果对话具有不需要的房间标签,则不应将其分配给具有该标签的房间。
  • 同一天讨论:所有共享主题标签或内容标签都应以最小天数(通常在同一天)内调度。

图 3.14. 价值定位

conferenceSchedulingValueProposition

问题大小

18talks-6timeslots-5rooms    has  18 talks,  6 timeslots and  5 rooms with a search space of  10^26.
36talks-12timeslots-5rooms   has  36 talks, 12 timeslots and  5 rooms with a search space of  10^64.
72talks-12timeslots-10rooms  has  72 talks, 12 timeslots and 10 rooms with a search space of 10^149.
108talks-18timeslots-10rooms has 108 talks, 18 timeslots and 10 rooms with a search space of 10^243.
216talks-18timeslots-20rooms has 216 talks, 18 timeslots and 20 rooms with a search space of 10^552.

3.22. rock Tur

从 show to show 驱动 rock 银行总线,但时间表仅在可用天显示。

硬限制:

  • 计划每个所需显示。
  • 计划尽可能多显示。

中型限制:

  • 最大化收入机会。
  • 尽量缩短驱动时间。
  • 比以后更早访问.

软限制:

  • 避免长时间驱动时间。

问题大小

47shows has 47 shows with a search space of 10^59.

3.23. flight crew 调度

为试验和上机分配交通。

硬限制:

  • 所需技能:每个员工都有所需的技能。例如,flight AB0001 需要 2 个试验和 3 个机票。
  • 交通冲突:每个员工可以同时参加一个交通
  • 在两个交易之间传输:在两个交易之间,员工必须能够从 arrival airport 传达给下机场所。例如,Amsterdam 中的 Brussels 到达 上午 10:00,地址为 15:00。
  • 员工不可用:员工必须在交易之日可用。例如,An 代表上 1-Feb。

软限制:

  • 第一个分配从家中分离
  • 到达 home 的最后分配
  • 每个员工的负载均衡 flight 持续时间

问题大小

175flights-7days-Europe  has 2 skills, 50 airports, 150 employees, 175 flights and  875 flight assignments with a search space of  10^1904.
700flights-28days-Europe has 2 skills, 50 airports, 150 employees, 700 flights and 3500 flight assignments with a search space of  10^7616.
875flights-7days-Europe  has 2 skills, 50 airports, 750 employees, 875 flights and 4375 flight assignments with a search space of 10^12578.
175flights-7days-US      has 2 skills, 48 airports, 150 employees, 175 flights and  875 flight assignments with a search space of  10^1904.

第 4 章 下载红帽构建的 OptaPlanner 示例

您可以下载 Red Hat build of OptaPlanner 示例,作为红帽客户门户网站中提供的 Red Hat Process Automation Manager 附加组件软件包的一部分。

流程

  1. 导航到红帽客户门户网站中的 Software Downloads 页面(需要登录),然后从下拉菜单中选择产品和版本:

    • 产品:流程自动化管理器
    • Version: 7.12
  2. 下载 Red Hat Process Automation Manager 7.12 Add Ons
  3. 提取 rhpam-7.12.0-add-ons.zip 文件。提取 的附加组件文件夹包含 rhpam-7.12.0-planner-engine.zip 文件。
  4. 提取 rhpam-7.12.0-planner-engine.zip 文件。

结果

提取的 rhpam-7.12.0-planner-engine 目录包含以下子目录下的示例源代码:

  • examples/sources/src/main/java/org/optaplanner/examples
  • examples/sources/src/main/resources/org/optaplanner/examples

4.1. 运行 OptaPlanner 示例

红帽构建的 OptaPlanner 包括一些演示各种规划用例的示例。下载并使用示例来探索不同类型的规划解决方案。

先决条件

流程

  1. 要运行示例,在 rhpam-7.12.0-planner-engine/examples 目录中输入以下命令之一:

    Linux 或 Mac:

    $ ./runExamples.sh

    Windows:

    $ runExamples.bat

    OptaPlanner Examples 窗口将打开。

  2. 选择一个运行该示例的示例。
注意

红帽构建的 OptaPlanner 没有 GUI 依赖项。它还在服务器或移动 JVM 上运行,就像它在桌面上一样。

4.2. 在 IDE (IntelliJ、Eclipse 或 Netbeans)中运行红帽构建的 OptaPlanner 示例。

如果您使用集成的开发环境(IDE),如 IntelliJ、Eclipse 或 Netbeans,您可以在开发环境中运行下载的 OptaPlanner 示例。

先决条件

流程

  1. 以新项目形式打开 OptaPlanner 示例:

    1. 对于 IntelliJ 或 Netbeans,打开 example/sources/pom.xml 作为新项目。Maven 集成指导您完成其余安装。跳过此过程中的其余步骤。
    2. 对于 Eclipse,为 /examples/binaries 目录打开一个新项目,它位于 rhpam-7.12.0-planner-engine 目录下。
  2. 将所有位于 二进制 目录的 JAR 文件添加到 classpath 中,但 examples/binaries/optaplanner-examples-7.59.0.Final-redhat-00006.jar 文件除外。
  3. 添加 Java 源目录 src/main/java 和 Java 资源目录 src/main/resources,位于 rhpam-7.12.0-planner-engine/examples/sources/ 目录下。
  4. 创建运行配置:

    • 主类: org.optaplanner.examples.app.OptaPlannerExamplesApp
    • VM 参数(可选): -Xmx512M -server -Dorg.optaplanner.examples.dataDir=examples/sources/data
    • 工作目录: example/sources
  5. 运行运行配置。

第 5 章 OptaPlanner 在 Business Central 入门:员工名列示例

您可以在 Business Central 中 构建和部署员工 示例项目。该项目演示了如何创建解决转型计划问题所需要的每个 Business Central 资产,并使用红帽构建 OptaPlanner 来查找最佳解决方案。

您可以在 Business Central 中部署预配置的 员工 -rostering 项目。或者,您可以使用 Business Central 自行创建项目。

注意

Business Central 中的 员工 示例项目不包括数据集。您必须使用 REST API 调用以 XML 格式提供数据集。

5.1. 在 Business Central 中部署员工漫长示例项目

Business Central 包括多个示例项目,可用于熟悉产品及其功能。员工漫长示例项目旨在并创建来演示红帽构建 OptaPlanner 的转变激情用例。使用以下流程在 Business Central 中部署和运行员工名词示例。

先决条件

  • Red Hat Process Automation Manager 已下载并安装。有关安装选项,请参阅 规划 Red Hat Process Automation Manager 安装
  • 您已按照安装文档启动 Red Hat Process Automation Manager,并以具有 admin 权限的用户身份登录到 Business Central。

流程

  1. 在 Business Central 中,点 MenuDesignProjects
  2. 在预配置的 MySpace 空间中,单击 Try Samples
  3. 从示例项目列表中选择 employee-rostering,再单击右上角的 Ok 以导入项目。
  4. 编译资产列表后,点 Build & Deploy 来部署员工名录示例。

本文档的其余部分解释了每个项目资产及其配置。

5.2. 重新创建员工示例项目

员工示例项目是 Business Central 中提供的预配置项目。您可以在 第 5.1 节 “在 Business Central 中部署员工漫长示例项目” 中了解如何部署此项目。

您可以创建员工对示例"从零开始"。您可以使用本例中的工作流在 Business Central 中创建您自己的类似项目。

5.2.1. 设置员工漫长项目

要在 Business Central 中开始开发解决者,您必须设置该项目。

先决条件

  • Red Hat Process Automation Manager 已下载并安装。
  • 您已部署了 Business Central,并使用具有 admin 角色的用户登录。

流程

  1. MenuDesignProjectsAdd Project,在 Business Central 中创建一个新项目。
  2. Add Project 窗口中填写以下字段:

    • 名称employee-rostering
    • 描述(可选):使用 OptaPlanner 扩大问题优化。根据自身技能,分配员工进行转变。

    可选:点 Configure Advanced Options 来填充 组 IDArtifact IDVersion 信息。

    • 组 ID :员工
    • 工件 ID :员工
    • Version:1.0.0-SNAPSHOT
  3. 单击 Add,将项目添加到 Business Central 项目存储库中。

5.2.2. 问题事实和规划实体

员工规划问题中的每个域类都归类为以下之一:

  • 一个不相关的类:不被分数限制使用。从规划的角度来看,这个数据已过时。
  • 问题事实 类:分数约束使用但在规划过程中不会改变(只要问题保持不变),例如 ShiftEmployee。问题类的所有属性都是问题属性。
  • 计划实体类:在规划 过程中分数约束和更改使用,例如 ShiftAssignment。计划期间更改的属性正在 规划变量。其他属性是问题属性。

    请您提出以下问题:

  • 计划期间发生了哪些类变化?
  • 哪个类具有我想要 更改 的变量?

    该类是计划实体。

    计划实体类需要使用 @PlanningEntity 注释标注,或者在 Business Central 中定义,并在域设计程序中使用红帽构建 OptaPlanner dock。

    每个计划实体类均有一个或多个 计划变量,并且必须具有一个或多个定义属性。

    大多数用例只有一个规划实体类,每个计划实体类都只有一个计划变量。

5.2.3. 为员工漫长项目创建数据模型

使用本节创建在 Business Central 中运行员工名录示例项目所需的数据对象。

先决条件

流程

  1. 使用新项目,点项目视角中的 Data Object,或者点击 Add AssetData Object 来创建新数据对象。
  2. 将第一个数据对象 time slot 命名为,然后选择 employeerostering.employeerostering 作为 软件包

    确定

  3. Data Objects 视角中,点 +add 字段Timeslot data 对象添加字段。
  4. id 字段中,键入 endTime
  5. 单击 Type 旁边的下拉菜单,再选择 LocalDateTime
  6. Create 并继续 添加另一个字段。
  7. 使用 id startTimeType LocalDateTime 添加另一个字段。
  8. Create
  9. 单击右上角的 Save,以保存 Timeslot data 对象。
  10. 单击右上角的 x 以关闭 Data Objects 透视图,并返回到 Assets 菜单。
  11. 使用前面的步骤,创建以下数据对象及其属性:

    表 5.1. 技能

    id类型

    name

    字符串

    表 5.2. 员工

    id类型

    name

    字符串

    skills

    employeerostering.employeerostering.Skill[List]

    表 5.3. 变动

    id类型

    requiredSkill

    employeerostering.employeerostering.Skill

    timeslot

    employeerostering.employeerostering.Timeslot

    表 5.4. DayOffRequest

    id类型

    date

    LocalDate

    employee

    employeerostering.employeerostering.Employee

    表 5.5. ShiftAssignment

    id类型

    employee

    employeerostering.employeerostering.Employee

    shift

    employeerostering.employeerostering.Shift

有关创建数据对象的更多示例,请参阅开始使用决策服务

5.2.3.1. 创建员工规划实体

为了解决员工对规划问题,您必须创建一个规划实体和解决者。计划实体使用红帽构建的 OptaPlanner dock 中提供的属性在域设计人员中定义。

使用以下步骤将 ShiftAssignment 数据对象定义为员工 rostering 的规划实体。

先决条件

流程

  1. 在项目 资产 菜单中,打开 ShiftAssignment data 对象。
  2. Data Objects 视角中,点右侧的 OptaPlanner icon 来打开 OptaPlanner dock。
  3. 选择 规划实体
  4. ShiftAssignment data 对象下的字段列表中选择 employee
  5. 在 OptaPlanner dock 中,选择 planning Variable

    Value Range Id 输入字段中,键入 employeeRange。这会将 @ValueRangeProvider 注释添加到计划实体,您可以通过单击设计程序中的 Source 选项卡来查看该注释。

    计划变量的值范围通过 @ValueRangeProvider 注释来定义。@ValueRangeProvider 注释始终具有属性 id,它被 @PlanningVariable 属性 valueRangeProviderRefs 引用。

  6. 关闭 dock 并点 Save 保存数据对象。

5.2.3.2. 创建员工规划解决方案

员工 roster 问题依赖于定义的规划解决方案。计划解决方案通过使用红帽构建的 OptaPlanner dock 中提供的属性在域设计人员中定义。

先决条件

流程

  1. 使用标识符 EmployeeRoster 创建新数据对象。
  2. 创建以下字段:

    表 5.6. EmployeeRoster

    id类型

    dayOffRequestList

    employeerostering.employeerostering.DayOffRequest[List]

    shiftAssignmentList

    employeerostering.employeerostering.ShiftAssignment[List]

    shiftList

    employeerostering.employeerostering.Shift[List]

    skillList

    employeerostering.employeerostering.Skill[List]

    timeslotList

    employeerostering.employeerostering.Timeslot[List]

  3. Data Objects 视角中,点右侧的 OptaPlanner icon 来打开 OptaPlanner dock。
  4. 选择 规划解决方案
  5. 将默认的硬 软分数 保留为 解决方案分数类型。这会在 EmployeeRoster 数据对象中自动生成 分数 字段,其解决方案分数作为类型。
  6. 使用以下属性添加新字段:

    id类型

    employeeList

    employeerostering.employeerostering.Employee[List]

  7. 选择 employeeList 字段后,打开 OptaPlanner dock,然后选择 planning Value Range Provider 框。

    id 字段中,键入 employeeRange。关闭 dock。

  8. 单击右上角的 Save 以保存资产。

5.2.4. 员工划分限制

员工名列是规划问题。所有规划问题均包括必须满足的约束才能找到最佳解决方案。

Business Central 中的员工漫长示例项目包括以下硬和软限制:

硬约束
  • 每天仅分配一次变化。
  • 所有需要特定员工技能的改变都被分配了具有该特定技能的员工。
软限制
  • 所有员工都分配有变化。
  • 如果员工每天没有请求,则会将其转移重新分配给另一个员工。

使用自由格式 DRL 设计程序或使用指导规则在 Business Central 中定义硬和软限制。

5.2.4.1. DRL (Drools 规则语言)规则

DRL (Drools 规则语言)规则是您直接在 .drl 文本文件中定义的业务规则。这些 DRL 文件是最终呈现 Business Central 中所有其他规则资产的源。您可以在 Business Central 界面中创建和管理 DRL 文件,或使用 Red Hat CodeReady Studio 或其他集成开发环境(IDE)作为 Maven 或 Java 项目的一部分创建它们。DRL 文件可以包含一个或多个规则,它们至少定义规则条件(何时)和操作(然后)。Business Central 中的 DRL 设计程序提供了 Java、DRL 和 XML 的语法高亮显示。

DRL 文件由以下组件组成:

DRL 文件中的组件

package

import

function  // Optional

query  // Optional

declare   // Optional

global   // Optional

rule "rule name"
    // Attributes
    when
        // Conditions
    then
        // Actions
end

rule "rule2 name"

...

以下示例 DRL 规则决定了 loan 应用程序决策服务中的年龄限制:

loan 应用程序年龄限制的规则示例

rule "Underage"
  salience 15
  agenda-group "applicationGroup"
  when
    $application : LoanApplication()
    Applicant( age < 21 )
  then
    $application.setApproved( false );
    $application.setExplanation( "Underage" );
end

DRL 文件可包含一个或多个规则、查询和功能,并可定义您的规则和查询分配的资源声明,如导入、全局和属性。DRL 软件包必须列在 DRL 文件的顶部,并且规则通常会最后一个列出。所有其他 DRL 组件都可以遵循任何顺序。

每个规则必须在规则软件包中都有一个唯一名称。如果您在软件包中的任何 DRL 文件中使用相同的规则名称多次,则规则无法编译。始终使用双引号(规则"rule name")括起规则名称以防止可能的编译错误,特别是在规则名称中使用空格。

与 DRL 规则相关的所有数据对象都必须与 Business Central 中的 DRL 文件位于同一个项目中。默认情况下,同一软件包中的资产会导入。其他软件包中现有的资产可使用 DRL 规则导入。

5.2.4.2. 使用 DRL 设计程序定义员工的约束

您可以使用 Business Central 中的自由格式 DRL 设计程序为员工漫长示例创建约束定义。

使用这个流程创建一个 硬约束,其中没有员工被分配时间超过 10 小时的改变。

流程

  1. 在 Business Central 中,前往 MenuDesignProjects,再单击项目名称。
  2. Add AssetDRL 文件
  3. DRL file name 字段中,键入 ComplexScoreRules
  4. 选择 employeerostering.employeerostering 软件包。
  5. 单击 +Ok 以创建 DRL 文件。
  6. 在 DRL 设计器的 Model 选项卡中,将 Employee10HourShiftSpace 规则定义为 DRL 文件:

    package employeerostering.employeerostering;
    
    rule "Employee10HourShiftSpace"
        when
            $shiftAssignment : ShiftAssignment( $employee : employee != null, $shiftEndDateTime : shift.timeslot.endTime)
            ShiftAssignment( this != $shiftAssignment, $employee == employee, $shiftEndDateTime <= shift.timeslot.endTime,
                    $shiftEndDateTime.until(shift.timeslot.startTime, java.time.temporal.ChronoUnit.HOURS) <10)
        then
            scoreHolder.addHardConstraintMatch(kcontext, -1);
    end
  7. Save 保存 DRL 文件。

有关创建 DRL 文件的更多信息,请参阅使用 DRL 规则设计决策服务

5.2.5. 使用指导规则为员工划分创建规则

您可以使用 Business Central 中的指导规则设计人员创建硬和软约束。

5.2.5.1. 指导规则

指导规则是在 Business Central 中的基于 UI 的引导规则设计程序中创建的业务规则,引导您通过规则创建流程。指导规则设计程序根据所定义的规则的数据对象提供可接受的输入字段和选项。您定义的指导规则被编译到 Drools 规则语言(DRL)规则中,与所有其他规则资产一样。

与指导规则相关的所有数据对象都必须与指导规则位于同一个项目中。默认情况下,同一软件包中的资产会导入。创建必要的数据对象和指导规则后,您可以使用指导规则设计器的 Data Objects 选项卡来验证所有所需的数据对象是否都已列出或导入其他现有 的数据对象

5.2.5.2. 创建指导规则来平衡员工的改变数字

BalanceEmployeesShiftNumber 指导规则创建一个软约束,以确保按尽可能平均平衡的方式分配到员工。它通过创建一个在变换发行版时增加的分数损失来达到此目的。分数公式由规则实施,以更均衡的方式替换 Solver 分发变化。

BalanceEmployeesShiftNumber Guided Rule

流程

  1. 在 Business Central 中,前往 MenuDesignProjects,再单击项目名称。
  2. Add AssetGuided Rule
  3. 输入 BalanceEmployeesShiftNumber 作为" 指导规则名称",然后选择 employeerostering.employeerostering Package
  4. Ok 创建规则资产。
  5. WHEN 字段中的 5686 来添加 WHEN 条件。
  6. Add a condition to the rule 窗口中选择 Employee。单击 +Ok
  7. Employee 条件来修改约束,并添加变量名称 $employee
  8. 累积 中添加 WHEN 条件

    1. 在 From A umulate 条件 上方,单击添加模式,然后从下拉列表中选择 Number 作为事实类型。
    2. 将变量名称 $shiftCount 添加到 Number 条件。
    3. From Accumulate 条件下,单击添加模式,然后从下拉列表中选择 ShiftAssignment 事实类型。
    4. 将变量名称 $shiftAssignment 添加到 ShiftAssignment 类型。
    5. 再次单击 ShiftAssignment 条件,然后从 字段下拉列表中的 Add a limit,选择 employee
    6. 员工 约束旁边的下拉列表中,选择 等于
    7. 点击下拉按钮旁的 edit 图标添加变量,然后点击 Field value 窗口中的 Bound 变量
    8. 从下拉列表中选择 $employee
    9. Function 框中,类型 count ($shiftAssignment)
  9. wordpress N 字段中的 5686 来添加 wordpressN 条件。
  10. Add a new action 窗口中选择 Modify Soft Score。单击 +Ok

    1. 在框中键入以下表达式: -($shiftCount.intValue ()*$shiftCount.intValue ())
  11. 单击右上角的 Validate,以检查所有规则条件是否有效。如果规则验证失败,请解决错误消息中描述的任何问题,检查规则中的所有组件,然后再次尝试验证规则,直到规则通过为止。
  12. 单击 Save 以保存规则。

有关创建指导规则的更多信息,请参阅使用指导规则设计决策服务

5.2.5.3. 为每天不多的改变创建指导规则

OneEmployeeShiftPerDay 指导规则创建硬约束,使员工不会每天分配多个变化。在 employee rostering 示例中,这个约束使用指导规则设计程序创建。

OneEmployeeShiftPerDay 指导规则

流程

  1. 在 Business Central 中,前往 MenuDesignProjects,再单击项目名称。
  2. Add AssetGuided Rule
  3. 输入 OneEmployeeShiftPerDay 作为" 指导规则名称",然后选择 employeerostering.employeerostering Package
  4. Ok 创建规则资产。
  5. WHEN 字段中的 5686 来添加 WHEN 条件。
  6. Add a condition to the rule 窗口中选择 Free form DRL
  7. 在 free 表单 DRL 框中,输入以下条件:

    $shiftAssignment : ShiftAssignment( employee != null )
    		ShiftAssignment( this != $shiftAssignment , employee == $shiftAssignment.employee , shift.timeslot.startTime.toLocalDate() == $shiftAssignment.shift.timeslot.startTime.toLocalDate() )

    此条件指出,无法分配给已经在同一天有另一个转移的员工。

  8. wordpress N 字段中的 5686 来添加 wordpressN 条件。
  9. Add a new action 窗口中选择 Add free form DRL
  10. 在 free 表单 DRL 框中,输入以下条件:

    scoreHolder.addHardConstraintMatch(kcontext, -1);
  11. 单击右上角的 Validate,以检查所有规则条件是否有效。如果规则验证失败,请解决错误消息中描述的任何问题,检查规则中的所有组件,然后再次尝试验证规则,直到规则通过为止。
  12. 单击 Save 以保存规则。

有关创建指导规则的更多信息,请参阅使用指导规则设计决策服务

5.2.5.4. 创建指导规则来匹配转换要求

ShiftReqiredSkillsAreMet 指导规则会创建一个硬约束,确保为员工分配有正确技能集的员工。在 employee rostering 示例中,这个约束使用指导规则设计程序创建。

ShiftRequiredSkillsAreMet 包含规则

流程

  1. 在 Business Central 中,前往 MenuDesignProjects,再单击项目名称。
  2. Add AssetGuided Rule
  3. 输入 ShiftReqiredSkillsAreMet 作为 指导规则 名称,然后选择 employeerostering.employeerostering Package
  4. Ok 创建规则资产。
  5. WHEN 字段中的 5686 来添加 WHEN 条件。
  6. Add a condition to the rule 窗口中选择 ShiftAssignment。单击 +Ok
  7. 单击 ShiftAssignment 条件,然后从 Add a limits on a field 下拉列表中选择 employee。
  8. 在设计人员中,单击 员工 旁边的下拉列表,然后选择 is not null
  9. 单击 ShiftAssignment 条件,然后单击 Expression 编辑器

    1. 在设计人员中,单击 [not bound] 以打开 Expression 编辑器,并将表达式绑定到变量 $requiredSkill。点 Set
    2. 在设计过程中,在 $requiredSkill 旁边,从第一个下拉列表中选择 切换,然后从下一个下拉列表中选择 requiredSkill
  10. 单击 ShiftAssignment 条件,然后单击 Expression 编辑器

    1. 在设计人员中,[not bound] 旁边,从第一个下拉列表中选择 员工,然后从下一下拉列表中选择员工。
    2. 将下一个下拉列表保留为 Choose
    3. 在下一个下拉菜单中,请更改 以排除
    4. 点击 排除edit 图标,在 Field value 窗口中点击 New formula 按钮。
    5. 在公式框中键入 $requiredSkill
  11. wordpress N 字段中的 5686 来添加 wordpressN 条件。
  12. Add a new action 窗口中选择 Modify Hard Score。单击 +Ok
  13. 在分数操作框中键入 -1
  14. 单击右上角的 Validate,以检查所有规则条件是否有效。如果规则验证失败,请解决错误消息中描述的任何问题,检查规则中的所有组件,然后再次尝试验证规则,直到规则通过为止。
  15. 单击 Save 以保存规则。

有关创建指导规则的更多信息,请参阅使用指导规则设计决策服务

5.2.5.5. 创建指导规则来管理一天

DayOffRequest 指导规则创建一个软约束。通过这个约束,当最初分配该转换的员工不再能够工作时,可以将转换重新分配给另一个员工。在 employee rostering 示例中,这个约束使用指导规则设计程序创建。

DayOffRequest 指导规则

流程

  1. 在 Business Central 中,前往 MenuDesignProjects,再单击项目名称。
  2. Add AssetGuided Rule
  3. 输入 DayOffRequest 作为" 指导规则名称",然后选择 employeerostering.employeerostering 软件包
  4. Ok 创建规则资产。
  5. WHEN 字段中的 5686 来添加 WHEN 条件。
  6. Add a condition to the rule 窗口中选择 Free form DRL
  7. 在 free 表单 DRL 框中,输入以下条件:

    $dayOffRequest : DayOffRequest( )
    		ShiftAssignment( employee == $dayOffRequest.employee , shift.timeslot.startTime.toLocalDate() == $dayOffRequest.date )

    此条件表示,如果将转换分配给制定一天的员工,则员工可以取消分配该日期的转变。

  8. wordpress N 字段中的 5686 来添加 wordpressN 条件。
  9. Add a new action 窗口中选择 Add free form DRL
  10. 在 free 表单 DRL 框中,输入以下条件:

    scoreHolder.addSoftConstraintMatch(kcontext, -100);
  11. 单击右上角的 Validate,以检查所有规则条件是否有效。如果规则验证失败,请解决错误消息中描述的任何问题,检查规则中的所有组件,然后再次尝试验证规则,直到规则通过为止。
  12. 单击 Save 以保存规则。

有关创建指导规则的更多信息,请参阅使用指导规则设计决策服务

5.2.6. 为员工名电创建解决者配置

您可以在 Business Central 中创建和编辑 Solver 配置。Solver 配置设计程序会创建一个解析器配置,可在部署项目后运行。

先决条件

  • Red Hat Process Automation Manager 已下载并安装。
  • 您已为员工漫长示例创建并配置了所有相关资产。

流程

  1. 在 Business Central 中,点 MenuProjects,然后点击您的项目来打开它。
  2. Assets 视角中,点 Add AssetSolver configuration
  3. Create new Solver configuration 窗口中,为您的 Solver 输入名称 EmployeeRosteringSolverConfig,然后点 Ok

    这将打开 Solv er 配置设计 程序。

  4. Score Director Factory 配置部分中,定义一个包含 scoring 规则定义的 KIE 基础。员工示例项目使用 defaultKieBase

    1. 选择在 KIE 库中定义的 KIE 会话之一。员工漫长示例项目使用 defaultKieSession
  5. 单击右上角的 Validate,以检查 Score Director Factory 配置是否正确。如果验证失败,请解决错误消息中描述的任何问题,然后重试验证,直到配置通过为止。
  6. Save 保存 Solver 配置。

5.2.7. 为员工名电项目配置 Solver 终止

您可以在指定时间后将 Solver 配置为终止。默认情况下,计划引擎会提供无限时间段来解决问题实例。

员工示例项目设置为运行 30 秒。

先决条件

流程

  1. Assets 视角打开 EmployeeRosteringSolverConfig。这将打开 Solv er 配置设计 程序。
  2. Termination 部分中,单击 Add 在所选逻辑组中创建新的 termination 元素。
  3. 从下拉列表中选择所用的 终止类型。这在终止配置中作为输入字段添加。
  4. 使用时间元素旁边的箭头将时间调整为 30 秒。
  5. 单击右上角的 Validate,以检查 Score Director Factory 配置是否正确。如果验证失败,请解决错误消息中描述的任何问题,然后重试验证,直到配置通过为止。
  6. Save 保存 Solver 配置。

5.3. 使用 REST API 访问解析器

在部署或重新创建示例解析程序后,您可以使用 REST API 访问它。

您必须使用 REST API 注册 solver 实例。然后,您可以提供数据集并检索优化的解决方案。

先决条件

5.3.1. 使用 REST API 注册 Solver

您必须使用 REST API 注册 solver 实例,然后才能使用 solver。

每个解决器实例都可以一次优化一个计划问题。

流程

  1. 使用以下标头创建 HTTP 请求:

    authorization: admin:admin
    X-KIE-ContentType: xstream
    content-type: application/xml
  2. 使用以下请求注册 Solver :

    PUT
    http://localhost:8080/kie-server/services/rest/server/containers/employeerostering_1.0.0-SNAPSHOT/solvers/EmployeeRosteringSolver
    请求正文
    <solver-instance>
      <solver-config-file>employeerostering/employeerostering/EmployeeRosteringSolverConfig.solver.xml</solver-config-file>
    </solver-instance>

5.3.2. 使用 REST API 调用 Solver

在注册解决程序实例后,您可以使用 REST API 向 solver 提交数据集并检索优化的解决方案。

流程

  1. 使用以下标头创建 HTTP 请求:

    authorization: admin:admin
    X-KIE-ContentType: xstream
    content-type: application/xml
  2. 使用数据集向 Solver 提交请求,如下例所示:

    POST
    http://localhost:8080/kie-server/services/rest/server/containers/employeerostering_1.0.0-SNAPSHOT/solvers/EmployeeRosteringSolver/state/solving
    请求正文
    <employeerostering.employeerostering.EmployeeRoster>
      <employeeList>
        <employeerostering.employeerostering.Employee>
          <name>John</name>
          <skills>
            <employeerostering.employeerostering.Skill>
              <name>reading</name>
            </employeerostering.employeerostering.Skill>
          </skills>
        </employeerostering.employeerostering.Employee>
        <employeerostering.employeerostering.Employee>
          <name>Mary</name>
          <skills>
            <employeerostering.employeerostering.Skill>
              <name>writing</name>
            </employeerostering.employeerostering.Skill>
          </skills>
        </employeerostering.employeerostering.Employee>
        <employeerostering.employeerostering.Employee>
          <name>Petr</name>
          <skills>
            <employeerostering.employeerostering.Skill>
              <name>speaking</name>
            </employeerostering.employeerostering.Skill>
          </skills>
        </employeerostering.employeerostering.Employee>
      </employeeList>
      <shiftList>
        <employeerostering.employeerostering.Shift>
          <timeslot>
            <startTime>2017-01-01T00:00:00</startTime>
            <endTime>2017-01-01T01:00:00</endTime>
          </timeslot>
          <requiredSkill reference="../../../employeeList/employeerostering.employeerostering.Employee/skills/employeerostering.employeerostering.Skill"/>
        </employeerostering.employeerostering.Shift>
        <employeerostering.employeerostering.Shift>
          <timeslot reference="../../employeerostering.employeerostering.Shift/timeslot"/>
          <requiredSkill reference="../../../employeeList/employeerostering.employeerostering.Employee[3]/skills/employeerostering.employeerostering.Skill"/>
        </employeerostering.employeerostering.Shift>
        <employeerostering.employeerostering.Shift>
          <timeslot reference="../../employeerostering.employeerostering.Shift/timeslot"/>
          <requiredSkill reference="../../../employeeList/employeerostering.employeerostering.Employee[2]/skills/employeerostering.employeerostering.Skill"/>
        </employeerostering.employeerostering.Shift>
      </shiftList>
      <skillList>
        <employeerostering.employeerostering.Skill reference="../../employeeList/employeerostering.employeerostering.Employee/skills/employeerostering.employeerostering.Skill"/>
        <employeerostering.employeerostering.Skill reference="../../employeeList/employeerostering.employeerostering.Employee[3]/skills/employeerostering.employeerostering.Skill"/>
        <employeerostering.employeerostering.Skill reference="../../employeeList/employeerostering.employeerostering.Employee[2]/skills/employeerostering.employeerostering.Skill"/>
      </skillList>
      <timeslotList>
        <employeerostering.employeerostering.Timeslot reference="../../shiftList/employeerostering.employeerostering.Shift/timeslot"/>
      </timeslotList>
      <dayOffRequestList/>
      <shiftAssignmentList>
        <employeerostering.employeerostering.ShiftAssignment>
          <shift reference="../../../shiftList/employeerostering.employeerostering.Shift"/>
        </employeerostering.employeerostering.ShiftAssignment>
        <employeerostering.employeerostering.ShiftAssignment>
          <shift reference="../../../shiftList/employeerostering.employeerostering.Shift[3]"/>
        </employeerostering.employeerostering.ShiftAssignment>
        <employeerostering.employeerostering.ShiftAssignment>
          <shift reference="../../../shiftList/employeerostering.employeerostering.Shift[2]"/>
        </employeerostering.employeerostering.ShiftAssignment>
      </shiftAssignmentList>
    </employeerostering.employeerostering.EmployeeRoster>
  3. 向规划问题请求最佳解决方案:

    GET

    http://localhost:8080/kie-server/services/rest/server/containers/employeerostering_1.0.0-SNAPSHOT/solvers/EmployeeRosteringSolver/bestsolution

    响应示例

    <solver-instance>
      <container-id>employee-rostering</container-id>
      <solver-id>solver1</solver-id>
      <solver-config-file>employeerostering/employeerostering/EmployeeRosteringSolverConfig.solver.xml</solver-config-file>
      <status>NOT_SOLVING</status>
      <score scoreClass="org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore">0hard/0soft</score>
      <best-solution class="employeerostering.employeerostering.EmployeeRoster">
        <employeeList>
          <employeerostering.employeerostering.Employee>
            <name>John</name>
            <skills>
              <employeerostering.employeerostering.Skill>
                <name>reading</name>
              </employeerostering.employeerostering.Skill>
            </skills>
          </employeerostering.employeerostering.Employee>
          <employeerostering.employeerostering.Employee>
            <name>Mary</name>
            <skills>
              <employeerostering.employeerostering.Skill>
                <name>writing</name>
              </employeerostering.employeerostering.Skill>
            </skills>
          </employeerostering.employeerostering.Employee>
          <employeerostering.employeerostering.Employee>
            <name>Petr</name>
            <skills>
              <employeerostering.employeerostering.Skill>
                <name>speaking</name>
              </employeerostering.employeerostering.Skill>
            </skills>
          </employeerostering.employeerostering.Employee>
        </employeeList>
        <shiftList>
          <employeerostering.employeerostering.Shift>
            <timeslot>
              <startTime>2017-01-01T00:00:00</startTime>
              <endTime>2017-01-01T01:00:00</endTime>
            </timeslot>
            <requiredSkill reference="../../../employeeList/employeerostering.employeerostering.Employee/skills/employeerostering.employeerostering.Skill"/>
          </employeerostering.employeerostering.Shift>
          <employeerostering.employeerostering.Shift>
            <timeslot reference="../../employeerostering.employeerostering.Shift/timeslot"/>
            <requiredSkill reference="../../../employeeList/employeerostering.employeerostering.Employee[3]/skills/employeerostering.employeerostering.Skill"/>
          </employeerostering.employeerostering.Shift>
          <employeerostering.employeerostering.Shift>
            <timeslot reference="../../employeerostering.employeerostering.Shift/timeslot"/>
            <requiredSkill reference="../../../employeeList/employeerostering.employeerostering.Employee[2]/skills/employeerostering.employeerostering.Skill"/>
          </employeerostering.employeerostering.Shift>
        </shiftList>
        <skillList>
          <employeerostering.employeerostering.Skill reference="../../employeeList/employeerostering.employeerostering.Employee/skills/employeerostering.employeerostering.Skill"/>
          <employeerostering.employeerostering.Skill reference="../../employeeList/employeerostering.employeerostering.Employee[3]/skills/employeerostering.employeerostering.Skill"/>
          <employeerostering.employeerostering.Skill reference="../../employeeList/employeerostering.employeerostering.Employee[2]/skills/employeerostering.employeerostering.Skill"/>
        </skillList>
        <timeslotList>
          <employeerostering.employeerostering.Timeslot reference="../../shiftList/employeerostering.employeerostering.Shift/timeslot"/>
        </timeslotList>
        <dayOffRequestList/>
        <shiftAssignmentList/>
        <score>0hard/0soft</score>
      </best-solution>
    </solver-instance>

第 6 章 OptaPlanner 和 Quarkus 入门

您可以使用 https://code.quarkus.redhat.com 网站生成红帽构建的 OptaPlanner Quarkus Maven 项目,并自动配置要在应用程序中使用的扩展。然后您可以下载 Quarkus Maven 存储库,或将在线 Maven 存储库用于您的项目。

6.1. Apache Maven 和红帽构建的 Quarkus

Apache Maven 是 Java 应用程序开发中使用的分布式构建自动化工具,用于创建、管理和构建软件项目。Maven 使用名为 Project Object Model(POM)文件的标准配置文件来定义项目并管理构建流程。POM 文件描述了使用 XML 文件生成的项目打包和输出的模块和组件依赖项、构建顺序和目标。这可确保以正确、一致的方式构建项目。

Maven 存储库

Maven 存储库存储 Java 库、插件和其他构建构件。默认公共存储库是 Maven 2 Central Repository,但存储库可以是私有的,也可以是公司内部的存储库,以在开发团队之间共享通用的工件。也可从第三方获得存储库。

您可以将在线 Maven 存储库与 Quarkus 项目一起使用,也可以下载红帽构建的 Quarkus Maven 存储库。

Maven 插件

Maven 插件是 POM 文件的定义部分,可实现一个或多个目标。Quarkus 应用程序使用以下 Maven 插件:

  • Quarkus Maven 插件(quarkus-maven-plugin):启用 Maven 创建 Quarkus 项目,支持 uber-JAR 文件的生成,并提供开发模式。
  • Maven Surefire 插件(maven-surefire-plugin):在构建生命周期的测试阶段使用,用于在您的应用程序上执行单元测试。插件生成包含测试报告的文本和 XML 文件。

6.1.1. 为在线存储库配置 Maven settings.xml 文件

您可以通过配置用户 settings.xml 文件,在 Maven 项目中使用在线 Maven 存储库。这是推荐的方法。与共享服务器上的存储库管理器或存储库一起使用的 Maven 设置提供更好的项目控制和管理。

注意

当您修改 Maven settings.xml 文件来配置存储库时,更改适用于所有 Maven 项目。

流程

  1. 在文本编辑器或集成开发环境(IDE)中打开 Maven ~/.m2/settings.xml 文件。

    注意

    如果 ~/.m2/ 目录中没有 settings.xml 文件,请将 $MAVEN_HOME/.m2/conf/ 目录中的 settings.xml 文件复制到 ~/.m2/ 目录中。

  2. settings.xml 文件的 <profiles > 元素中添加以下行:

    <!-- Configure the Maven repository -->
    <profile>
      <id>red-hat-enterprise-maven-repository</id>
      <repositories>
        <repository>
          <id>red-hat-enterprise-maven-repository</id>
          <url>https://maven.repository.redhat.com/ga/</url>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </repository>
      </repositories>
      <pluginRepositories>
        <pluginRepository>
          <id>red-hat-enterprise-maven-repository</id>
          <url>https://maven.repository.redhat.com/ga/</url>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </pluginRepository>
      </pluginRepositories>
    </profile>
  3. settings.xml 文件的 <activeProfiles > 元素中添加以下行,并保存文件。

    <activeProfile>red-hat-enterprise-maven-repository</activeProfile>

6.1.2. 下载并配置 Quarkus Maven 存储库

如果您不想使用在线 Maven 存储库,您可以下载并配置 Quarkus Maven 存储库,以使用 Maven 创建 Quarkus 应用。Quarkus Maven 存储库包含 Java 开发人员通常用来构建应用程序的许多要求。此流程描述了如何编辑 settings.xml 文件来配置 Quarkus Maven 存储库。

注意

当您修改 Maven settings.xml 文件来配置存储库时,更改适用于所有 Maven 项目。

流程

  1. 从红帽客户门户的软件 下载页面 (需要登录)下载 Quarkus Maven 存储库 ZIP 文件。
  2. 展开下载的存档。
  3. 将目录更改到 ~/.m2/ 目录,并在文本编辑器或集成开发环境(IDE)中打开 Maven settings.xml 文件。
  4. 将以下行添加到 settings.xml 文件的 & lt;profiles > 元素中,其中 QUARKUS_MAVEN_REPOSITORY 是您下载的 Quarkus Maven 存储库的路径。QUARKUS_MAVEN_REPOSITORY 的格式必须是 file://$PATH,例如 file:///home/userX/rh-quarkus-2.2.3.GA-maven-repository/maven-repository

    <!-- Configure the Quarkus Maven repository -->
    <profile>
      <id>red-hat-enterprise-maven-repository</id>
      <repositories>
        <repository>
          <id>red-hat-enterprise-maven-repository</id>
          <url>QUARKUS_MAVEN_REPOSITORY</url>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </repository>
      </repositories>
      <pluginRepositories>
        <pluginRepository>
          <id>red-hat-enterprise-maven-repository</id>
          <url>QUARKUS_MAVEN_REPOSITORY</url>
          <releases>
            <enabled>true</enabled>
          </releases>
          <snapshots>
            <enabled>false</enabled>
          </snapshots>
        </pluginRepository>
      </pluginRepositories>
    </profile>
  5. settings.xml 文件的 <activeProfiles > 元素中添加以下行,并保存文件。

    <activeProfile>red-hat-enterprise-maven-repository</activeProfile>
重要

如果您的 Maven 存储库包含过时的工件,则构建或部署项目时可能会遇到以下 Maven 错误消息之一,其中 ARTIFACT_NAME 是缺少的工件的名称,PROJECT_NAME 是您要构建的项目的名称:

  • 缺少工件 PROJECT_NAME
  • [ERROR] Failed to execute goal on project ARTIFACT_NAME; Could not resolve dependencies for PROJECT_NAME

要解决这个问题,请删除位于 ~/.m2/repository 目录中的本地存储库的缓存版本,以强制下载最新的 Maven 工件。

6.2. 使用 Maven 插件创建 OptaPlanner Red Hat build of Quarkus Maven 项目

您可以使用 Apache Maven 和 Quarkus Maven 插件,通过红帽构建的 OptaPlanner 和 Quarkus 应用程序来获取和运行。

先决条件

  • OpenJDK 11 或更高版本已安装。红帽构建的 Open JDK 可从红帽客户门户中的 Software Downloads 页面获得(需要登录)。
  • 已安装 Apache Maven 3.6 或更高版本。Maven 可从 Apache Maven Project 网站获取。

流程

  1. 在命令终端中,输入以下命令来验证 Maven 是否使用 JDK 11,并且 Maven 版本是否为 3.6 或更高版本:

    mvn --version
  2. 如果前面的命令没有返回 JDK 11,请将到 JDK 11 的路径添加到 PATH 环境变量中,然后再次输入前面的命令。
  3. 要生成 Quarkus OptaPlanner quickstart 项目,请输入以下命令:

    mvn com.redhat.quarkus.platform:quarkus-maven-plugin:2.2.3.Final-redhat-00013:create \
        -DprojectGroupId=com.example \
        -DprojectArtifactId=optaplanner-quickstart  \
        -Dextensions="resteasy,resteasy-jackson,optaplanner-quarkus,optaplanner-quarkus-jackson" \
        -DplatformGroupId=com.redhat.quarkus.platform
        -DplatformVersion=2.2.3.Final-redhat-00013 \
        -DnoExamples

    这个命令在 ./optaplanner-quickstart 目录中创建以下元素:

    • Maven 结构
    • src/main/docker中的 Dockerfile 文件示例
    • 应用程序配置文件

      表 6.1. mvn io.quarkus:quarkus-maven-plugin:2.2.3.Final-redhat-00013:create 命令中使用的属性

      属性描述

      projectGroupId

      项目的组 ID。

      projectArtifactId

      项目的工件 ID。

      extensions

      以逗号分隔的 Quarkus 扩展列表,用于此项目。如需 Quarkus 扩展的完整列表,请在命令行中输入 mvn quarkus:list-extensions

      noExamples

      创建一个具有项目结构的项目,但没有测试或类。

      projectGroupIDprojectArtifactID 属性的值用于生成项目版本。默认项目版本为 1.0.0-SNAPSHOT

  4. 要查看您的 OptaPlanner 项目,将目录改为 OptaPlanner Quickstarts 目录:

    cd optaplanner-quickstart
  5. 检查 pom.xml 文件。内容应类似以下示例:

    <dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>io.quarkus.platform</groupId>
          <artifactId>quarkus-bom</artifactId>
          <version>2.2.3.Final-redhat-00013</version>
          <type>pom</type>
          <scope>import</scope>
        </dependency>
        <dependency>
          <groupId>io.quarkus.platform</groupId>
          <artifactId>quarkus-optaplanner-bom</artifactId>
          <version>2.2.3.Final-redhat-00013</version>
          <type>pom</type>
          <scope>import</scope>
        </dependency>
      </dependencies>
    </dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy</artifactId>
      </dependency>
      <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy-jackson</artifactId>
      </dependency>
      <dependency>
        <groupId>org.optaplanner</groupId>
        <artifactId>optaplanner-quarkus</artifactId>
      </dependency>
      <dependency>
        <groupId>org.optaplanner</groupId>
        <artifactId>optaplanner-quarkus-jackson</artifactId>
      </dependency>
      <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5</artifactId>
        <scope>test</scope>
      </dependency>
    </dependencies>

6.3. 使用 code.quarkus.redhat.com 创建红帽构建的 Quarkus Maven 项目

您可以使用 code.quarkus.redhat.com 网站来生成红帽构建的 OptaPlanner Quarkus Maven 项目,并自动添加并配置要在应用程序中使用的扩展。此外,code.quarkus.redhat.com 会自动管理将项目编译到原生可执行文件所需的配置参数。

本节介绍了生成 OptaPlanner Maven 项目的过程,并包括以下主题:

  • 指定应用程序的基本详情。
  • 选择您要包含在项目中的扩展。
  • 使用您的项目文件生成可下载的存档。
  • 使用自定义命令编译并启动您的应用程序。

先决条件

  • 您有一个 Web 浏览器。

流程

  1. 在网页浏览器中打开 https://code.quarkus.redhat.com:
  2. 指定项目详情:
  3. 输入项目的组名称。名称格式遵循 Java 软件包命名约定,例如 com.example
  4. 输入您要用于项目生成的 Maven 工件的名称,如 code-with-quarkus
  5. 选择 Build Tool > Maven 以指定您要创建 Maven 项目。您选择的构建工具决定了项目:

    • 生成项目的目录结构
    • 生成项目中使用的配置文件的格式
    • 在生成项目后,自定义构建脚本和命令用于编译并启动 code.quarkus.redhat.com 的应用程序

      注意

      红帽提供了对使用 code.quarkus.redhat.com 的支持,来仅创建 OptaPlanner Maven 项目。红帽不支持生成 Gradle 项目。

  6. 输入要在项目生成的工件中使用的版本。此字段的默认值为 1.0.0-SNAPSHOT。建议使用 语义版本,但如果愿意,您可以使用不同类型的版本。
  7. 输入构建工具在软件包项目时生成的工件的软件包名称。

    根据 Java 软件包命名约定,软件包名称应与项目的组名称匹配,但您可以指定不同的名称。

    注意

    code.quarkus.redhat.com 网站自动使用 OptaPlanner 的最新版本。在生成项目后,您可以手动更改 pom.xml 文件中的 BOM 版本。

  8. 选择以下扩展以包含在依赖项中:

    • RESTEasy JAX-RS (quarkus-resteasy)
    • RESTEasy Jackson (quarkus-resteasy-jackson)
    • OptaPlanner AI constraint solver(optaplanner-quarkus)
    • OptaPlanner Jackson (optaplanner-quarkus-jackson)

      红帽为列表上的单个扩展提供不同级别的支持,这些扩展由每个扩展名称旁的标签表示:

      • 红帽完全支持 SUPPORTED 扩展,以便在生产环境中的企业应用程序中使用。
      • 在" 技术预览 "扩展方面,红帽在 功能支持范围下,受红帽的支持限制
      • 红帽不支持 DEV-SUPPORT 扩展以供红帽在生产环境中使用,但红帽开发人员为开发新应用程序提供支持的核心功能。
      • DEPRECATED 扩展计划被替换为提供相同功能的较新技术或实施。

        红帽不支持未标记的扩展以用于生产环境。

  9. 选择 Generate your application 来确认您的选择,并使用包含您生成的项目的存档的下载链接显示覆盖屏幕。覆盖屏幕还显示可用于编译和启动应用程序的自定义命令。
  10. 选择 Download the ZIP 将存档与生成的项目文件保存到您的系统中。
  11. 提取存档的内容。
  12. 进入包含您提取的项目文件的目录:

    cd <directory_name>
  13. 以开发模式编译并启动应用程序:

    ./mvnw compile quarkus:dev

6.4. 使用 Quarkus CLI 创建红帽构建的 Quarkus Maven 项目

您可以使用 Quarkus 命令行界面(CLI)创建一个 Quarkus OptaPlanner 项目。

先决条件

流程

  1. 创建 Quarkus 应用程序:

    quarkus create app -P io.quarkus:quarkus-bom:2.2.3.Final-redhat-00013
  2. 要查看可用的扩展,请输入以下命令:

    quarkus ext -i

    这个命令返回以下扩展:

    optaplanner-quarkus
    optaplanner-quarkus-benchmark
    optaplanner-quarkus-jackson
    optaplanner-quarkus-jsonb
  3. 输入以下命令在项目的 pom.xml 文件中添加扩展:

    quarkus ext add resteasy-jackson
    quarkus ext add optaplanner-quarkus
    quarkus ext add optaplanner-quarkus-jackson
  4. 在文本编辑器中打开 pom.xml 文件。文件的内容应类似以下示例:

    <?xml version="1.0"?>
    <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
      <modelVersion>4.0.0</modelVersion>
      <groupId>org.acme</groupId>
      <artifactId>code-with-quarkus-optaplanner</artifactId>
      <version>1.0.0-SNAPSHOT</version>
      <properties>
    	<compiler-plugin.version>3.8.1</compiler-plugin.version>
    	<maven.compiler.parameters>true</maven.compiler.parameters>
    	<maven.compiler.source>11</maven.compiler.source>
    	<maven.compiler.target>11</maven.compiler.target>
    	<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    	<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    	<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
    	<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
    	<quarkus.platform.version>2.2.3.Final-redhat-00013</quarkus.platform.version>
    	<surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
      </properties>
      <dependencyManagement>
    	<dependencies>
      	<dependency>
        	<groupId>${quarkus.platform.group-id}</groupId>
        	<artifactId>${quarkus.platform.artifact-id}</artifactId>
        	<version>${quarkus.platform.version}</version>
        	<type>pom</type>
        	<scope>import</scope>
      	</dependency>
      	<dependency>
        	<groupId>io.quarkus.platform</groupId>
        	<artifactId>optaplanner-quarkus</artifactId>
        	<version>2.2.2.Final</version>
        	<type>pom</type>
        	<scope>import</scope>
      	</dependency>
    	</dependencies>
      </dependencyManagement>
      <dependencies>
    	<dependency>
      	<groupId>io.quarkus</groupId>
      	<artifactId>quarkus-arc</artifactId>
    	</dependency>
    	<dependency>
      	<groupId>io.quarkus</groupId>
      	<artifactId>quarkus-resteasy</artifactId>
    	</dependency>
    	<dependency>
      	<groupId>org.optaplanner</groupId>
      	<artifactId>optaplanner-quarkus</artifactId>
    	</dependency>
    	<dependency>
      	<groupId>org.optaplanner</groupId>
      	<artifactId>optaplanner-quarkus-jackson</artifactId>
    	</dependency>
    	<dependency>
      	<groupId>io.quarkus</groupId>
      	<artifactId>quarkus-resteasy-jackson</artifactId>
    	</dependency>
    	<dependency>
      	<groupId>io.quarkus</groupId>
      	<artifactId>quarkus-junit5</artifactId>
      	<scope>test</scope>
    	</dependency>
    	<dependency>
      	<groupId>io.rest-assured</groupId>
      	<artifactId>rest-assured</artifactId>
      	<scope>test</scope>
    	</dependency>
      </dependencies>
      <build>
    	<plugins>
      	<plugin>
        	<groupId>${quarkus.platform.group-id}</groupId>
        	<artifactId>quarkus-maven-plugin</artifactId>
        	<version>${quarkus.platform.version}</version>
        	<extensions>true</extensions>
        	<executions>
          	<execution>
            	<goals>
              	<goal>build</goal>
              	<goal>generate-code</goal>
              	<goal>generate-code-tests</goal>
            	</goals>
          	</execution>
        	</executions>
      	</plugin>
      	<plugin>
        	<artifactId>maven-compiler-plugin</artifactId>
        	<version>${compiler-plugin.version}</version>
        	<configuration>
          	<parameters>${maven.compiler.parameters}</parameters>
        	</configuration>
      	</plugin>
      	<plugin>
        	<artifactId>maven-surefire-plugin</artifactId>
        	<version>${surefire-plugin.version}</version>
        	<configuration>
          	<systemPropertyVariables>
            	<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
            	<maven.home>${maven.home}</maven.home>
          	</systemPropertyVariables>
        	</configuration>
      	</plugin>
    	</plugins>
      </build>
      <profiles>
    	<profile>
      	<id>native</id>
      	<activation>
        	<property>
          	<name>native</name>
        	</property>
      	</activation>
      	<build>
        	<plugins>
          	<plugin>
            	<artifactId>maven-failsafe-plugin</artifactId>
            	<version>${surefire-plugin.version}</version>
            	<executions>
              	<execution>
                	<goals>
                  	<goal>integration-test</goal>
                  	<goal>verify</goal>
                	</goals>
                	<configuration>
                  	<systemPropertyVariables>
                    	<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                    	<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                    	<maven.home>${maven.home}</maven.home>
                  	</systemPropertyVariables>
                	</configuration>
              	</execution>
            	</executions>
          	</plugin>
        	</plugins>
      	</build>
      	<properties>
        	<quarkus.package.type>native</quarkus.package.type>
      	</properties>
    	</profile>
      </profiles>
    </project>

部分 III. 红帽构建的 OptaPlanner solver

通过 OptaPlanner 解决计划问题包括以下步骤:

  1. 将计划问题建模 为使用 @PlanningSolution 注释(例如,NQueens 类)注解的类。
  2. 配置 Solver (例如,为任何 NQueens 实例配置第一个 Fit 和 Tabu Search solver)。
  3. 数据层加载问题 数据集(如 Four Queens 实例)。这就是规划问题。
  4. 通过 Solv er.solve (问题)解决此问题,这将返回找到的最佳解决方案。
inputOutputOverview

第 7 章 配置红帽构建的 OptaPlanner solver

您可以使用以下方法配置 OptaPlanner solver:

  • 使用 XML 文件。
  • 使用 SolverConfig API。
  • 在域模型中添加类注解和 JavaBean 属性注解。
  • 控制 OptaPlanner 用来访问您的域的方法。
  • 定义自定义属性。

7.1. 使用 XML 文件配置 OptaPlanner solver

您可以使用 XML 文件来配置解析器。在遵循 Maven 目录结构的典型项目中,在使用 Solver Factory 构建 SolverFactory 实例后,solverConfig XML 文件位于 $PROJECT_DIR/src/main/resources/org/optaplanner/examples/<PROJECT>/solver 目录,其中 &lt ;PROJECT > 是您的 OptaPlanner 项目的名称。或者,您可以使用 SolverFactory .createFromXmlFile ()从文件创建 SolverFactory。但是,出于可移植性的原因,建议使用 classpath 资源。

SolverSolverFactory 都有一个称为 Solution_ 的通用类型,即代表计划问题和解决方案的类。

OptaPlanner 通过更改配置来切换优化算法相对容易。

流程

  1. 使用 Solver Factory 构建 Solver 实例。
  2. 配置解析器配置 XML 文件:

    1. 定义模型。
    2. 定义 score 功能。
    3. 可选:配置优化算法。

      以下示例是 NQueens 问题的 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>
          <scoreDrl>org/optaplanner/examples/nqueens/solver/nQueensConstraints.drl</scoreDrl>
        </scoreDirectorFactory>
      
        <!-- Configure the optimization algorithms (optional) -->
        <termination>
          ...
        </termination>
        <constructionHeuristic>
          ...
        </constructionHeuristic>
        <localSearch>
          ...
        </localSearch>
      </solver>
      注意

      在某些环境中,如 OSGi 和 JBoss 模块,在 JAR 文件中的 solver 配置、分数 DRL 和域类等类路径资源可能不适用于 optaplanner-core JAR 文件的默认 ClassLoader。在这些情况下,以参数形式提供类的 ClassLoader

             SolverFactory<NQueens> solverFactory = SolverFactory.createFromXmlResource(
                     ".../nqueensSolverConfig.xml", getClass().getClassLoader());
  3. 使用 solver 配置 XML 文件配置 SolverFactory,作为 ClassLoader.getResource () 定义的 classpath 资源:

           SolverFasctory<NQueens> solverFactory = SolverFactory.createFromXmlResource(
                   "org/optaplanner/examples/nqueens/solver/nqueensSolverConfig.xml");
           Solver<NQueens> solver = solverFactory.buildSolver();

7.2. 使用 Java API 配置 OptaPlanner solver

您可以使用 SolverConfig API 配置 solver。这对在运行时动态更改值特别有用。以下示例在 NQueens 项目中构建 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 类或软件包命名空间 org.optaplanner.core.config 中的 Config 类属性提供。这些配置 类是 XML 格式的 Java 表示。它们构建软件包命名空间的运行时组件 org.optaplanner.core.impl,并将它们汇编成高效的 Solver

注意

要为每个用户请求动态配置 SolverFactory,请在初始化过程中构建模板 SolverConfig,并使用每个用户请求的复制构造器复制它。以下示例演示了如何使用 NQueens 问题进行此操作:

    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();
        ...
    }

7.3. OptaPlanner 注解

您必须指定域模型中的哪些类是规划实体、哪些属性正在规划变量等。使用以下方法之一在 OptaPlanner 项目中添加注解:

  • 在域模型中添加类注解和 JavaBean 属性注解。属性注解必须位于 getter 方法上,而不是在 setter 方法上。注解的 getter 方法不需要是公共的。这是推荐的方法。
  • 在域模型中添加类注解和字段注解。注解的字段不需要是公共字段。

7.4. 指定 OptaPlanner 域访问

默认情况下,OptaPlanner 使用反射访问您的域。reflection 是可靠的,但与直接访问相比会较慢。或者,您可以将 OptaPlanner 配置为使用 Gizmo 访问您的域,这将生成字节码直接访问域的字段和方法,而无需反映。但是,此方法有以下限制:

  • 计划注解仅适用于公共字段和公共 getter。
  • io.quarkus.gizmo:gizmo 必须位于 classpath 上。
注意

当您将 OptaPlanner 与 Quarkus 搭配使用时,这些限制不适用,因为 Gizmo 是默认的域访问类型。

流程

要使用 Quarkus 之外的 Gizmo,请在 solver 配置中设置 domainAccessType

  <solver>
    <domainAccessType>GIZMO</domainAccessType>
  </solver>

7.5. 配置自定义属性

在 OptaPlanner 项目中,您可以添加自定义属性来解析配置元素来实例化类并明确提到自定义属性的文档。

先决条件

  • 您有一个解决者。

流程

  1. 添加自定义属性。

    例如,如果您的 EasyScoreCalculator 具有重缓存计算,并且您希望在一个基准测试中增加缓存大小,请添加 myCacheSize 属性:

      <scoreDirectorFactory>
        <easyScoreCalculatorClass>...MyEasyScoreCalculator</easyScoreCalculatorClass>
        <easyScoreCalculatorCustomProperties>
          <property name="myCacheSize" value="1000"/><!-- Override value -->
        </easyScoreCalculatorCustomProperties>
      </scoreDirectorFactory>
  2. 为每个自定义属性添加一个公共设置,该属性会在构建 Solver 时调用。

    public class MyEasyScoreCalculator extends EasyScoreCalculator<MySolution, SimpleScore> {
    
            private int myCacheSize = 500; // Default value
    
            @SuppressWarnings("unused")
            public void setMyCacheSize(int myCacheSize) {
                this.myCacheSize = myCacheSize;
            }
    
        ...
    }

    大多数值类型都受到支持,包括 布尔值intPimalStringenum

第 8 章 OptaPlanner Solver

解决者为您的规划问题找到最佳、最佳解决方案。解决者只能一次解决一个规划问题实例。解决者使用 SolverFactory 方法构建:

public interface Solver<Solution_> {

    Solution_ solve(Solution_ problem);

    ...
}

解决者应只从单个线程访问,除了 javadoc 中特别记录的方法作为线程安全的方法。solve () 方法会占用当前的线程。Hogging the thread 可能会导致 REST 服务的 HTTP 超时,它需要额外的代码来并行解决多个数据集。要避免这个问题,请改为使用 SolverManager

8.1. 解决问题

使用问题解决计划问题。

先决条件

  • 从 solver 配置构建的 Solver
  • 代表规划问题实例的 @PlanningSolution 注释

流程

将计划问题作为 solve () 方法的参数提供。解决者将返回找到的最佳解决方案。

以下示例解决了 NQueens 问题:

    NQueens problem = ...;
    NQueens bestSolution = solver.solve(problem);

在本例中,solve () 方法将返回一个 NQueens 实例,每 Queen 分配给一个 Row

注意

solve (Solution) 方法提供的解决方案实例可以部分或完全初始化,这通常是重复计划中的情况。

图 8.1. 在 8ms (Also a Optimal Solution)中 Four Queens Puzzle 最佳解决方案。

solvedNQueens04

解决(Solution) 方法可能需要很长时间,具体取决于问题大小和解决者配置。Solver 智能地完成可能解决方案的搜索空间,并记住解决过程中遇到的最佳解决方案。根据多个因素,包括问题大小、Solver 具有多少时间、解决者配置等,最好的 解决方案可能是或者 可能不是最佳 解决方案。

注意

给方法 解决 的解决方案实例由 Solver 更改,但不会误认为最佳解决方案。

方法返回的解决方案实例 solve (Solution)getBestSolution () 最有可能是提供给方法 解决(Solution) 的实例的计划克隆,这意味着它是不同的实例。

8.2. 解决方案环境模式

通过解决环境模式,您可以检测实施中的常见错误。它不会影响日志级别。

解决者只有一个随机实例。有些解析器配置使用随机实例。例如,Simulated Annealing 算法高度依赖于随机数字,而 Tabu Search 仅依赖于它来解析分数。环境模式会影响该随机实例的 seed。

您可以在 solver 配置 XML 文件中设置环境模式。以下示例设置 FAST_ASSERT 模式:

<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">
  <environmentMode>FAST_ASSERT</environmentMode>
  ...
</solver>

以下列表描述了您可以在 solver 配置文件中使用的环境模式:

  • FULL_ASSERT 模式打开所有断言,例如,每次移动实施、约束、引擎本身等操作,每次移动实施错误出现增量分数计算不正确的断言。这个模式可以重复。它也是入侵的,因为它调用方法 compute Score () 频率会比非 assert 模式更频繁。FULL_ASSERT 模式非常慢,因为它不依赖于增量分数计算。
  • NON_INTRUSIVE_FULL_ASSERT 模式打开几个断言,以便在移动实施、约束、引擎本身等错误上快速失败。这个模式可以重复。它不是非侵入性,因为它不会调用方法 compute Score () 比非 assert 模式更频繁。NON_INTRUSIVE_FULL_ASSERT 模式非常慢,因为它不依赖于增量分数计算。
  • FAST_ASSERT 模式打开大多数断言,例如 undoMove 的分数与移动之前相同的断言,以便在移动实现、移动实施、约束、引擎本身等错误上快速失败。这个模式可以重复。它也是入侵的,因为它调用方法 compute Score () 频率会比非 assert 模式更频繁。FAST_ASSERT 模式缓慢。编写一个测试案例,通过 FAST_ASSERT 模式执行简短运行您的计划问题。
  • REPRODUCIBLE 模式是默认模式,因为它在开发过程中是推荐的模式。在这个模式中,两个以相同的 OptaPlanner 版本运行,以相同的顺序执行相同的代码。它们每一步都运行的结果都相同,但如果适用以下备注。这可让您一致地重现错误。它还使您能够对某些重构进行基准测试,如分数约束性能优化,在运行之间相当好。

    注意

    尽管使用 REPRODCIBLE 模式,但出于以下原因,您的应用程序可能仍然无法完全可重复生成:

    • 使用 HashSet 或另一个 集合,它在 JVM 运行之间对计划实体或规划值的集合有不一致的顺序,而不是普通问题事实,特别是在解决方案实现中。使用 LinkedHashSet 替换它。
    • 结合时间渐进的依赖算法,最重要的是 Simulated Annealing 算法以及花费时间终止。分配的 CPU 时间有非常大的差异会影响时间渐变的值。使用 Late Acceptance 算法替换 Simulated Annealing 算法,或使用步骤终止替换终止的时间。
  • REPRODUCIBLE 模式可能比 NON_REPRODUCIBLE 模式稍慢。如果您的生产环境可从可重复生成过程中获益,请在生产环境中使用此模式。在实践中,如果未指定 seed,则 REPRODUCIBLE 模式使用默认的固定随机发现,同时还会禁用某些并发优化,如工作窃取。
  • NON_REPRODUCIBLE 模式可能比 REPRODUCIBLE 模式稍快。避免在开发过程中使用它,因为它会导致调试和程序错误修复比较困难。如果您的生产环境中的可重复生成性并不重要,请在生产环境中使用 NON_REPRODUCIBLE 模式。在实践中,如果没有指定 seed,这个模式不使用固定的随机发现。

8.3. 更改 OptaPlanner solver 日志记录级别

您可以更改 OptaPlanner solver 中的日志级别来查看 solver 活动。以下列表描述了不同的日志记录级别:

  • Error: Logs 错误,除了作为 RuntimeException 形式丢弃到调用代码中的错误。

    如果发生错误,OptaPlanner 通常会快速失败。它将引发一系列 RuntimeException,并将详细消息提供给调用代码。为避免重复的日志消息,它不会将其记录为错误。除非调用代码明确捕获并消除 RuntimeException,否则 线程的默认 'ExceptionHandler 会将它作为错误记录。同时,代码会因为执行进一步危害或模糊处理错误而中断。

  • warn :日志可疑情况
  • info :记录每个阶段和解决方案本身
  • debug :记录每个阶段的每个步骤
  • trace :记录每个阶段每个步骤的每个移动
注意

指定 trace 日志记录会大大降低性能。但是,在开发过程中可以发现瓶颈。

对于 Late Acceptance 和 Simulated Anneing 等快速步骤算法,调试日志记录可能会大大降低性能,但不适用于 Tabu Search 等慢步算法。

trace 和 debug 日志都使用大多数附加器在多线程中解决导致拥塞的问题。

在 Eclipse 中,调试日志记录 到控制台可能会导致拥塞,分数计算速度高于 10000 每秒。IntelliJ 或 Maven 命令行都不会受到此问题。

流程

将日志记录级别设置为 debug 日志记录,以查看阶段何时结束以及执行快速步骤。

以下示例显示了 debug 日志记录的输出:

INFO  Solving started: time spent (3), best score (-4init/0), random (JDK with seed 0).
DEBUG     CH step (0), time spent (5), score (-3init/0), selected move count (1), picked move (Queen-2 {null -> Row-0}).
DEBUG     CH step (1), time spent (7), score (-2init/0), selected move count (3), picked move (Queen-1 {null -> Row-2}).
DEBUG     CH step (2), time spent (10), score (-1init/0), selected move count (4), picked move (Queen-3 {null -> Row-3}).
DEBUG     CH step (3), time spent (12), score (-1), selected move count (4), picked move (Queen-0 {null -> Row-1}).
INFO  Construction Heuristic phase (0) ended: time spent (12), best score (-1), score calculation speed (9000/sec), step total (4).
DEBUG     LS step (0), time spent (19), score (-1),     best score (-1), accepted/selected move count (12/12), picked move (Queen-1 {Row-2 -> Row-3}).
DEBUG     LS step (1), time spent (24), score (0), new best score (0), accepted/selected move count (9/12), picked move (Queen-3 {Row-3 -> Row-2}).
INFO  Local Search phase (1) ended: time spent (24), best score (0), score calculation speed (4000/sec), step total (2).
INFO  Solving ended: time spent (24), best score (0), score calculation speed (7000/sec), phase total (2), environment mode (REPRODUCIBLE).

所有使用值的时间都以毫秒为单位。

所有内容都记录到 SLF4J,这是一个简单的日志记录传真,它将每个日志消息委派给 Logback、Apache Commons Logging、Log4j 或 java.util.logging。为您的日志记录框架添加依赖项到日志记录适配器。

8.4. 使用 Logback 记录 OptaPlanner solver 活动

Logback 是推荐的日志记录框架,用于 OptaPlanner。使用 Logback 记录 OptaPlanner solver 活动。

先决条件

  • 您有一个 OptaPlanner 项目。

流程

  1. 在 OptaPlanner 项目的 pom.xml 文件中添加以下 Maven 依赖项:

    注意

    您不需要添加额外的网桥依赖项。

        <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-classic</artifactId>
          <version>1.x</version>
        </dependency>
  2. logback.xml 文件中配置 org.optaplanner 软件包的日志级别,如下例所示,其中 < LEVEL&gt; 是 第 8.4 节 “使用 Logback 记录 OptaPlanner solver 活动” 中列出的日志级别。

    <configuration>
    
      <logger name="org.optaplanner" level="<LEVEL>"/>
    
      ...
    
    </configuration>
  3. 可选:如果您有一个多租户应用程序,其中可能同时运行多个 Solver 实例,请将每个实例的日志记录分成单独的文件:

    1. 使用 Mapped Diagnostic Context (MDC)括起 solve () 调用:

              MDC.put("tenant.name",tenantName);
              MySolution bestSolution = solver.solve(problem);
              MDC.remove("tenant.name");
    2. 将您的日志记录器配置为为每个 ${tenant.name} 使用不同的文件。例如,在 logback.xml 文件中使用 SiftingAppender

        <appender name="fileAppender" class="ch.qos.logback.classic.sift.SiftingAppender">
          <discriminator>
            <key>tenant.name</key>
            <defaultValue>unknown</defaultValue>
          </discriminator>
          <sift>
            <appender name="fileAppender.${tenant.name}" class="...FileAppender">
              <file>local/log/optaplanner-${tenant.name}.log</file>
              ...
            </appender>
          </sift>
        </appender>
      注意

      当运行多个解析程序或一个多线程解决时,大多数附加程序(包括控制台)会导致 debugtrace 日志记录的拥塞。切换到 async 附加程序以避免出现这个问题或关闭 调试日志记录

  4. 如果 OptaPlanner 无法识别新级别,请临时添加系统属性 -Dlogback.LEVEL=true 以排除故障。

8.5. 使用 Log4J 记录 OptaPlanner solver 活动

如果您已在使用 Log4J,且您不想切换到更快的成功者 Logback,您可以为 Log4J 配置 OptaPlanner 项目。

先决条件

  • 您有一个 OptaPlanner 项目
  • 您使用 Log4J 日志记录框架

流程

  1. 将网桥依赖项添加到项目 pom.xml 文件中:

        <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
          <version>1.x</version>
        </dependency>
  2. log4j.xml 文件中配置软件包 org.optaplanner 的日志记录级别,如下例所示,其中 < LEVEL&gt; 是 第 8.4 节 “使用 Logback 记录 OptaPlanner solver 活动” 中列出的日志级别。

    <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
    
      <category name="org.optaplanner">
        <priority value="<LEVEL>" />
      </category>
    
      ...
    
    </log4j:configuration>
  3. 可选:如果您有一个多租户应用程序,其中可能同时运行多个 Solver 实例,请将每个实例的日志记录分成单独的文件:

    1. 使用 Mapped Diagnostic Context (MDC)括起 solve () 调用:

              MDC.put("tenant.name",tenantName);
              MySolution bestSolution = solver.solve(problem);
              MDC.remove("tenant.name");
    2. 将您的日志记录器配置为为每个 ${tenant.name} 使用不同的文件。例如,在 logback.xml 文件中使用 SiftingAppender

        <appender name="fileAppender" class="ch.qos.logback.classic.sift.SiftingAppender">
          <discriminator>
            <key>tenant.name</key>
            <defaultValue>unknown</defaultValue>
          </discriminator>
          <sift>
            <appender name="fileAppender.${tenant.name}" class="...FileAppender">
              <file>local/log/optaplanner-${tenant.name}.log</file>
              ...
            </appender>
          </sift>
        </appender>
      注意

      当运行多个解析程序或一个多线程解决时,大多数附加程序(包括控制台)会导致 debugtrace 日志记录的拥塞。切换到 async 附加程序以避免出现这个问题或关闭 调试日志记录

8.6. 监控问题

OptaPlanner 通过 Micrometer (Java 应用程序的指标检测库)公开指标。您可以使用带有流行监控系统的 Micrometer 来监控 OptaPlanner solver。

8.6.1. 为 Micrometer 配置 Quarkus OptaPlanner 应用程序

要将 OptaPlanner Quarkus 应用程序配置为使用 Micrometer 和指定的监控系统,请将 Micrometer 依赖项添加到 pom.xml 文件中。

先决条件

  • 您有一个 Quarkus OptaPlanner 应用程序。

流程

  1. 将以下依赖项添加到应用程序的 pom.xml 文件,其中 < MONITORING_SYSTEM& gt; 是 Micrometer 和 Quarkus 支持的监控系统:

    注意

    Prometheus 目前是 Quarkus 支持的唯一监控系统。

    <dependency>
     <groupId>io.quarkus</groupId>
     <artifactId>quarkus-micrometer-registry-<MONITORING_SYSTEM></artifactId>
    </dependency>
  2. 要以开发模式运行应用程序,请输入以下命令:

    mvn compile quarkus:dev
  3. 要查看应用程序的指标,请在浏览器中输入以下 URL:

    http://localhost:8080/q/metrics

8.6.2. 为 Micrometer 配置 Spring Boot OptaPlanner 应用程序

要将 Spring Boot OptaPlanner 应用程序配置为使用 Micrometer 和指定的监控系统,请将 Micrometer 依赖项添加到 pom.xml 文件中。

先决条件

  • 您有一个 Spring Boot OptaPlanner 应用程序。

流程

  1. 将以下依赖项添加到应用程序的 pom.xml 文件,其中 < MONITORING_SYSTEM& gt; 是 Micrometer 和 Spring Boot 支持的监控系统:

    <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
     <groupId>io.micrometer</groupId>
     <artifactId>micrometer-registry-<MONITORING_SYSTEM></artifactId>
    </dependency>
  2. 在应用程序的 application.properties 文件中添加配置信息。如需更多信息,请参阅 Micrometer 网站。
  3. 要运行应用程序,请输入以下命令:

    mvn spring-boot:run
  4. 要查看应用程序的指标,请在浏览器中输入以下 URL:

    http://localhost:8080/actuator/metrics

    注意

    使用以下 URL 作为 Prometheus scraper 路径: http://localhost:8080/actuator/prometheus

8.6.3. 为 Micrometer 配置普通 Java OptaPlanner 应用程序

要将普通 Java OptaPlanner 应用程序配置为使用 Micrometer,您必须将您选择的监控系统的 Micrometer 依赖项和配置信息添加到项目的 POM.XML 文件中。

先决条件

  • 您有一个普通 Java OptaPlanner 应用程序。

流程

  1. 将以下依赖项添加到应用程序的 pom.xml 文件,其中 < MONITORING_SYSTEM& gt; 是一个监控系统,配置了 Micrometer,& lt;VERSION > 是您使用的 Micrometer 版本:

    <dependency>
     <groupId>io.micrometer</groupId>
     <artifactId>micrometer-registry-<MONITORING_SYSTEM></artifactId>
     <version><VERSION></version>
    </dependency>
    <dependency>
     <groupId>io.micrometer</groupId>
     <artifactId>micrometer-core</artifactId>
     <version>`<VERSION>`</version>
    </dependency>
  2. 将您的监控系统的 Micrometer 配置信息添加到项目的 pom.xml 文件的开头。如需更多信息,请参阅 Micrometer 网站。
  3. 在配置信息下添加以下行,其中 &lt ;MONITORING_SYSTEM > 是您添加的监控系统:

    Metrics.addRegistry(<MONITORING_SYSTEM>);

    以下示例演示了如何添加 Prometheus 监控系统:

    PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
    try {
        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
        server.createContext("/prometheus", httpExchange -> {
            String response = prometheusRegistry.scrape();
            httpExchange.sendResponseHeaders(200, response.getBytes().length);
            try (OutputStream os = httpExchange.getResponseBody()) {
                os.write(response.getBytes());
            }
        });
        new Thread(server::start).start();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    Metrics.addRegistry(prometheusRegistry);
  4. 打开监控系统,以查看 OptaPlanner 项目的指标。公开以下指标:

    注意

    指标的名称和格式因 registry 而异。

    • OptaPlanner.solver.errors.total :从测量开始解决时发生的错误总数。
    • OptaPlanner.solver.solve-length.active-count :当前解决者的数量。
    • OptaPlanner.solver.solve-length.seconds-max: 运行当前活跃解决器的最长时间。
    • OptaPlanner.solver.solve-length.seconds-sum:每个活跃解析器的解析持续时间的总和。例如,如果存在两个活动解决者,一个运行三分钟,另一个运行一分钟,则总解决时间为四分钟。

8.7. 配置随机数字生成器

许多 heuristics 和 metaheuristics 依赖于伪随机数生成器来移动选择,以解析分数划分、基于概率的移动接受度等。在解决期间,会重复使用相同的随机实例来提高随机值的可重复性、性能和统一分布。

一个随机 seed 是一个用于初始化伪随机数生成器的数字。

流程

  1. 可选: 要更改随机实例的随机 seed,请指定 randomSeed

    <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">
      <randomSeed>0</randomSeed>
      ...
    </solver>
  2. 可选: 要更改伪随机数生成器实现,请为下面的 solver 配置文件中列出的 randomType 属性指定一个值,其中 < RANDOM_NUMBER_GENERATOR& gt; 是一个伪随机数生成器:

    <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">
      <randomType><RANDOM_NUMBER_GENERATOR></randomType>
      ...
    </solver>

    支持以下伪随机数生成器:

    • JDK (默认):标准随机数生成器实现(java.util.Random)
    • MERSENNE_TWISTER: Commons Math:随机数字生成器实现
    • WELL512A,WELL1024A,WELL19937A,WELL19937C,WELL44497AWELL44497B: Commons Math

对于大多数用例,randomType 属性的值对多个数据集的最佳解决方案的平均质量没有严重影响。

第 9 章 OptaPlanner SolverManager

SolverManager 是一个或多个 Solver 实例的一个常见问题,可简化 REST 和其他企业服务中的规划问题。

Solver.solve (…​) 方法不同,SolverManager 具有以下特征:

  • SolverManager.solve (…​) 立即返回:它会在不阻止调用线程的情况下调度异步解决的问题。这可避免 HTTP 和其他技术的超时问题。
  • SolverManager.solve (…​) 可以并行解决同一域的多个规划问题。

在内部,SolverManager 管理解决程序线程的线程池,它调用 Solver.solve (…​) 和消费者线程的线程池,用于处理最佳解决方案更改事件。

在 Quarkus 和 Spring Boot 中,SolverManager 实例会自动注入到您的代码中。如果您使用 Quarkus 或 Spring Boot 以外的平台,请使用 create (…​) 方法构建 SolverManager 实例:

SolverConfig solverConfig = SolverConfig.createFromXmlResource(".../cloudBalancingSolverConfig.xml");
SolverManager<CloudBalance, UUID> solverManager = SolverManager.create(solverConfig, new SolverManagerConfig());

每个提交给 SolverManager.solve (…​) 方法的问题都必须有一个唯一的问题 ID。稍后调用 getSolverStatus (problemId)terminationEarly (problemId) 使用问题 ID 来区分规划问题。问题 ID 必须是不可变类,如 Long,String, 或 java.util.UUID

SolverManagerConfig 类有一个 parallelSolverCount 属性,用于控制并行运行多少个 solvers。例如,如果 parallelSolverCount 属性设置为 4,并且您提交五个问题,则四个问题会立即解决问题,并在第一个问题之一结束时启动第五个问题。如果这些问题每五分钟解决,则第五个问题需要 10 分钟才能完成。默认情况下,parallelSolverCount 设置为 AUTO,它解析到一半的 CPU 内核,无论解决者的 moveThreadCount 是什么。

要检索最佳解决方案,在解决问题后,通常会使用 SolverJob.getFinalBestSolution ()

CloudBalance problem1 = ...;
UUID problemId = UUID.randomUUID();
// Returns immediately
SolverJob<CloudBalance, UUID> solverJob = solverManager.solve(problemId, problem1);
...
CloudBalance solution1;
try {
    // Returns only after solving terminates
    solution1 = solverJob.getFinalBestSolution();
} catch (InterruptedException | ExecutionException e) {
    throw ...;
}

但是,有更好的方法,在用户需要解决方案以及实时解决之前解决批处理问题,同时用户主动等待解决方案。

当前 SolverManager 实施在单一计算机节点上运行,但未来的工作旨在在云中分发可解决者负载。

9.1. 批量解决问题

批量解决正在并行解决多个数据集。批量解决在夜间特别有用:

  • 夜间通常有一些或没有问题更改。例如,一些组织会强制使用截止时间,例如,在 午夜前提交所有一天发出的请求
  • 解决者可能会运行更长的时间,通常是数小时,因为 nobody 正在等待结果和 CPU 资源成本低得多。
  • 当员工参与下一工作日时,即可使用解决方案。

流程

要批量解决问题,由 parallelSolverCount 的限制,每个数据集都会创建以下类的 call solve (…​)

+

public class TimeTableService {

    private SolverManager<TimeTable, Long> solverManager;

    // Returns immediately, call it for every data set
    public void solveBatch(Long timeTableId) {
        solverManager.solve(timeTableId,
                // Called once, when solving starts
                this::findById,
                // Called once, when solving ends
                this::save);
    }

    public TimeTable findById(Long timeTableId) {...}

    public void save(TimeTable timeTable) {...}

}

9.2. 解决并侦听显示进度

当用户等待解决方案时运行解决者时,用户可能需要等待几分钟或小时后才能获得结果。为确保用户一切正常,通过显示最佳解决方案和目前最佳分数来显示进度。

流程

  1. 要处理中间的最佳解决方案,请使用 solveAndListen (…​)

    public class TimeTableService {
    
        private SolverManager<TimeTable, Long> solverManager;
    
        // Returns immediately
        public void solveLive(Long timeTableId) {
            solverManager.solveAndListen(timeTableId,
                    // Called once, when solving starts
                    this::findById,
                    // Called multiple times, for every best solution change
                    this::save);
        }
    
        public TimeTable findById(Long timeTableId) {...}
    
        public void save(TimeTable timeTable) {...}
    
        public void stopSolving(Long timeTableId) {
            solverManager.terminateEarly(timeTableId);
        }
    
    }

    这种实施使用数据库与 UI 通信,后者轮询数据库。更高级的实施将最佳解决方案直接推送到 UI 或消息传递队列。

  2. 当用户对中间最佳解决方案满意且不想再等待更好的解决方案时,请调用 SolverManager.terminateEarly (problemId)

部分 IV. 红帽构建的 OptaPlanner 快速入门指南

红帽构建的 OptaPlanner 提供以下快速入门指南,以演示如何与不同的技术集成:

  • Red Hat build of OptaPlanner on Red Hat build of Quarkus: a ute Timetable quick Start Guide
  • Red Hat build of OptaPlanner on Red Hat build of Quarkus: a vaccination appointment scheduler quick start Guide
  • Red Hat build of OptaPlanner on Spring Boot: a National timetable quick Start guide
  • Red Hat build of OptaPlanner with Java solvers: Cloud balancing quick Start Guide

第 10 章 Red Hat build of OptaPlanner on Red Hat build of Quarkus: a ute Timetable quick Start Guide

本指南指导您使用 Red Hat build of OptaPlanner 的约束解决人为智能(AI)来创建红帽构建的 Quarkus 应用程序的过程。您将构建可针对学生和教师优化时的 REST 应用程序

timeTableAppScreenshot

通过使用 AI 遵循以下硬和软 调度限制,您的服务会将 Lesson 实例分配给 TimeslotRoom 实例:

  • 房间最多可以有一节时间。
  • 教师可以指导大多数课程同时进行学习。
  • 学员最多可以同时参加一门课程。
  • 教师更喜欢在单一房间教授。
  • 教员更喜欢教授在课程之间的顺序课程和分解。

以数学方式讲,中立时间是 NP-hard 问题。这意味着很难扩展。只需通过与 brute 强制实现所有可能的组合迭代将花费数以百万计的数据集,即使在超级计算机上也是如此。需要的是,AI 约束解决者(如红帽构建的 OptaPlanner)具有在合理的时间内提供近优化型解决方案的高级算法。认为是合理的时间,取决于问题的目标。

先决条件

  • OpenJDK 11 或更高版本已安装。红帽构建的 Open JDK 可从红帽客户门户中的 Software Downloads 页面获得(需要登录)。
  • 已安装 Apache Maven 3.6 或更高版本。Maven 可从 Apache Maven Project 网站获取。
  • 提供了 IDE,如 IntelliJ IDEA、VSCode、Eclipse 或 NetBeans。

10.1. 使用 Maven 插件创建 OptaPlanner Red Hat build of Quarkus Maven 项目

您可以使用 Apache Maven 和 Quarkus Maven 插件,通过红帽构建的 OptaPlanner 和 Quarkus 应用程序来获取和运行。

先决条件

  • OpenJDK 11 或更高版本已安装。红帽构建的 Open JDK 可从红帽客户门户中的 Software Downloads 页面获得(需要登录)。
  • 已安装 Apache Maven 3.6 或更高版本。Maven 可从 Apache Maven Project 网站获取。

流程

  1. 在命令终端中,输入以下命令来验证 Maven 是否使用 JDK 11,并且 Maven 版本是否为 3.6 或更高版本:

    mvn --version
  2. 如果前面的命令没有返回 JDK 11,请将到 JDK 11 的路径添加到 PATH 环境变量中,然后再次输入前面的命令。
  3. 要生成 Quarkus OptaPlanner quickstart 项目,请输入以下命令:

    mvn com.redhat.quarkus.platform:quarkus-maven-plugin:2.2.3.Final-redhat-00013:create \
        -DprojectGroupId=com.example \
        -DprojectArtifactId=optaplanner-quickstart  \
        -Dextensions="resteasy,resteasy-jackson,optaplanner-quarkus,optaplanner-quarkus-jackson" \
        -DplatformGroupId=com.redhat.quarkus.platform
        -DplatformVersion=2.2.3.Final-redhat-00013 \
        -DnoExamples

    这个命令在 ./optaplanner-quickstart 目录中创建以下元素:

    • Maven 结构
    • src/main/docker中的 Dockerfile 文件示例
    • 应用程序配置文件

      表 10.1. mvn io.quarkus:quarkus-maven-plugin:2.2.3.Final-redhat-00013:create 命令中使用的属性

      属性描述

      projectGroupId

      项目的组 ID。

      projectArtifactId

      项目的工件 ID。

      extensions

      以逗号分隔的 Quarkus 扩展列表,用于此项目。如需 Quarkus 扩展的完整列表,请在命令行中输入 mvn quarkus:list-extensions

      noExamples

      创建一个具有项目结构的项目,但没有测试或类。

      projectGroupIDprojectArtifactID 属性的值用于生成项目版本。默认项目版本为 1.0.0-SNAPSHOT

  4. 要查看您的 OptaPlanner 项目,将目录改为 OptaPlanner Quickstarts 目录:

    cd optaplanner-quickstart
  5. 检查 pom.xml 文件。内容应类似以下示例:

    <dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>io.quarkus.platform</groupId>
          <artifactId>quarkus-bom</artifactId>
          <version>2.2.3.Final-redhat-00013</version>
          <type>pom</type>
          <scope>import</scope>
        </dependency>
        <dependency>
          <groupId>io.quarkus.platform</groupId>
          <artifactId>quarkus-optaplanner-bom</artifactId>
          <version>2.2.3.Final-redhat-00013</version>
          <type>pom</type>
          <scope>import</scope>
        </dependency>
      </dependencies>
    </dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy</artifactId>
      </dependency>
      <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy-jackson</artifactId>
      </dependency>
      <dependency>
        <groupId>org.optaplanner</groupId>
        <artifactId>optaplanner-quarkus</artifactId>
      </dependency>
      <dependency>
        <groupId>org.optaplanner</groupId>
        <artifactId>optaplanner-quarkus-jackson</artifactId>
      </dependency>
      <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5</artifactId>
        <scope>test</scope>
      </dependency>
    </dependencies>

10.2. 对域对象建模

红帽构建的 OptaPlanner timetable 项目旨在为每个时间插槽和房间分配。要做到这一点,添加三个类、TimeslotLessonRoom,如下图所示:

timeTableClassDiagramPure

Timeslot

Timeslot 类代表在教授课程时的时间间隔,例如: 星期一 10:30 - 11:30Tuesday 13:30 - 14:30。在这个示例中,所有时间插槽都具有相同的持续时间,在午餐或其他中断期间都没有时间插槽。

时间插槽没有日期,因为高校的计划仅每周重复。不需要 持续规划。一个时间被认为是个问题,因为在解决 过程中,实例不会发生改变。此类类不需要任何特定于 OptaPlanner 的注解。

room

Room 类代表教授课程的位置,例如 Room ARoom B。在这个示例中,所有房间都没有容量限制,它们可以适应所有课程。

空间 实例在解决过程中不会改变,因此 Room 也是 问题事实

courseon

在课程学习期间,教员将教授一门主题给一组学员,例如: A.Turing for 9th gradeChemistry,M.Curie 为 10th grade如果每周向同一学员组教授了多次主题,则有多个较少实例,只有 id 区分。例如,每周的第 9 个学分有 6 个学分。

在解决期间,OptaPlanner 会改变下课的计时和 房间 字段,以将每个上课时间分配到一个时间插槽和房间。 因为 OptaPlanner 更改了这些字段,所以 Lesson 是一个 计划实体

timeTableClassDiagramAnnotated

上图中的大多数字段都包含输入数据,但 orange 字段除外。在输入数据中未分配(空),并在输出数据中分配(非 null)字段的不计时和 房间 字段。在解决过程中,OptaPlanner 会更改这些字段。此类字段称为规划变量。为了使 OptaPlanner 能够识别它们,lot 和 room 字段都需要 @PlanningVariable 注解。它们包含类 Lesson,需要 @PlanningEntity 注释。

流程

  1. 创建 src/main/java/com/example/domain/Timeslot.java 类:

    package com.example.domain;
    
    import java.time.DayOfWeek;
    import java.time.LocalTime;
    
    public class Timeslot {
    
        private DayOfWeek dayOfWeek;
        private LocalTime startTime;
        private LocalTime endTime;
    
        private Timeslot() {
        }
    
        public Timeslot(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
            this.dayOfWeek = dayOfWeek;
            this.startTime = startTime;
            this.endTime = endTime;
        }
    
        @Override
        public String toString() {
            return dayOfWeek + " " + startTime.toString();
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public DayOfWeek getDayOfWeek() {
            return dayOfWeek;
        }
    
        public LocalTime getStartTime() {
            return startTime;
        }
    
        public LocalTime getEndTime() {
            return endTime;
        }
    
    }

    注意 toString () 方法保持输出短,以便更轻松地读取 OptaPlanner 的 DEBUGTRACE 日志,如后文所示。

  2. 创建 src/main/java/com/example/domain/Room.java 类:

    package com.example.domain;
    
    public class Room {
    
        private String name;
    
        private Room() {
        }
    
        public Room(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return name;
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public String getName() {
            return name;
        }
    
    }
  3. 创建 src/main/java/com/example/domain/Lesson.java 类:

    package com.example.domain;
    
    import org.optaplanner.core.api.domain.entity.PlanningEntity;
    import org.optaplanner.core.api.domain.variable.PlanningVariable;
    
    @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() {
        }
    
        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() {
            return subject + "(" + id + ")";
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        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;
        }
    
    }

    Lesson 类具有 @PlanningEntity 注释,因此 OptaPlanner 知道该类在解决问题过程中发生了变化,因为它包含一个或多个规划变量。

    timeslot 字段具有 @PlanningVariable 注释,因此 OptaPlanner 知道它可以更改其值。要查找分配给此字段的潜在 Timeslot 实例,OptaPlanner 使用 valueRangeProviderRefs 属性连接到提供 List<Timeslot > 来选择的值范围供应商。有关值范围供应商的信息,请参阅 第 10.4 节 “在规划解决方案中收集域对象”

    room 字段也具有 @PlanningVariable 注释,理由相同。

10.3. 定义限制并计算分数

在解决问题时,分数 代表特定解决方案的质量。分数越大。红帽构建的 OptaPlanner 会寻找最佳解决方案,这是在可用时间内获得最高分数的解决方案。它可能 是最佳解决方案

因为 timetable 示例用例有硬和软限制,所以使用 HardSoftScore 类来代表分数:

  • 硬限制不能中断。例如: 一个房间最多可以有一课时间。
  • 软限制不应中断。例如 :教员更倾向于在单一房间进行学习。

硬限制会根据其他硬限制加权。软限制会根据其他软限制加权。硬限制总是大于软限制,无论它们的权重是什么。

要计算分数,您可以实施 EasyScoreCalculator 类:

public class TimeTableEasyScoreCalculator implements EasyScoreCalculator<TimeTable> {

    @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 "complete" implementation
        return HardSoftScore.of(hardScore, softScore);
    }

}

不幸的是,这个解决方案无法很好地扩展,因为它没有递增:每次课程被分配到不同的时间插槽或房间,所有课程均会重新评估以计算新分数。

更好的解决方案是创建一个 src/main/java/com/example/solver/TimeTableConstraintProvider.java 类,以执行增量分数计算。这个类使用 OptaPlanner 的 ConstraintStream API,它被 Java 8 Streams 和 SQL 实现。ConstraintProvider 扩展一个比 EasyScoreCalculator:O(n)而不是 O(n)的 magnitude 顺序。

流程

创建以下 src/main/java/com/example/solver/TimeTableConstraintProvider.java 类:

package com.example.solver;

import com.example.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 "complete" implementation
        };
    }

    private Constraint roomConflict(ConstraintFactory constraintFactory) {
        // A room can accommodate at most one lesson at the same time.

        // Select a lesson ...
        return constraintFactory.from(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.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 attend at most one lesson at the same 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);
    }

}

10.4. 在规划解决方案中收集域对象

TimeTable 实例将单个数据集的所有 TimeslotRoomLesson 实例包装。此外,由于它包含所有经验,因此每个均具有特定的计划变量状态,因此它是一个 规划解决方案,它分数如下:

  • 如果尚未取消分配课程,则它是一个 未初始化的 解决方案,例如,分数为 -4init/0hard/0soft 的解决方案。
  • 如果它破坏了硬约束,则它是一个不可分的解决方案,例如,分数为 -2hard/-3soft 的解决方案。
  • 如果它遵循所有硬约束,则它是一个可行的解决方案,例如,分数为 0hard/-7soft 的解决方案。

TimeTable 类具有 @PlanningSolution 注释,因此红帽构建的 OptaPlanner 知道此类包含所有输入和输出数据。

具体来说,这个类是问题的输入:

  • 一个 timeslotList 字段以及所有时间插槽

    • 这是问题事实的列表,因为它们在解决过程中不会改变。
  • 带有所有空间的 roomList 字段

    • 这是问题事实的列表,因为它们在解决过程中不会改变。
  • 包含所有课程的 lessonList 字段

    • 这是计划实体列表,因为它们在解决过程中发生了变化。
    • 在每个课

      • timeslotroom 字段的值通常仍为 null,因此未分配。它们正在规划变量。
      • 其他字段(如 主题教师和 学员 )填写完毕。这些字段是问题属性。

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

  • 一个 lessonList 字段,每个 Lesson 实例在解决后 都带有非空 次数和 房间 字段
  • 代表输出解决方案的质量的 score 字段,如 0hard/-5soft

流程

创建 src/main/java/com/example/domain/TimeTable.java 类:

package com.example.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;

    private TimeTable() {
    }

    public TimeTable(List<Timeslot> timeslotList, List<Room> roomList,
            List<Lesson> lessonList) {
        this.timeslotList = timeslotList;
        this.roomList = roomList;
        this.lessonList = lessonList;
    }

    // ********************************
    // Getters and setters
    // ********************************

    public List<Timeslot> getTimeslotList() {
        return timeslotList;
    }

    public List<Room> getRoomList() {
        return roomList;
    }

    public List<Lesson> getLessonList() {
        return lessonList;
    }

    public HardSoftScore getScore() {
        return score;
    }

}

值范围供应商

timeslotList 字段是一个值范围 provider。它包含 OptaPlanner 可以从中选择的 Timeslot 实例,以分配给 Lesson 实例的 timeslot 字段。timeslotList 字段具有一个 @ValueRangeProvider 注释,用于连接这两者,方法是将 idLesson 中的 @PlanningVariablevalueRangeProviderRefs 匹配。

遵循同一逻辑时,roomList 字段也具有 @ValueRangeProvider 注释。

问题事实和规划实体属性

另外,OptaPlanner 需要知道哪个 Lesson 实例可以更改,以及如何检索由 TimeTableConstraintProvider 分数计算使用的 TimeslotRoom 实例。

timeslotListroomList 字段具有 @ProblemFactCollectionProperty 注释,因此您的 TimeTableConstraintProvider 可以从这些实例中进行选择。

lessonList 具有 @PlanningEntityCollectionProperty 注释,因此 OptaPlanner 可以在解决问题期间更改它们,并且您的 TimeTableConstraintProvider 也可以从这些注释中选择。

10.5. 创建 solver 服务

解决 REST 线程上的规划问题会导致 HTTP 超时问题。因此,Quarkus 扩展会注入 SolverManager,它会在单独的线程池中运行 solvers,并可并行解决多个数据集。

流程

创建 src/main/java/org/acme/optaplanner/rest/TimeTableResource.java 类:

package org.acme.optaplanner.rest;

import java.util.UUID;
import java.util.concurrent.ExecutionException;
import javax.inject.Inject;
import javax.ws.rs.POST;
import javax.ws.rs.Path;

import org.acme.optaplanner.domain.TimeTable;
import org.optaplanner.core.api.solver.SolverJob;
import org.optaplanner.core.api.solver.SolverManager;

@Path("/timeTable")
public class TimeTableResource {

    @Inject
    SolverManager<TimeTable, UUID> solverManager;

    @POST
    @Path("/solve")
    public TimeTable solve(TimeTable problem) {
        UUID problemId = UUID.randomUUID();
        // Submit the problem to start solving
        SolverJob<TimeTable, UUID> solverJob = solverManager.solve(problemId, problem);
        TimeTable solution;
        try {
            // Wait until the solving ends
            solution = solverJob.getFinalBestSolution();
        } catch (InterruptedException | ExecutionException e) {
            throw new IllegalStateException("Solving failed.", e);
        }
        return solution;
    }

}

初始实施会等待 solver 完成,这仍可能导致 HTTP 超时。完整的实施避免了 HTTP 超时非常明显。

10.6. 设置解决者终止的时间

如果您的规划应用程序没有终止设置或终止事件,它理论上会永久运行,并最终导致 HTTP 超时错误。要防止这种情况发生,请使用 optaplanner.solver.termination.spent-limit 参数指定应用程序终止的时间长度。在大多数应用程序中,将时间设置为至少五分钟(5m)。但是,在 Timetable 示例中,将解决问题时间限制为 5 秒,这足以避免 HTTP 超时。

流程

使用以下内容创建 src/main/resources/application.properties 文件:

quarkus.optaplanner.solver.termination.spent-limit=5s

10.7. 运行 school timetable 应用程序

创建 school timetable 项目后,以开发模式运行它。在开发模式中,您可以在应用程序运行时更新应用程序源和配置。您的更改将显示在正在运行的应用程序中。

先决条件

  • 您已创建了 school timetable 项目。

流程

  1. 要以开发模式编译应用程序,请从项目目录中输入以下命令:

    ./mvnw compile quarkus:dev
  2. 测试 REST 服务。您可以使用任何 REST 客户端。以下示例使用 Linux 命令 curl 发送 POST 请求:

    $ curl -i -X POST http://localhost:8080/timeTable/solve -H "Content-Type:application/json" -d '{"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"}]}'

    终止中指定的时间段花费在 application.properties 文件中定义的时间后,服务会返回类似以下示例的输出:

    HTTP/1.1 200
    Content-Type: application/json
    ...
    
    {"timeslotList":...,"roomList":...,"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"}

    请注意,您的应用程序将全部四节分配给两个时间中的插槽之一,以及两个空间之一。另请注意,它符合所有硬约束。例如,M. Curie 在不同的时间插槽中有两个经验。

  3. 要在解决期间检查 OptaPlanner 在解决期间执行的操作,请查看服务器端的 info 日志。以下是 info 日志输出示例:

    ... 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).

10.8. 测试应用程序

良好的应用程序包括测试覆盖。测试时间表项目中的限制和 solver。

10.8.1. 测试 school 计时的限制

要以隔离方式测试每个 timetable 项目约束,请在单元测试中使用 ConstraintVerifier。这会测试每个约束与其他测试隔离的情况,这会在添加具有适当测试覆盖的新约束时降低维护。

此测试会验证约束 TimeTableConstraintProvider::roomConflict,当同一间有 3 个经验时,两个经验都有相同的 timeslot,被 penalizes 为 1。因此,如果约束权重为 10hard,它将分数降低为 -10hard

流程

创建 src/test/java/org/acme/optaplanner/solver/TimeTableConstraintProviderTest.java 类:

package org.acme.optaplanner.solver;

import java.time.DayOfWeek;
import java.time.LocalTime;

import javax.inject.Inject;

import io.quarkus.test.junit.QuarkusTest;
import org.acme.optaplanner.domain.Lesson;
import org.acme.optaplanner.domain.Room;
import org.acme.optaplanner.domain.TimeTable;
import org.acme.optaplanner.domain.Timeslot;
import org.junit.jupiter.api.Test;
import org.optaplanner.test.api.score.stream.ConstraintVerifier;

@QuarkusTest
class TimeTableConstraintProviderTest {

    private static final Room ROOM = new Room("Room1");
    private static final Timeslot TIMESLOT1 = new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9,0), LocalTime.NOON);
    private static final Timeslot TIMESLOT2 = new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9,0), LocalTime.NOON);

    @Inject
    ConstraintVerifier<TimeTableConstraintProvider, TimeTable> constraintVerifier;

    @Test
    void roomConflict() {
        Lesson firstLesson = new Lesson(1, "Subject1", "Teacher1", "Group1");
        Lesson conflictingLesson = new Lesson(2, "Subject2", "Teacher2", "Group2");
        Lesson nonConflictingLesson = new Lesson(3, "Subject3", "Teacher3", "Group3");

        firstLesson.setRoom(ROOM);
        firstLesson.setTimeslot(TIMESLOT1);

        conflictingLesson.setRoom(ROOM);
        conflictingLesson.setTimeslot(TIMESLOT1);

        nonConflictingLesson.setRoom(ROOM);
        nonConflictingLesson.setTimeslot(TIMESLOT2);

        constraintVerifier.verifyThat(TimeTableConstraintProvider::roomConflict)
                .given(firstLesson, conflictingLesson, nonConflictingLesson)
                .penalizesBy(1);
    }

}

请注意,ConstraintVerifier 在测试过程中如何忽略约束权重,即使这些约束权重在 ConstraintProvider 中被硬编码。这是因为在进入生产环境前,会定期更改约束权重。这样,约束的权重调整不会破坏单元测试。

10.8.2. 测试 school timetable solver

这个示例测试红帽在 Red Hat build of Quarkus 上构建的 OptaPlanner National Timetable 项目。它使用 JUnit 测试来生成测试数据集,并将其发送到 TimeTableController 来解决。

流程

  1. 使用以下内容创建 src/test/java/com/example/rest/TimeTableResourceTest.java 类:

    package com.exmaple.optaplanner.rest;
    
    import java.time.DayOfWeek;
    import java.time.LocalTime;
    import java.util.ArrayList;
    import java.util.List;
    
    import javax.inject.Inject;
    
    import io.quarkus.test.junit.QuarkusTest;
    import com.exmaple.optaplanner.domain.Room;
    import com.exmaple.optaplanner.domain.Timeslot;
    import com.exmaple.optaplanner.domain.Lesson;
    import com.exmaple.optaplanner.domain.TimeTable;
    import com.exmaple.optaplanner.rest.TimeTableResource;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.Timeout;
    
    import static org.junit.jupiter.api.Assertions.assertFalse;
    import static org.junit.jupiter.api.Assertions.assertNotNull;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    @QuarkusTest
    public class TimeTableResourceTest {
    
        @Inject
        TimeTableResource timeTableResource;
    
        @Test
        @Timeout(600_000)
        public void solve() {
            TimeTable problem = generateProblem();
            TimeTable solution = timeTableResource.solve(problem);
            assertFalse(solution.getLessonList().isEmpty());
            for (Lesson lesson : solution.getLessonList()) {
                assertNotNull(lesson.getTimeslot());
                assertNotNull(lesson.getRoom());
            }
            assertTrue(solution.getScore().isFeasible());
        }
    
        private TimeTable generateProblem() {
            List<Timeslot> timeslotList = new ArrayList<>();
            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)));
    
            List<Room> roomList = new ArrayList<>();
            roomList.add(new Room("Room A"));
            roomList.add(new Room("Room B"));
            roomList.add(new Room("Room C"));
    
            List<Lesson> lessonList = new ArrayList<>();
            lessonList.add(new Lesson(101L, "Math", "B. May", "9th grade"));
            lessonList.add(new Lesson(102L, "Physics", "M. Curie", "9th grade"));
            lessonList.add(new Lesson(103L, "Geography", "M. Polo", "9th grade"));
            lessonList.add(new Lesson(104L, "English", "I. Jones", "9th grade"));
            lessonList.add(new Lesson(105L, "Spanish", "P. Cruz", "9th grade"));
    
            lessonList.add(new Lesson(201L, "Math", "B. May", "10th grade"));
            lessonList.add(new Lesson(202L, "Chemistry", "M. Curie", "10th grade"));
            lessonList.add(new Lesson(203L, "History", "I. Jones", "10th grade"));
            lessonList.add(new Lesson(204L, "English", "P. Cruz", "10th grade"));
            lessonList.add(new Lesson(205L, "French", "M. Curie", "10th grade"));
            return new TimeTable(timeslotList, roomList, lessonList);
        }
    
    }

    此测试会验证在解决后,所有课程都会被分配给一个时间插槽和房间。它还会验证它是否找到了可行的解决方案(没有硬约束问题)。

  2. 将 test 属性添加到 src/main/resources/application.properties 文件中:

    # The solver runs only for 5 seconds to avoid a HTTP timeout in this simple implementation.
    # It's recommended to run for at least 5 minutes ("5m") otherwise.
    quarkus.optaplanner.solver.termination.spent-limit=5s
    
    # Effectively disable this termination in favor of the best-score-limit
    %test.quarkus.optaplanner.solver.termination.spent-limit=1h
    %test.quarkus.optaplanner.solver.termination.best-score-limit=0hard/*soft

通常,解决者在 200 毫秒内找到可行的解决方案。注意 application.properties 文件如何在测试过程中覆盖 solver 终止,以便在找到可行的解决方案 (0hard86_64) soft) 后马上终止。这可避免对解决者进行硬编码,因为单元测试可能会在任意硬件上运行。这种方法可确保测试运行足够长,以查找可行的解决方案,即使在较慢的系统中。但它不会严格运行比严格运行的时间更长,即使在快速系统上也是如此。

10.9. 日志记录

完成红帽构建的 OptaPlanner school Timetable 项目后,您可以使用日志信息来帮助微调 ConstraintProvider 中的限制。查看 info 日志文件中的分数计算速度,以评估更改对您的限制的影响。以 debug 模式运行应用程序,以显示应用程序接受的每个步骤或使用 trace 日志记录来记录每个步骤和每次移动。

流程

  1. 为固定时间(如五分钟)运行 button Timetable 应用程序。
  2. 查看日志文件中的分数计算速度,如下例所示:

    ... Solving ended: ..., score calculation speed (29455/sec), ...
  3. 更改约束,再次运行计划应用程序以获得相同时间,并查看日志文件中记录的分数计算速度。
  4. 以 debug 模式运行应用程序,记录应用程序所做的每个步骤:

    • 要从命令行运行调试模式,请使用 -D 系统属性。
    • 要永久启用调试模式,请在 application.properties 文件中添加以下行:

      quarkus.log.category."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}]).
      ...
  5. 使用 trace logging 来显示每个步骤以及每个步骤的每个移动。

10.10. 将数据库与您的 Quarkus OptaPlanner 64)程序集成

创建 Quarkus OptaPlanner respectively Timetable 应用程序后,您可以将它与数据库集成,并创建一个基于 web 的用户界面来显示可的时间。

先决条件

  • 您有一个 Quarkus OptaPlanner school Timetable 应用程序。

流程

  1. 使用 Hibernate 和 Panache 将 时间RoomLesson 实例存储在数据库中。如需更多信息,请参阅 Simplified Hibernate ORM with Panache
  2. 通过 REST 公开实例。如需更多信息,请参阅 编写 JSON REST 服务
  3. 更新 TimeTableResource 类,以在单个事务中读取和写入 TimeTable 实例:

    package org.acme.optaplanner.rest;
    
    import javax.inject.Inject;
    import javax.transaction.Transactional;
    import javax.ws.rs.GET;
    import javax.ws.rs.POST;
    import javax.ws.rs.Path;
    
    import io.quarkus.panache.common.Sort;
    import org.acme.optaplanner.domain.Lesson;
    import org.acme.optaplanner.domain.Room;
    import org.acme.optaplanner.domain.TimeTable;
    import org.acme.optaplanner.domain.Timeslot;
    import org.optaplanner.core.api.score.ScoreManager;
    import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
    import org.optaplanner.core.api.solver.SolverManager;
    import org.optaplanner.core.api.solver.SolverStatus;
    
    @Path("/timeTable")
    public class TimeTableResource {
    
        public static final Long SINGLETON_TIME_TABLE_ID = 1L;
    
        @Inject
        SolverManager<TimeTable, Long> solverManager;
        @Inject
        ScoreManager<TimeTable, HardSoftScore> scoreManager;
    
        // To try, open http://localhost:8080/timeTable
        @GET
        public TimeTable getTimeTable() {
            // Get the solver status before loading the solution
            // to avoid the race condition that the solver terminates between them
            SolverStatus solverStatus = getSolverStatus();
            TimeTable solution = findById(SINGLETON_TIME_TABLE_ID);
            scoreManager.updateScore(solution); // Sets the score
            solution.setSolverStatus(solverStatus);
            return solution;
        }
    
        @POST
        @Path("/solve")
        public void solve() {
            solverManager.solveAndListen(SINGLETON_TIME_TABLE_ID,
                    this::findById,
                    this::save);
        }
    
        public SolverStatus getSolverStatus() {
            return solverManager.getSolverStatus(SINGLETON_TIME_TABLE_ID);
        }
    
        @POST
        @Path("/stopSolving")
        public void stopSolving() {
            solverManager.terminateEarly(SINGLETON_TIME_TABLE_ID);
        }
    
        @Transactional
        protected TimeTable findById(Long id) {
            if (!SINGLETON_TIME_TABLE_ID.equals(id)) {
                throw new IllegalStateException("There is no timeTable with id (" + id + ").");
            }
            // Occurs in a single transaction, so each initialized lesson references the same timeslot/room instance
            // that is contained by the timeTable's timeslotList/roomList.
            return new TimeTable(
                    Timeslot.listAll(Sort.by("dayOfWeek").and("startTime").and("endTime").and("id")),
                    Room.listAll(Sort.by("name").and("id")),
                    Lesson.listAll(Sort.by("subject").and("teacher").and("studentGroup").and("id")));
        }
    
        @Transactional
        protected void save(TimeTable timeTable) {
            for (Lesson lesson : timeTable.getLessonList()) {
                // TODO this is awfully naive: optimistic locking causes issues if called by the SolverManager
                Lesson attachedLesson = Lesson.findById(lesson.getId());
                attachedLesson.setTimeslot(lesson.getTimeslot());
                attachedLesson.setRoom(lesson.getRoom());
            }
        }
    
    }

    这个示例包含一个 TimeTable 实例。但是,您可以为并行的多个校园启用多租户并处理 TimeTable 实例。

    getTimeTable () 方法返回数据库的最新时间表。它使用 ScoreManager 方法(自动注入)来计算该时间表的分数,并使其可用于 UI。

    solve () 方法启动一个作业,以解决当前的时间表并将时间插槽和房间分配存储在数据库中。它使用 SolverManager.solveAndListen () 方法侦听中间最佳解决方案并相应地更新数据库。UI 使用它来显示后端仍在解决时的进度。

  4. 更新 TimeTableResourceTest 类,以反映 solve () 方法立即返回,并轮询最新的解决方案,直到解决者完成解决问题:

    package org.acme.optaplanner.rest;
    
    import javax.inject.Inject;
    
    import io.quarkus.test.junit.QuarkusTest;
    import org.acme.optaplanner.domain.Lesson;
    import org.acme.optaplanner.domain.TimeTable;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.Timeout;
    import org.optaplanner.core.api.solver.SolverStatus;
    
    import static org.junit.jupiter.api.Assertions.assertFalse;
    import static org.junit.jupiter.api.Assertions.assertNotNull;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    @QuarkusTest
    public class TimeTableResourceTest {
    
        @Inject
        TimeTableResource timeTableResource;
    
        @Test
        @Timeout(600_000)
        public void solveDemoDataUntilFeasible() throws InterruptedException {
            timeTableResource.solve();
            TimeTable timeTable = timeTableResource.getTimeTable();
            while (timeTable.getSolverStatus() != SolverStatus.NOT_SOLVING) {
                // Quick polling (not a Test Thread Sleep anti-pattern)
                // Test is still fast on fast machines and doesn't randomly fail on slow machines.
                Thread.sleep(20L);
                timeTable = timeTableResource.getTimeTable();
            }
            assertFalse(timeTable.getLessonList().isEmpty());
            for (Lesson lesson : timeTable.getLessonList()) {
                assertNotNull(lesson.getTimeslot());
                assertNotNull(lesson.getRoom());
            }
            assertTrue(timeTable.getScore().isFeasible());
        }
    
    }
  5. 在这些 REST 方法基础上构建 Web UI,以提供可时间表的可视化表示。
  6. 查看 快速入门源代码

10.11. 使用 Micrometer 和 Prometheus 来监控您的 ors 时间序列 OptaPlanner Quarkus 应用程序

OptaPlanner 通过 Micrometer (Java 应用程序的指标检测库)公开指标。您可以将 Micrometer 与 Prometheus 搭配使用,以监控 button Timetable 应用程序中的 OptaPlanner solver。

先决条件

  • 您已创建了 Quarkus OptaPlanner ors 工作程序。
  • 已安装 Prometheus。有关安装 Prometheus 的详情,请查看 Prometheus 网站。

流程

  1. 将 Micrometer Prometheus 依赖项添加到 school timetable pom.xml 文件中:

    <dependency>
     <groupId>io.quarkus</groupId>
     <artifactId>quarkus-micrometer-registry-prometheus</artifactId>
    </dependency>
  2. 启动 button timetable 应用程序:

    mvn compile quarkus:dev
  3. 在 Web 浏览器中打开 http://localhost:8080/q/metric

第 11 章 Red Hat build of OptaPlanner on Red Hat build of Quarkus: a vaccination appointment scheduler quick start Guide

您可以使用 OptaPlanner vaccination appointment 调度程序快速开始开发高效且公平的 vaccination 调度。vaccination appointment 调度程序使用人工智能(AI)来优先选择人员并根据多个约束和优先级分配时间插槽。

先决条件

  • OpenJDK 11 或更高版本已安装。红帽构建的 Open JDK 可从红帽客户门户中的 Software Downloads 页面获得(需要登录)。
  • 已安装 Apache Maven 3.6 或更高版本。Maven 可从 Apache Maven Project 网站获取。
  • 提供了 IDE,如 IntelliJ IDEA、VSCode、Eclipse 或 NetBeans。
  • 您已创建了 Quakus OptaPlanner 项目,如 第 6 章 OptaPlanner 和 Quarkus 入门 所述。

11.1. OptaPlanner vaccination appointment 调度程序如何工作

调度点的方法主要有两种。系统可以让个人选择一个代表插槽(用户选择)或者系统分配一个插槽,并告知个人何时和在哪里参加(系统自动分配系统)。OptaPlanner vaccination appointment 调度程序使用 system-automaticly-assigns 方法。使用 OptaPlanner vaccination appointment 调度程序,您可以创建一个应用程序来向系统提供信息,系统会分配 appointment。

这种方法的特性:

  • Appointment 插槽根据优先级分配。
  • 系统根据预先配置的规划限制分配最佳分点时间和位置。
  • 对于有限数量的点点,系统不会给大量用户带来负担。

这种方法通过使用规划限制为每个人创建分数来解决尽可能多的人问题。个人分数决定了它们何时获得代表。个人分数越大,他们获得较早收到的成效几率就越好。

11.1.1. OptaPlanner vaccination appointment 调度程序限制

OptaPlanner vaccination appointment 调度程序限制是 hard、medium 或 soft :

  • 硬约束不能中断。如果有任何硬约束,则计划不可避免,且无法执行:

    • 容量 :在任何位置,不要随时进行手册 vaccine 容量。
    • Vaccine max age:如果一个 vaccine 具有最长期限,请不要将其管理给首次执行 vaccine 最长期限旧的人。确保向人们提供适合其年龄的 vaccine 类型。例如,不要为 vaccine 分配 75 年旧人,其最长期限限制为 65 年。
    • 所需的 vaccine 类型 :使用所需的 vaccine 类型。例如,第二个 vaccine 的作用必须是与第一个操作相同的 vaccine 类型。
    • ready date:管理指定日期或之后的 vaccine。例如,如果个人收到第二个目的,请不要在特定 vaccine 类型的推荐可能 vaccination 日期前管理它,例如 26 天。
    • 到期日期:管理指定日期之前或之前的 vaccine。例如,如果个人收到第二个操作,请在特定 vaccine 的具体 vaccine 的最终到期日期前管理它,例如在第一个操作后 3 个月。
    • 限制最大差差:将每个人分配给最接近一组 vaccination 中心之一。这通常是三个中心中的一个。这个限制通过旅行时间而不是距离计算,因此位于uran 区域中的个人通常比 rural 区域的旅行减少最大的距离。
  • Medium 约束决定在没有足够容量来为所有人分配点时没有获得点点。这在限制的规划中称为:

    • schedule second dose vaccinations:除非理想日期超出计划窗口外,不要留下第二个不准确的 vaccination appointments。
    • 根据优先级评级调度人员:每个人具有优先权评级。这通常是其年龄,但如果它们是健康的 worker,则可能会有更高的值。仅使优先级最低评级的人保留为未分配。它们将在下一次运行中考虑。这个约束比之前的约束软,因为第二个操作始终优先于优先级评级。
  • 软限制不应中断:

    • 首选 vaccination 中心:如果个人有一个首选的 vaccination 中心,请为他们提供该中心的提示。
    • 距离:减少个人必须前往其分配的 vaccination 中心的距离。
    • 理想日期:管理 vaccine on 或以尽可能接近指定日期。例如,如果某人收到第二个操作,则根据特定 vaccine 的高级日期对其进行管理,例如在第一次做后 28 天。这个约束比距离距离的软约束,以避免在国家/地区发送半年,只是接近其理想日期的一天。
    • 优先级评级:调度在规划窗口中之前具有较高优先级评级的人。这个约束比 distance 约束软,以避免在国家(地区)发送一半。这个约束也比理想的日期约束软,因为第二个操作的优先级高于优先级评级。

硬限制会根据其他硬限制加权。软限制会根据其他软限制加权。但是,硬限制总是优先于中型和软限制。如果硬约束出现问题,则计划不可行。但是,如果没有硬限制,则考虑软和中型限制来确定优先级。因为用户通常比可用的 appointment 插槽更多,所以您必须优先选择。第二个问题总是被首先分配,以避免创建以后系统造成过大的积压。之后,根据优先级评级分配人员。每个人都以优先级评级开始,是其年龄。这样做可优先选择旧的人员。之后,特定优先级组中的人员会收到,例如几百个额外点。这因组的优先级而异。例如,nurses 可能会收到额外的 1000 个点。这样一来,旧的 nurses 的优先级高于年轻的 nurses,而年轻的日子则优先选择那些不是紧张的人。下表描述了这个概念:

表 11.1. 优先级评级表

年龄作业(job)优先级评级

60

nurse

1060

33

nurse

1033

71

弃用

71

52

办公室工作者

52

11.1.2. OptaPlanner solver

在 OptaPlanner 的核心是解决者,使用问题数据集并覆盖计划限制和配置引擎。问题数据集包括有关人员、vaccines 和 vaccination 中心的所有信息。解决者通过各种数据组合进行工作,最终决定使用分配给特定中心 vaccination appointments 的人员优化点时间表。以下图显示了解决者创建的调度:

vaccinationSchedulingValueProposal

11.1.3. 持续规划

持续规划是同时管理一个或多个即将进行的计划周期的技术,并重复每个进程每月、每周、每天、每小时甚至更频繁的计划周期。计划窗口按指定间隔逐步推进。以下图显示了每日更新的两周规划窗口:

vaccinationSchedulingContinuousPlanning

两周的规划窗口被分为一半。第一周处于 published 状态,第二周处于草案状态。人们在规划窗口的已发布和草案部分被分配。但是,只有发布的计划窗口部分中的人员才会通知其标点。其它声明仍可在下一次运行时轻松更改。这样做可让 OptaPlanner 在再次运行解决方案时更改草案部分(如有必要)。例如,如果需要第二个确实有一周的准备日期,并且是星期三的理想日期,OptaPlanner 不必为下称,如果您可以证明 OptaPlanner 可以演示它可以在稍后的星期几内给出一个草案。

您可以确定计划窗口的大小,但只了解问题空间的大小。问题空间是创建调度的所有各种元素。您提前规划的天数越大,问题空间越大。

11.1.4. 固定规划实体

如果您每天都不断规划,则在分配给用户的两周内将出现代表性。要确保 appointments 不是双引号的,OptaPlanner 会标记通过固定点来分配的现有 appointments。固定用于定位一个或多个特定分配,并强制 OptaPlanner 计划这些固定分配。在解决过程中,固定计划实体(如 appointment)不会改变。

实体是否被固定,由 appointment 状态决定。一个代表可以有五个状态: 打开、拒绝、接受拒绝 或重新计划。

注意

您实际不会在快速启动演示代码中直接看到这些状态,因为 OptaPlanner 引擎只对 appointment 没有被固定。

您需要能够规划已经调度的点。带有 InvitedAccepted 状态的指示会被固定。OpenRescheduleRejected 状态的代表没有固定,并可用于调度。

在本例中,当解决者在已发布和草案范围内搜索两周规划窗口时。除了未调度的输入数据外,解决者还考虑任何未固定实体、与 OpenReschedule 或拒绝状态相关的实体,以查找最佳解决方案。如果解决者每天运行,您会在运行 solver 前看到一个新的日期添加到调度中。

请注意,新日上的要点已被分配,之前在计划窗口的草案中调度了 Amy 和 Edna,现在在窗口的已发布部分调度。这是因为 Gus 和 Hugo 请求重新调度。这不会引起混淆,因为 Amy 和 Edna 从未通知其草案日期。现在,因为它们在规划窗口的 published 部分有点点,所以会通知它们并被要求接受或拒绝其意见,并且它们现在会被固定。

11.2. 下载并运行 OptaPlanner vaccination appointment 调度程序

下载 OptaPlanner vaccination appointment 调度程序快速启动存档,以 Quarkus 开发模式启动,并在浏览器中查看应用程序。Quarkus 开发模式允许您在应用程序运行时进行更改和更新应用程序。

流程

  1. 导航到红帽客户门户网站中的 Software Downloads 页面(需要登录),然后从下拉菜单中选择产品和版本:

    • 产品:流程自动化管理器
    • Version: 7.12
  2. 下载 Red Hat Process Automation Manager 7.12.0 Kogito 和 OptaPlanner 8 Decision Services Quickstarts (rhpam-7.12.0-kogito-and-optaplanner-quickstarts.zip)。
  3. 提取 rhpam-7.12.0-kogito-and-optaplanner-quickstarts.zip 文件。
  4. 导航到 optaplanner-quickstarts-8.11.1.Final-redhat-00006 目录。
  5. 导航到 optaplanner-quickstarts-8.11.1.Final-redhat-00006/use-cases/vaccination-scheduling 目录。
  6. 输入以下命令在开发模式中启动 OptaPlanner vaccination appointment 调度程序:

    $ mvn quarkus:dev
  7. 要查看 OptaPlanner vaccination appointment 调度程序,在网页浏览器中输入以下 URL:

    http://localhost:8080/
  8. 要运行 OptaPlanner vaccination appointment 调度程序,请单击 Solve
  9. 对源代码进行更改,然后按 F5 键刷新浏览器。请注意,您所做的更改现已可用。

11.3. 软件包并运行 OptaPlanner vaccination appointment 调度程序

当您完成了开发以 quarkus:dev 模式在 OptaPlanner vaccination appointment 调度程序上工作时,请将应用程序作为传统 jar 文件运行。

先决条件

流程

  1. 导航到 /use-cases/vaccination-scheduling 目录。
  2. 要编译 OptaPlanner vaccination appointment 调度程序,请输入以下命令:

    $ mvn package
  3. 要运行编译的 OptaPlanner vaccination appointment 调度程序,请输入以下命令:

    $ java -jar ./target/*-runner.jar
    注意

    若要在端口 8081 上运行应用,请在前面的命令中添加 -Dquarkus.http.port=8081

  4. 要启动 OptaPlanner vaccination appointment 调度程序,在 web 浏览器中输入以下 URL:

    http://localhost:8080/

11.4. 将 OptaPlanner vaccination appointment 调度程序作为原生可执行文件运行

要利用 Quarkus 提供的小内存占用和访问速度,在 Quarkus 原生模式中编译 OptaPlanner vaccination appointment 调度程序。

流程

  1. 安装 GraalVM 和 native-image 工具。如需更多信息,请参阅在 Quarkus 网站上 配置 GraalVMl
  2. 导航到 /use-cases/vaccination-scheduling 目录。
  3. 要原生编译 OptaPlanner vaccination appointment 调度程序,请输入以下命令:

    $ mvn package -Dnative -DskipTests
  4. 要运行原生可执行文件,请输入以下命令:

    $ ./target/*-runner
  5. 要启动 OptaPlanner vaccination appointment 调度程序,在 web 浏览器中输入以下 URL:

    http://localhost:8080/

11.5. 其他资源

第 12 章 Red Hat build of OptaPlanner on Spring Boot: a National timetable quick Start guide

本指南指导您完成使用 OptaPlanner 的约束解决人工智能(AI)创建 Spring Boot 应用程序的过程。您将构建可针对学生和教师优化时限的 REST 应用程序。

timeTableAppScreenshot

通过使用 AI 遵循以下硬和软 调度限制,您的服务会将 Lesson 实例分配给 TimeslotRoom 实例:

  • 房间最多可以有一节时间。
  • 教师可以指导大多数课程同时进行学习。
  • 学员最多可以同时参加一门课程。
  • 教师更喜欢在单一房间教授。
  • 教员更喜欢教授在课程之间的顺序课程和分解。

以数学方式讲,中立时间是 NP-hard 问题。这意味着很难扩展。只需通过与 brute 强制实现所有可能的组合迭代将花费数以百万计的数据集,即使在超级计算机上也是如此。需要的是,AI 约束解决者(如 OptaPlanner)具有在合理的时间内提供接近优化解决方案的高级算法。认为是合理的时间,取决于问题的目标。

先决条件

  • OpenJDK 11 或更高版本已安装。红帽构建的 Open JDK 可从红帽客户门户中的 Software Downloads 页面获得(需要登录)。
  • 已安装 Apache Maven 3.6 或更高版本。Maven 可从 Apache Maven Project 网站获取。
  • 提供了 IDE,如 IntelliJ IDEA、VSCode、Eclipse 或 NetBeans。

12.1. 下载并构建 Spring Boot National timetable 快速启动

要查看红帽使用 Spring Boot 产品的 OptaPlanner 构建 OptaPlanner 项目的完整示例,请从红帽客户门户网站下载初学者应用程序。

流程

  1. 导航到红帽客户门户网站中的 Software Downloads 页面(需要登录),然后从下拉菜单中选择产品和版本:

    • 产品:流程自动化管理器
    • Version: 7.12
  2. 下载 Red Hat Process Automation Manager 7.12.0 Kogito 和 OptaPlanner 8 Decision Services Quickstarts (rhpam-7.12.0-kogito-and-optaplanner-quickstarts.zip)。
  3. 提取 rhpam-7.12.0-kogito-and-optaplanner-quickstarts.zip 文件。
  4. 下载 Red Hat Process Automation Manager 7.12.0 Kogito 和 OptaPlanner 8 Decision Services Maven Repositroy (rhpam-7.12.0-kogito-maven-repository.zip)。
  5. 提取 rhpam-7.12.0-kogito-maven-repository.zip 文件。
  6. rhpam-7.12.0-kogito-maven-repository/maven-repository 子目录的内容复制到 ~/.m2/repository 目录中。
  7. 导航到 optaplanner-quickstarts-8.11.1.Final-redhat-00006/ Technology/java-spring-boot 目录。
  8. 输入以下命令构建 Spring Boot school 计时建立项目:

    mvn clean install -DskipTests
  9. 要构建 Spring Boot orsling 项目,请输入以下命令:

    mvn spring-boot:run -DskipTests
  10. 要查看项目,请在网页浏览器中输入以下 URL:

    http://localhost:8080/

12.2. 对域对象建模

红帽构建的 OptaPlanner timetable 项目旨在为每个时间插槽和房间分配。要做到这一点,添加三个类、TimeslotLessonRoom,如下图所示:

timeTableClassDiagramPure

Timeslot

Timeslot 类代表在教授课程时的时间间隔,例如: 星期一 10:30 - 11:30Tuesday 13:30 - 14:30。在这个示例中,所有时间插槽都具有相同的持续时间,在午餐或其他中断期间都没有时间插槽。

时间插槽没有日期,因为高校的计划仅每周重复。不需要 持续规划。一个时间被认为是个问题,因为在解决 过程中,实例不会发生改变。此类类不需要任何特定于 OptaPlanner 的注解。

room

Room 类代表教授课程的位置,例如 Room ARoom B。在这个示例中,所有房间都没有容量限制,它们可以适应所有课程。

空间 实例在解决过程中不会改变,因此 Room 也是 问题事实

courseon

在课程学习期间,教员将教授一门主题给一组学员,例如: A.Turing for 9th gradeChemistry,M.Curie 为 10th grade如果每周向同一学员组教授了多次主题,则有多个较少实例,只有 id 区分。例如,每周的第 9 个学分有 6 个学分。

在解决期间,OptaPlanner 会改变下课的计时和 房间 字段,以将每个上课时间分配到一个时间插槽和房间。 因为 OptaPlanner 更改了这些字段,所以 Lesson 是一个 计划实体

timeTableClassDiagramAnnotated

上图中的大多数字段都包含输入数据,但 orange 字段除外。在输入数据中未分配(空),并在输出数据中分配(非 null)字段的不计时和 房间 字段。在解决过程中,OptaPlanner 会更改这些字段。此类字段称为规划变量。为了使 OptaPlanner 能够识别它们,lot 和 room 字段都需要 @PlanningVariable 注解。它们包含类 Lesson,需要 @PlanningEntity 注释。

流程

  1. 创建 src/main/java/com/example/domain/Timeslot.java 类:

    package com.example.domain;
    
    import java.time.DayOfWeek;
    import java.time.LocalTime;
    
    public class Timeslot {
    
        private DayOfWeek dayOfWeek;
        private LocalTime startTime;
        private LocalTime endTime;
    
        private Timeslot() {
        }
    
        public Timeslot(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
            this.dayOfWeek = dayOfWeek;
            this.startTime = startTime;
            this.endTime = endTime;
        }
    
        @Override
        public String toString() {
            return dayOfWeek + " " + startTime.toString();
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public DayOfWeek getDayOfWeek() {
            return dayOfWeek;
        }
    
        public LocalTime getStartTime() {
            return startTime;
        }
    
        public LocalTime getEndTime() {
            return endTime;
        }
    
    }

    注意 toString () 方法保持输出短,以便更轻松地读取 OptaPlanner 的 DEBUGTRACE 日志,如后文所示。

  2. 创建 src/main/java/com/example/domain/Room.java 类:

    package com.example.domain;
    
    public class Room {
    
        private String name;
    
        private Room() {
        }
    
        public Room(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return name;
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public String getName() {
            return name;
        }
    
    }
  3. 创建 src/main/java/com/example/domain/Lesson.java 类:

    package com.example.domain;
    
    import org.optaplanner.core.api.domain.entity.PlanningEntity;
    import org.optaplanner.core.api.domain.variable.PlanningVariable;
    
    @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() {
        }
    
        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() {
            return subject + "(" + id + ")";
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        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;
        }
    
    }

    Lesson 类具有 @PlanningEntity 注释,因此 OptaPlanner 知道该类在解决问题过程中发生了变化,因为它包含一个或多个规划变量。

    timeslot 字段具有 @PlanningVariable 注释,因此 OptaPlanner 知道它可以更改其值。要查找分配给此字段的潜在 Timeslot 实例,OptaPlanner 使用 valueRangeProviderRefs 属性连接到提供 List<Timeslot > 来选择的值范围供应商。有关值范围供应商的信息,请参阅 第 12.4 节 “在规划解决方案中收集域对象”

    room 字段也具有 @PlanningVariable 注释,理由相同。

12.3. 定义限制并计算分数

在解决问题时,分数 代表特定解决方案的质量。分数越大。红帽构建的 OptaPlanner 会寻找最佳解决方案,这是在可用时间内获得最高分数的解决方案。它可能 是最佳解决方案

因为 timetable 示例用例有硬和软限制,所以使用 HardSoftScore 类来代表分数:

  • 硬限制不能中断。例如: 一个房间最多可以有一课时间。
  • 软限制不应中断。例如 :教员更倾向于在单一房间进行学习。

硬限制会根据其他硬限制加权。软限制会根据其他软限制加权。硬限制总是大于软限制,无论它们的权重是什么。

要计算分数,您可以实施 EasyScoreCalculator 类:

public class TimeTableEasyScoreCalculator implements EasyScoreCalculator<TimeTable> {

    @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 "complete" implementation
        return HardSoftScore.of(hardScore, softScore);
    }

}

不幸的是,这个解决方案无法很好地扩展,因为它没有递增:每次课程被分配到不同的时间插槽或房间,所有课程均会重新评估以计算新分数。

更好的解决方案是创建一个 src/main/java/com/example/solver/TimeTableConstraintProvider.java 类,以执行增量分数计算。这个类使用 OptaPlanner 的 ConstraintStream API,它被 Java 8 Streams 和 SQL 实现。ConstraintProvider 扩展一个比 EasyScoreCalculator:O(n)而不是 O(n)的 magnitude 顺序。

流程

创建以下 src/main/java/com/example/solver/TimeTableConstraintProvider.java 类:

package com.example.solver;

import com.example.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 "complete" implementation
        };
    }

    private Constraint roomConflict(ConstraintFactory constraintFactory) {
        // A room can accommodate at most one lesson at the same time.

        // Select a lesson ...
        return constraintFactory.from(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.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 attend at most one lesson at the same 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);
    }

}

12.4. 在规划解决方案中收集域对象

TimeTable 实例将单个数据集的所有 TimeslotRoomLesson 实例包装。此外,由于它包含所有经验,因此每个均具有特定的计划变量状态,因此它是一个 规划解决方案,它分数如下:

  • 如果尚未取消分配课程,则它是一个 未初始化的 解决方案,例如,分数为 -4init/0hard/0soft 的解决方案。
  • 如果它破坏了硬约束,则它是一个不可分的解决方案,例如,分数为 -2hard/-3soft 的解决方案。
  • 如果它遵循所有硬约束,则它是一个可行的解决方案,例如,分数为 0hard/-7soft 的解决方案。

TimeTable 类具有 @PlanningSolution 注释,因此红帽构建的 OptaPlanner 知道此类包含所有输入和输出数据。

具体来说,这个类是问题的输入:

  • 一个 timeslotList 字段以及所有时间插槽

    • 这是问题事实的列表,因为它们在解决过程中不会改变。
  • 带有所有空间的 roomList 字段

    • 这是问题事实的列表,因为它们在解决过程中不会改变。
  • 包含所有课程的 lessonList 字段

    • 这是计划实体列表,因为它们在解决过程中发生了变化。
    • 在每个课

      • timeslotroom 字段的值通常仍为 null,因此未分配。它们正在规划变量。
      • 其他字段(如 主题教师和 学员 )填写完毕。这些字段是问题属性。

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

  • 一个 lessonList 字段,每个 Lesson 实例在解决后 都带有非空 次数和 房间 字段
  • 代表输出解决方案的质量的 score 字段,如 0hard/-5soft

流程

创建 src/main/java/com/example/domain/TimeTable.java 类:

package com.example.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;

    private TimeTable() {
    }

    public TimeTable(List<Timeslot> timeslotList, List<Room> roomList,
            List<Lesson> lessonList) {
        this.timeslotList = timeslotList;
        this.roomList = roomList;
        this.lessonList = lessonList;
    }

    // ********************************
    // Getters and setters
    // ********************************

    public List<Timeslot> getTimeslotList() {
        return timeslotList;
    }

    public List<Room> getRoomList() {
        return roomList;
    }

    public List<Lesson> getLessonList() {
        return lessonList;
    }

    public HardSoftScore getScore() {
        return score;
    }

}

值范围供应商

timeslotList 字段是一个值范围 provider。它包含 OptaPlanner 可以从中选择的 Timeslot 实例,以分配给 Lesson 实例的 timeslot 字段。timeslotList 字段具有一个 @ValueRangeProvider 注释,用于连接这两者,方法是将 idLesson 中的 @PlanningVariablevalueRangeProviderRefs 匹配。

遵循同一逻辑时,roomList 字段也具有 @ValueRangeProvider 注释。

问题事实和规划实体属性

另外,OptaPlanner 需要知道哪个 Lesson 实例可以更改,以及如何检索由 TimeTableConstraintProvider 分数计算使用的 TimeslotRoom 实例。

timeslotListroomList 字段具有 @ProblemFactCollectionProperty 注释,因此您的 TimeTableConstraintProvider 可以从这些实例中进行选择。

lessonList 具有 @PlanningEntityCollectionProperty 注释,因此 OptaPlanner 可以在解决问题期间更改它们,并且您的 TimeTableConstraintProvider 也可以从这些注释中选择。

12.5. 创建 Timetable 服务

现在,您已准备好将所有内容放在一起并创建 REST 服务。但解决 REST 线程中的规划问题会导致 HTTP 超时问题。因此,Spring Boot 启动程序注入 SolverManager,它会在单独的线程池中运行 solvers,并可并行解决多个数据集。

流程

创建 src/main/java/com/example/solver/TimeTableController.java 类:

package com.example.solver;

import java.util.UUID;
import java.util.concurrent.ExecutionException;

import com.example.domain.TimeTable;
import org.optaplanner.core.api.solver.SolverJob;
import org.optaplanner.core.api.solver.SolverManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@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 to start solving
        SolverJob<TimeTable, UUID> solverJob = solverManager.solve(problemId, problem);
        TimeTable solution;
        try {
            // Wait until the solving ends
            solution = solverJob.getFinalBestSolution();
        } catch (InterruptedException | ExecutionException e) {
            throw new IllegalStateException("Solving failed.", e);
        }
        return solution;
    }

}

在本例中,初始实现会等待 solver 完成,这仍可能导致 HTTP 超时。完整的 实施避免了 HTTP 超时非常明显。

12.6. 设置解决者终止的时间

如果您的规划应用程序没有终止设置或终止事件,它理论上会永久运行,并最终导致 HTTP 超时错误。要防止这种情况发生,请使用 optaplanner.solver.termination.spent-limit 参数指定应用程序终止的时间长度。在大多数应用程序中,将时间设置为至少五分钟(5m)。但是,在 Timetable 示例中,将解决问题时间限制为 5 秒,这足以避免 HTTP 超时。

流程

使用以下内容创建 src/main/resources/application.properties 文件:

quarkus.optaplanner.solver.termination.spent-limit=5s

12.7. 使应用程序可执行

完成 Red Hat build of OptaPlanner Spring Boot timetable 项目后,将所有内容打包成由标准 Java main () 方法驱动的单个可执行 JAR 文件。

先决条件

  • 您有一个已完成的 OptaPlanner Spring Boot timetable 项目。

流程

  1. 使用以下内容创建 TimeTableSpringBootApp.java 类:

    package com.example;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class TimeTableSpringBootApp {
    
        public static void main(String[] args) {
            SpringApplication.run(TimeTableSpringBootApp.class, args);
        }
    
    }
  2. 将 Spring Initializr 创建的 src/main/java/com/example/DemoApplication.java 类替换为 TimeTableSpringBootApp.java 类。
  3. 运行 TimeTableSpringBootApp.java 类作为常规 Java 应用程序的主类。

12.7.1. 试用可时间的应用程序

启动 Red Hat build of OptaPlanner Spring Boot timetable 应用程序后,您可以使用您想要的任何 REST 客户端测试 REST 服务。本例使用 Linux curl 命令发送 POST 请求。

先决条件

  • OptaPlanner Spring Boot Timetable 应用程序正在运行。

流程

使用以下命令:

$ curl -i -X POST http://localhost:8080/timeTable/solve -H "Content-Type:application/json" -d '{"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"}]}'

大约 5 秒后,终止会花费在 application.properties 中定义的时间,服务会返回类似以下示例的输出:

HTTP/1.1 200
Content-Type: application/json
...

{"timeslotList":...,"roomList":...,"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"}

请注意,应用会将全部四节分配给两个时间插槽中的一个,以及两个房间之一。另请注意,它符合所有硬约束。例如,M. Curie 在不同的时间插槽中有两个经验。

在服务器端,info 日志显示 OptaPlanner 在 5 秒内的功能:

... 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).

12.7.2. 测试应用

良好的应用程序包括测试覆盖。这个示例测试 OptaPlanner Spring Boot 应用程序的 Timetable Red Hat build。它使用 JUnit 测试来生成测试数据集,并将其发送到 TimeTableController 来解决。

流程

使用以下内容创建 src/test/java/com/example/solver/TimeTableControllerTest.java 类:

package com.example.solver;

import java.time.DayOfWeek;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;

import com.example.domain.Lesson;
import com.example.domain.Room;
import com.example.domain.TimeTable;
import com.example.domain.Timeslot;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

@SpringBootTest(properties = {
        "optaplanner.solver.termination.spent-limit=1h", // Effectively disable this termination in favor of the best-score-limit
        "optaplanner.solver.termination.best-score-limit=0hard/*soft"})
public class TimeTableControllerTest {

    @Autowired
    private TimeTableController timeTableController;

    @Test
    @Timeout(600_000)
    public void solve() {
        TimeTable problem = generateProblem();
        TimeTable solution = timeTableController.solve(problem);
        assertFalse(solution.getLessonList().isEmpty());
        for (Lesson lesson : solution.getLessonList()) {
            assertNotNull(lesson.getTimeslot());
            assertNotNull(lesson.getRoom());
        }
        assertTrue(solution.getScore().isFeasible());
    }

    private TimeTable generateProblem() {
        List<Timeslot> timeslotList = new ArrayList<>();
        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)));

        List<Room> roomList = new ArrayList<>();
        roomList.add(new Room("Room A"));
        roomList.add(new Room("Room B"));
        roomList.add(new Room("Room C"));

        List<Lesson> lessonList = new ArrayList<>();
        lessonList.add(new Lesson(101L, "Math", "B. May", "9th grade"));
        lessonList.add(new Lesson(102L, "Physics", "M. Curie", "9th grade"));
        lessonList.add(new Lesson(103L, "Geography", "M. Polo", "9th grade"));
        lessonList.add(new Lesson(104L, "English", "I. Jones", "9th grade"));
        lessonList.add(new Lesson(105L, "Spanish", "P. Cruz", "9th grade"));

        lessonList.add(new Lesson(201L, "Math", "B. May", "10th grade"));
        lessonList.add(new Lesson(202L, "Chemistry", "M. Curie", "10th grade"));
        lessonList.add(new Lesson(203L, "History", "I. Jones", "10th grade"));
        lessonList.add(new Lesson(204L, "English", "P. Cruz", "10th grade"));
        lessonList.add(new Lesson(205L, "French", "M. Curie", "10th grade"));
        return new TimeTable(timeslotList, roomList, lessonList);
    }

}

此测试会验证在解决后,所有课程都会被分配给一个时间插槽和房间。它还会验证它是否找到了可行的解决方案(没有硬约束问题)。

通常,解决者在 200 毫秒内找到可行的解决方案。请注意,@SpringBootTest 注释 的属性 如何在可行的解决方案(0hard locatesoft)后马上终止,以终止。这可避免对解决者进行硬编码,因为单元测试可能会在任意硬件上运行。这种方法可确保测试运行足够长,以查找可行的解决方案,即使在较慢的系统中。但是,它没有严格运行比严格运行的时间更长,即使在快速系统中也是如此。

12.7.3. 日志记录

完成 OptaPlanner Spring Boot Timetable 应用程序的红帽构建后,您可以使用日志信息来帮助微调 ConstraintProvider 中的限制。查看 info 日志文件中的分数计算速度,以评估更改对您的限制的影响。以 debug 模式运行应用程序,以显示应用程序接受的每个步骤或使用 trace 日志记录来记录每个步骤和每次移动。

流程

  1. 为固定时间(如五分钟)运行可定时应用程序。
  2. 查看日志文件中的分数计算速度,如下例所示:

    ... Solving ended: ..., score calculation speed (29455/sec), ...
  3. 更改约束,再次运行计划应用程序以获得相同时间,并查看日志文件中记录的分数计算速度。
  4. 在 debug 模式下运行应用程序来记录每个步骤:

    • 要从命令行运行调试模式,请使用 -D 系统属性。
    • 要更改 application.properties 文件中的日志记录,请在该文件中添加以下行:

      logging.level.org.optaplanner=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}]).
      ...
  5. 使用 trace logging 来显示每个步骤以及每个步骤的每个移动。

12.8. 添加数据库和 UI 集成

在使用 Spring Boot 创建 OptaPlanner 应用程序示例的红帽构建后,添加数据库和 UI 集成。

前提条件

  • 您已创建了 OptaPlanner Spring Boot Timetable 示例。

流程

  1. TimeslotRoomLesson 创建 Java Persistence API (permanent)存储库。有关创建 JPA 存储库的详情,请参考 Spring 网站 通过 JPA 访问数据。
  2. 通过 REST 公开 JPA 存储库。有关公开存储库的详情,请参考 Spring 网站 通过 REST 访问 JPA 数据。
  3. 构建一个 TimeTableRepository facade,在单个事务中读取和写入一个 TimeTable
  4. 如以下示例所示调整 TimeTableController:

    package com.example.solver;
    
    import com.example.domain.TimeTable;
    import com.example.persistence.TimeTableRepository;
    import org.optaplanner.core.api.score.ScoreManager;
    import org.optaplanner.core.api.solver.SolverManager;
    import org.optaplanner.core.api.solver.SolverStatus;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/timeTable")
    public class TimeTableController {
    
        @Autowired
        private TimeTableRepository timeTableRepository;
        @Autowired
        private SolverManager<TimeTable, Long> solverManager;
        @Autowired
        private ScoreManager<TimeTable> scoreManager;
    
        // To try, GET http://localhost:8080/timeTable
        @GetMapping()
        public TimeTable getTimeTable() {
            // Get the solver status before loading the solution
            // to avoid the race condition that the solver terminates between them
            SolverStatus solverStatus = getSolverStatus();
            TimeTable solution = timeTableRepository.findById(TimeTableRepository.SINGLETON_TIME_TABLE_ID);
            scoreManager.updateScore(solution); // Sets the score
            solution.setSolverStatus(solverStatus);
            return solution;
        }
    
        @PostMapping("/solve")
        public void solve() {
            solverManager.solveAndListen(TimeTableRepository.SINGLETON_TIME_TABLE_ID,
                    timeTableRepository::findById,
                    timeTableRepository::save);
        }
    
        public SolverStatus getSolverStatus() {
            return solverManager.getSolverStatus(TimeTableRepository.SINGLETON_TIME_TABLE_ID);
        }
    
        @PostMapping("/stopSolving")
        public void stopSolving() {
            solverManager.terminateEarly(TimeTableRepository.SINGLETON_TIME_TABLE_ID);
        }
    
    }

    为了简单起见,这个代码只处理一个 TimeTable 实例,但启用多租户并并行处理不同高校的多个时间表实例是简单的。

    getTimeTable () 方法返回数据库的最新时间表。它使用 ScoreManager (自动注入)计算该时间表的分数,以便 UI 显示分数。

    solve () 方法启动一个作业,从而解决当前的时间表,并将时间插槽和空间分配存储在数据库中。它使用 SolverManager.solveAndListen () 方法侦听中间最佳解决方案并相应地更新数据库。这可让 UI 在后端仍在解决时显示进度。

  5. 现在,solve () 方法会立即返回,请调整 TimeTableControllerTest,如下例所示:

    package com.example.solver;
    
    import com.example.domain.Lesson;
    import com.example.domain.TimeTable;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.Timeout;
    import org.optaplanner.core.api.solver.SolverStatus;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import static org.junit.jupiter.api.Assertions.assertFalse;
    import static org.junit.jupiter.api.Assertions.assertNotNull;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    @SpringBootTest(properties = {
            "optaplanner.solver.termination.spent-limit=1h", // Effectively disable this termination in favor of the best-score-limit
            "optaplanner.solver.termination.best-score-limit=0hard/*soft"})
    public class TimeTableControllerTest {
    
        @Autowired
        private TimeTableController timeTableController;
    
        @Test
        @Timeout(600_000)
        public void solveDemoDataUntilFeasible() throws InterruptedException {
            timeTableController.solve();
            TimeTable timeTable = timeTableController.getTimeTable();
            while (timeTable.getSolverStatus() != SolverStatus.NOT_SOLVING) {
                // Quick polling (not a Test Thread Sleep anti-pattern)
                // Test is still fast on fast systems and doesn't randomly fail on slow systems.
                Thread.sleep(20L);
                timeTable = timeTableController.getTimeTable();
            }
            assertFalse(timeTable.getLessonList().isEmpty());
            for (Lesson lesson : timeTable.getLessonList()) {
                assertNotNull(lesson.getTimeslot());
                assertNotNull(lesson.getRoom());
            }
            assertTrue(timeTable.getScore().isFeasible());
        }
    
    }
  6. 轮询最新的解决方案,直到解决者完成解决为止。
  7. 要视觉化时间表,请在这些 REST 方法之上构建有吸引力的 Web UI。

12.9. 使用 Micrometer 和 Prometheus 来监控您的 ors 时区 OptaPlanner Spring Boot 应用程序

OptaPlanner 通过 Micrometer (Java 应用程序的指标检测库)公开指标。您可以将 Micrometer 与 Prometheus 搭配使用,以监控 button Timetable 应用程序中的 OptaPlanner solver。

先决条件

  • 您已创建 Spring Boot OptaPlanner 64)定程序应用程序。
  • 已安装 Prometheus。有关安装 Prometheus 的详情,请查看 Prometheus 网站。

流程

  1. 导航到 技术/java-spring-boot 目录。
  2. 将 Micrometer Prometheus 依赖项添加到 school timetable pom.xml 文件中:

    <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
     <groupId>io.micrometer</groupId>
     <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
  3. 在 application.properties 文件中添加以下属性:

    management.endpoints.web.exposure.include=metrics,prometheus
  4. 启动 button timetable 应用程序:

    mvn spring-boot:run
  5. 在 Web 浏览器中打开 http://localhost:8080/actuator/prometheus

第 13 章 红帽构建的 OptaPlanner 和 Java: a button Timetable Quickstart 指南

本指南指导您完成使用 OptaPlanner 的约束解决人工智能(AI)创建简单 Java 应用程序的过程。您将构建一种针对学生和教师优化中学时间的命令行应用程序:

...
INFO  Solving ended: time spent (5000), best score (0hard/9soft), ...
INFO
INFO  |            | Room A     | Room B     | Room C     |
INFO  |------------|------------|------------|------------|
INFO  | MON 08:30  | English    | Math       |            |
INFO  |            | I. Jones   | A. Turing  |            |
INFO  |            | 9th grade  | 10th grade |            |
INFO  |------------|------------|------------|------------|
INFO  | MON 09:30  | History    | Physics    |            |
INFO  |            | I. Jones   | M. Curie   |            |
INFO  |            | 9th grade  | 10th grade |            |
INFO  |------------|------------|------------|------------|
INFO  | MON 10:30  | History    | Physics    |            |
INFO  |            | I. Jones   | M. Curie   |            |
INFO  |            | 10th grade | 9th grade  |            |
INFO  |------------|------------|------------|------------|
...
INFO  |------------|------------|------------|------------|

您的应用程序将使用 AI 遵循硬和软调度 限制,将 Lesson 实例分配给 TimeslotRoom 实例,例如:

  • 房间最多可以有一节时间。
  • 教师可以指导大多数课程同时进行学习。
  • 学员最多可以同时参加一门课程。
  • 教师更喜欢在同一房间教授所有课程。
  • 教员更喜欢教授在课程之间的顺序课程和分解。
  • 同一主题上的学员不喜欢顺序课程。

以数学方式讲,中立时间是 NP-hard 问题。这意味着很难扩展。简而言之,对于一个非三维数据集,即使是超级计算机,即使是超级计算器,所有可能的组合都要花数年时间。需要的是,AI 约束解决者(如 OptaPlanner)具有在合理的时间内提供接近优化解决方案的高级算法。

先决条件

  • OpenJDK (JDK) 11 已安装。红帽构建的 Open JDK 可从红帽客户门户中的 Software Downloads 页面获得(需要登录)。
  • 已安装 Apache Maven 3.6 或更高版本。Maven 可从 Apache Maven Project 网站获取。
  • IDE,如 IntelliJ IDEA、VSCode 或 Eclipse

13.1. 创建 Maven 或 Gradle 构建文件并添加依赖项

您可以将 Maven 或 Gradle 用于 OptaPlanner button application。创建构建文件后,添加以下依赖项:

  • OptaPlanner-core (编译范围)来解决中计时问题
  • OptaPlanner-test (test scope) to JUnit 测试中立时间稳定限制
  • logback-classic (runtime scope)等实现,用于查看 OptaPlanner 采取的步骤

流程

  1. 创建 Maven 或 Gradle 构建文件。
  2. 在您的构建文件中添加 optaplanner-core、Soptaplanner-testlogback-classic 依赖项:

    • 对于 Maven,请在 pom.xml 文件中添加以下依赖项:

        <dependency>
          <groupId>org.optaplanner</groupId>
          <artifactId>optaplanner-core</artifactId>
        </dependency>
      
        <dependency>
          <groupId>org.optaplanner</groupId>
          <artifactId>optaplanner-test</artifactId>
          <scope>test</scope>
        </dependency>
      
        <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-classic</artifactId>
          <version>1.2.3</version>
        </dependency>

      以下示例显示了完整的 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>
      
          <version.org.optaplanner>8.11.1.Final-redhat-00006</version.org.optaplanner>
          <version.org.logback>1.2.3</version.org.logback>
      
          <version.compiler.plugin>3.8.1</version.compiler.plugin>
          <version.surefire.plugin>3.0.0-M5</version.surefire.plugin>
          <version.exec.plugin>3.0.0</version.exec.plugin>
        </properties>
      
        <dependencyManagement>
          <dependencies>
            <dependency>
              <groupId>org.optaplanner</groupId>
              <artifactId>optaplanner-bom</artifactId>
              <version>${version.org.optaplanner}</version>
              <type>pom</type>
              <scope>import</scope>
            </dependency>
            <dependency>
              <groupId>ch.qos.logback</groupId>
              <artifactId>logback-classic</artifactId>
              <version>${version.org.logback}</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>
              <artifactId>maven-compiler-plugin</artifactId>
              <version>${version.compiler.plugin}</version>
            </plugin>
            <plugin>
              <artifactId>maven-surefire-plugin</artifactId>
              <version>${version.surefire.plugin}</version>
            </plugin>
            <plugin>
              <groupId>org.codehaus.mojo</groupId>
              <artifactId>exec-maven-plugin</artifactId>
              <version>${version.exec.plugin}</version>
              <configuration>
                <mainClass>org.acme.schooltimetabling.TimeTableApp</mainClass>
              </configuration>
            </plugin>
          </plugins>
        </build>
      
        <repositories>
          <repository>
            <id>jboss-public-repository-group</id>
            <url>https://repository.jboss.org/nexus/content/groups/public/</url>
            <releases>
              <!-- Get releases only from Maven Central which is faster. -->
              <enabled>false</enabled>
            </releases>
            <snapshots>
              <enabled>true</enabled>
            </snapshots>
          </repository>
        </repositories>
      </project>
    • 对于 Gradle,将以下依赖项添加到 gradle.build 文件中:

      dependencies {
          implementation "org.optaplanner:optaplanner-core:${optaplannerVersion}"
          runtimeOnly "ch.qos.logback:logback-classic:${logbackVersion}"
      
          testImplementation "org.optaplanner:optaplanner-test:${optaplannerVersion}"
      }

      以下示例显示了已完成的 gradle.build 文件。

      plugins {
          id "java"
          id "application"
      }
      
      def optaplannerVersion = "{project-version}"
      def logbackVersion = "1.2.3"
      
      group = "org.acme"
      version = "0.1.0-SNAPSHOT"
      
      repositories {
          mavenCentral()
      }
      
      dependencies {
          implementation "org.optaplanner:optaplanner-core:${optaplannerVersion}"
          runtimeOnly "ch.qos.logback:logback-classic:${logbackVersion}"
      
          testImplementation "org.optaplanner:optaplanner-test:${optaplannerVersion}"
      }
      
      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"
          }
      }

13.2. 对域对象建模

红帽构建的 OptaPlanner timetable 项目旨在为每个时间插槽和房间分配。要做到这一点,添加三个类、TimeslotLessonRoom,如下图所示:

timeTableClassDiagramPure

Timeslot

Timeslot 类代表在教授课程时的时间间隔,例如: 星期一 10:30 - 11:30Tuesday 13:30 - 14:30。在这个示例中,所有时间插槽都具有相同的持续时间,在午餐或其他中断期间都没有时间插槽。

时间插槽没有日期,因为高校的计划仅每周重复。不需要 持续规划。一个时间被认为是个问题,因为在解决 过程中,实例不会发生改变。此类类不需要任何特定于 OptaPlanner 的注解。

room

Room 类代表教授课程的位置,例如 Room ARoom B。在这个示例中,所有房间都没有容量限制,它们可以适应所有课程。

空间 实例在解决过程中不会改变,因此 Room 也是 问题事实

courseon

在课程学习期间,教员将教授一门主题给一组学员,例如: A.Turing for 9th gradeChemistry,M.Curie 为 10th grade如果每周向同一学员组教授了多次主题,则有多个较少实例,只有 id 区分。例如,每周的第 9 个学分有 6 个学分。

在解决期间,OptaPlanner 会改变下课的计时和 房间 字段,以将每个上课时间分配到一个时间插槽和房间。 因为 OptaPlanner 更改了这些字段,所以 Lesson 是一个 计划实体

timeTableClassDiagramAnnotated

上图中的大多数字段都包含输入数据,但 orange 字段除外。在输入数据中未分配(空),并在输出数据中分配(非 null)字段的不计时和 房间 字段。在解决过程中,OptaPlanner 会更改这些字段。此类字段称为规划变量。为了使 OptaPlanner 能够识别它们,lot 和 room 字段都需要 @PlanningVariable 注解。它们包含类 Lesson,需要 @PlanningEntity 注释。

流程

  1. 创建 src/main/java/com/example/domain/Timeslot.java 类:

    package com.example.domain;
    
    import java.time.DayOfWeek;
    import java.time.LocalTime;
    
    public class Timeslot {
    
        private DayOfWeek dayOfWeek;
        private LocalTime startTime;
        private LocalTime endTime;
    
        private Timeslot() {
        }
    
        public Timeslot(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
            this.dayOfWeek = dayOfWeek;
            this.startTime = startTime;
            this.endTime = endTime;
        }
    
        @Override
        public String toString() {
            return dayOfWeek + " " + startTime.toString();
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public DayOfWeek getDayOfWeek() {
            return dayOfWeek;
        }
    
        public LocalTime getStartTime() {
            return startTime;
        }
    
        public LocalTime getEndTime() {
            return endTime;
        }
    
    }

    注意 toString () 方法保持输出短,以便更轻松地读取 OptaPlanner 的 DEBUGTRACE 日志,如后文所示。

  2. 创建 src/main/java/com/example/domain/Room.java 类:

    package com.example.domain;
    
    public class Room {
    
        private String name;
    
        private Room() {
        }
    
        public Room(String name) {
            this.name = name;
        }
    
        @Override
        public String toString() {
            return name;
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        public String getName() {
            return name;
        }
    
    }
  3. 创建 src/main/java/com/example/domain/Lesson.java 类:

    package com.example.domain;
    
    import org.optaplanner.core.api.domain.entity.PlanningEntity;
    import org.optaplanner.core.api.domain.variable.PlanningVariable;
    
    @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() {
        }
    
        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() {
            return subject + "(" + id + ")";
        }
    
        // ********************************
        // Getters and setters
        // ********************************
    
        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;
        }
    
    }

    Lesson 类具有 @PlanningEntity 注释,因此 OptaPlanner 知道该类在解决问题过程中发生了变化,因为它包含一个或多个规划变量。

    timeslot 字段具有 @PlanningVariable 注释,因此 OptaPlanner 知道它可以更改其值。要查找分配给此字段的潜在 Timeslot 实例,OptaPlanner 使用 valueRangeProviderRefs 属性连接到提供 List<Timeslot > 来选择的值范围供应商。有关值范围供应商的信息,请参阅 第 13.4 节 “在规划解决方案中收集域对象”

    room 字段也具有 @PlanningVariable 注释,理由相同。

13.3. 定义限制并计算分数

在解决问题时,分数 代表特定解决方案的质量。分数越大。红帽构建的 OptaPlanner 会寻找最佳解决方案,这是在可用时间内获得最高分数的解决方案。它可能 是最佳解决方案

因为 timetable 示例用例有硬和软限制,所以使用 HardSoftScore 类来代表分数:

  • 硬限制不能中断。例如: 一个房间最多可以有一课时间。
  • 软限制不应中断。例如 :教员更倾向于在单一房间进行学习。

硬限制会根据其他硬限制加权。软限制会根据其他软限制加权。硬限制总是大于软限制,无论它们的权重是什么。

要计算分数,您可以实施 EasyScoreCalculator 类:

public class TimeTableEasyScoreCalculator implements EasyScoreCalculator<TimeTable> {

    @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 "complete" implementation
        return HardSoftScore.of(hardScore, softScore);
    }

}

不幸的是,这个解决方案无法很好地扩展,因为它没有递增:每次课程被分配到不同的时间插槽或房间,所有课程均会重新评估以计算新分数。

更好的解决方案是创建一个 src/main/java/com/example/solver/TimeTableConstraintProvider.java 类,以执行增量分数计算。这个类使用 OptaPlanner 的 ConstraintStream API,它被 Java 8 Streams 和 SQL 实现。ConstraintProvider 扩展一个比 EasyScoreCalculator:O(n)而不是 O(n)的 magnitude 顺序。

流程

创建以下 src/main/java/com/example/solver/TimeTableConstraintProvider.java 类:

package com.example.solver;

import com.example.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 "complete" implementation
        };
    }

    private Constraint roomConflict(ConstraintFactory constraintFactory) {
        // A room can accommodate at most one lesson at the same time.

        // Select a lesson ...
        return constraintFactory.from(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.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 attend at most one lesson at the same 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);
    }

}

13.4. 在规划解决方案中收集域对象

TimeTable 实例将单个数据集的所有 TimeslotRoomLesson 实例包装。此外,由于它包含所有经验,因此每个均具有特定的计划变量状态,因此它是一个 规划解决方案,它分数如下:

  • 如果尚未取消分配课程,则它是一个 未初始化的 解决方案,例如,分数为 -4init/0hard/0soft 的解决方案。
  • 如果它破坏了硬约束,则它是一个不可分的解决方案,例如,分数为 -2hard/-3soft 的解决方案。
  • 如果它遵循所有硬约束,则它是一个可行的解决方案,例如,分数为 0hard/-7soft 的解决方案。

TimeTable 类具有 @PlanningSolution 注释,因此红帽构建的 OptaPlanner 知道此类包含所有输入和输出数据。

具体来说,这个类是问题的输入:

  • 一个 timeslotList 字段以及所有时间插槽

    • 这是问题事实的列表,因为它们在解决过程中不会改变。
  • 带有所有空间的 roomList 字段

    • 这是问题事实的列表,因为它们在解决过程中不会改变。
  • 包含所有课程的 lessonList 字段

    • 这是计划实体列表,因为它们在解决过程中发生了变化。
    • 在每个课

      • timeslotroom 字段的值通常仍为 null,因此未分配。它们正在规划变量。
      • 其他字段(如 主题教师和 学员 )填写完毕。这些字段是问题属性。

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

  • 一个 lessonList 字段,每个 Lesson 实例在解决后 都带有非空 次数和 房间 字段
  • 代表输出解决方案的质量的 score 字段,如 0hard/-5soft

流程

创建 src/main/java/com/example/domain/TimeTable.java 类:

package com.example.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;

    private TimeTable() {
    }

    public TimeTable(List<Timeslot> timeslotList, List<Room> roomList,
            List<Lesson> lessonList) {
        this.timeslotList = timeslotList;
        this.roomList = roomList;
        this.lessonList = lessonList;
    }

    // ********************************
    // Getters and setters
    // ********************************

    public List<Timeslot> getTimeslotList() {
        return timeslotList;
    }

    public List<Room> getRoomList() {
        return roomList;
    }

    public List<Lesson> getLessonList() {
        return lessonList;
    }

    public HardSoftScore getScore() {
        return score;
    }

}

值范围供应商

timeslotList 字段是一个值范围 provider。它包含 OptaPlanner 可以从中选择的 Timeslot 实例,以分配给 Lesson 实例的 timeslot 字段。timeslotList 字段具有一个 @ValueRangeProvider 注释,用于连接这两者,方法是将 idLesson 中的 @PlanningVariablevalueRangeProviderRefs 匹配。

遵循同一逻辑时,roomList 字段也具有 @ValueRangeProvider 注释。

问题事实和规划实体属性

另外,OptaPlanner 需要知道哪个 Lesson 实例可以更改,以及如何检索由 TimeTableConstraintProvider 分数计算使用的 TimeslotRoom 实例。

timeslotListroomList 字段具有 @ProblemFactCollectionProperty 注释,因此您的 TimeTableConstraintProvider 可以从这些实例中进行选择。

lessonList 具有 @PlanningEntityCollectionProperty 注释,因此 OptaPlanner 可以在解决问题期间更改它们,并且您的 TimeTableConstraintProvider 也可以从这些注释中选择。

13.5. TimeTableApp.java 类

创建了 school timetable 应用程序的所有组件后,您将在 TimeTableApp.java 类中将它们全部放在一起。

main () 方法执行以下任务:

  1. 创建 Solver Factory 以为每个数据集构建 Solver。
  2. 加载数据集。
  3. 通过 Solv er.solve ()解决此问题
  4. 视觉化该数据集的解决方案。

通常,应用程序有一个 SolverFactory 来为要解决的每个问题数据构建一个新的 Solver 实例。SolverFactory 是 thread-safe,但 Solver 并不是。对于中学时间的应用程序,只有一个数据集,因此只有一个 Solver 实例。

以下是已完成的 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 data set.
                // 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 data set.
        // It's recommended to run for at least 5 minutes ("5m") otherwise.
        .withTerminationSpentLimit(Duration.ofSeconds(5)));

SolverFactory 创建注册 @PlanningSolution 类、@PlanningEntity 类和 ConstraintProvider 类(您之前创建的所有对象)。

如果没有终止设置或 terminationEarly () 事件,则解决者会永久运行。为避免这种情况,解决者可将解决时间限制为 5 秒。

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 () 方法不会立即返回。在返回最佳解决方案前,它会运行 5 秒。

OptaPlanner 返回可用终止时间中找到的最佳解决方案。由于 NP 硬问题的性质,最佳解决方案可能不是最佳,特别是对于较大的数据集。增加终止时间,以发现更好的解决方案。

generateDemoData () 方法生成可计时问题来解决此问题。

printTimetable () 方法对控制台而言是非常好的打印,因此可以轻松地确定是否是很好的调度。

13.6. 创建并运行 National timetable 应用程序

既然您已完成了本校紧张 Java 应用程序的所有组件,现在您已准备好将它们全部放在 TimeTableApp.java 类中,并运行它。

先决条件

  • 您已创建了 ute Timetable 应用程序所需的所有组件。

流程

  1. 创建 src/main/java/org/acme/amazontimetabling/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 data set.
                    // 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());
                }
            }
        }
    
    }
  2. 运行 TimeTableApp 类作为普通 Java 应用程序的主类。以下输出应结果:

    ...
    INFO  |            | Room A     | Room B     | Room C     |
    INFO  |------------|------------|------------|------------|
    INFO  | MON 08:30  | English    | Math       |            |
    INFO  |            | I. Jones   | A. Turing  |            |
    INFO  |            | 9th grade  | 10th grade |            |
    INFO  |------------|------------|------------|------------|
    INFO  | MON 09:30  | History    | Physics    |            |
    INFO  |            | I. Jones   | M. Curie   |            |
    INFO  |            | 9th grade  | 10th grade |            |
    ...
  3. 验证控制台输出。它是否符合所有硬约束?如果您在 TimeTableConstraintProvider 中注释掉 roomConflict 约束,会出现什么情况?

info 日志显示 OptaPlanner 在 5 秒内执行的操作:

... 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).

13.7. 测试应用

良好的应用程序包括测试覆盖。测试时间表项目中的限制和 solver。

13.7.1. 测试 school 计时的限制

要以隔离方式测试每个 timetable 项目约束,请在单元测试中使用 ConstraintVerifier。这会测试每个约束与其他测试隔离的情况,这会在添加具有适当测试覆盖的新约束时降低维护。

此测试会验证约束 TimeTableConstraintProvider::roomConflict,当同一间有 3 个经验时,两个经验都有相同的 timeslot,被 penalizes 为 1。因此,如果约束权重为 10hard,它将分数降低为 -10hard

流程

创建 src/test/java/org/acme/optaplanner/solver/TimeTableConstraintProviderTest.java 类:

package org.acme.optaplanner.solver;

import java.time.DayOfWeek;
import java.time.LocalTime;

import javax.inject.Inject;

import io.quarkus.test.junit.QuarkusTest;
import org.acme.optaplanner.domain.Lesson;
import org.acme.optaplanner.domain.Room;
import org.acme.optaplanner.domain.TimeTable;
import org.acme.optaplanner.domain.Timeslot;
import org.junit.jupiter.api.Test;
import org.optaplanner.test.api.score.stream.ConstraintVerifier;

@QuarkusTest
class TimeTableConstraintProviderTest {

    private static final Room ROOM = new Room("Room1");
    private static final Timeslot TIMESLOT1 = new Timeslot(DayOfWeek.MONDAY, LocalTime.of(9,0), LocalTime.NOON);
    private static final Timeslot TIMESLOT2 = new Timeslot(DayOfWeek.TUESDAY, LocalTime.of(9,0), LocalTime.NOON);

    @Inject
    ConstraintVerifier<TimeTableConstraintProvider, TimeTable> constraintVerifier;

    @Test
    void roomConflict() {
        Lesson firstLesson = new Lesson(1, "Subject1", "Teacher1", "Group1");
        Lesson conflictingLesson = new Lesson(2, "Subject2", "Teacher2", "Group2");
        Lesson nonConflictingLesson = new Lesson(3, "Subject3", "Teacher3", "Group3");

        firstLesson.setRoom(ROOM);
        firstLesson.setTimeslot(TIMESLOT1);

        conflictingLesson.setRoom(ROOM);
        conflictingLesson.setTimeslot(TIMESLOT1);

        nonConflictingLesson.setRoom(ROOM);
        nonConflictingLesson.setTimeslot(TIMESLOT2);

        constraintVerifier.verifyThat(TimeTableConstraintProvider::roomConflict)
                .given(firstLesson, conflictingLesson, nonConflictingLesson)
                .penalizesBy(1);
    }

}

请注意,ConstraintVerifier 在测试过程中如何忽略约束权重,即使这些约束权重在 ConstraintProvider 中被硬编码。这是因为在进入生产环境前,会定期更改约束权重。这样,约束的权重调整不会破坏单元测试。

13.7.2. 测试 school timetable solver

这个示例测试红帽在 Red Hat build of Quarkus 上构建的 OptaPlanner National Timetable 项目。它使用 JUnit 测试来生成测试数据集,并将其发送到 TimeTableController 来解决。

流程

  1. 使用以下内容创建 src/test/java/com/example/rest/TimeTableResourceTest.java 类:

    package com.exmaple.optaplanner.rest;
    
    import java.time.DayOfWeek;
    import java.time.LocalTime;
    import java.util.ArrayList;
    import java.util.List;
    
    import javax.inject.Inject;
    
    import io.quarkus.test.junit.QuarkusTest;
    import com.exmaple.optaplanner.domain.Room;
    import com.exmaple.optaplanner.domain.Timeslot;
    import com.exmaple.optaplanner.domain.Lesson;
    import com.exmaple.optaplanner.domain.TimeTable;
    import com.exmaple.optaplanner.rest.TimeTableResource;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.Timeout;
    
    import static org.junit.jupiter.api.Assertions.assertFalse;
    import static org.junit.jupiter.api.Assertions.assertNotNull;
    import static org.junit.jupiter.api.Assertions.assertTrue;
    
    @QuarkusTest
    public class TimeTableResourceTest {
    
        @Inject
        TimeTableResource timeTableResource;
    
        @Test
        @Timeout(600_000)
        public void solve() {
            TimeTable problem = generateProblem();
            TimeTable solution = timeTableResource.solve(problem);
            assertFalse(solution.getLessonList().isEmpty());
            for (Lesson lesson : solution.getLessonList()) {
                assertNotNull(lesson.getTimeslot());
                assertNotNull(lesson.getRoom());
            }
            assertTrue(solution.getScore().isFeasible());
        }
    
        private TimeTable generateProblem() {
            List<Timeslot> timeslotList = new ArrayList<>();
            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)));
    
            List<Room> roomList = new ArrayList<>();
            roomList.add(new Room("Room A"));
            roomList.add(new Room("Room B"));
            roomList.add(new Room("Room C"));
    
            List<Lesson> lessonList = new ArrayList<>();
            lessonList.add(new Lesson(101L, "Math", "B. May", "9th grade"));
            lessonList.add(new Lesson(102L, "Physics", "M. Curie", "9th grade"));
            lessonList.add(new Lesson(103L, "Geography", "M. Polo", "9th grade"));
            lessonList.add(new Lesson(104L, "English", "I. Jones", "9th grade"));
            lessonList.add(new Lesson(105L, "Spanish", "P. Cruz", "9th grade"));
    
            lessonList.add(new Lesson(201L, "Math", "B. May", "10th grade"));
            lessonList.add(new Lesson(202L, "Chemistry", "M. Curie", "10th grade"));
            lessonList.add(new Lesson(203L, "History", "I. Jones", "10th grade"));
            lessonList.add(new Lesson(204L, "English", "P. Cruz", "10th grade"));
            lessonList.add(new Lesson(205L, "French", "M. Curie", "10th grade"));
            return new TimeTable(timeslotList, roomList, lessonList);
        }
    
    }

    此测试会验证在解决后,所有课程都会被分配给一个时间插槽和房间。它还会验证它是否找到了可行的解决方案(没有硬约束问题)。

  2. 将 test 属性添加到 src/main/resources/application.properties 文件中:

    # The solver runs only for 5 seconds to avoid a HTTP timeout in this simple implementation.
    # It's recommended to run for at least 5 minutes ("5m") otherwise.
    quarkus.optaplanner.solver.termination.spent-limit=5s
    
    # Effectively disable this termination in favor of the best-score-limit
    %test.quarkus.optaplanner.solver.termination.spent-limit=1h
    %test.quarkus.optaplanner.solver.termination.best-score-limit=0hard/*soft

通常,解决者在 200 毫秒内找到可行的解决方案。注意 application.properties 文件如何在测试过程中覆盖 solver 终止,以便在找到可行的解决方案 (0hard86_64) soft) 后马上终止。这可避免对解决者进行硬编码,因为单元测试可能会在任意硬件上运行。这种方法可确保测试运行足够长,以查找可行的解决方案,即使在较慢的系统中。但它不会严格运行比严格运行的时间更长,即使在快速系统上也是如此。

13.8. 日志记录

完成红帽构建的 OptaPlanner school Timetable 项目后,您可以使用日志信息来帮助微调 ConstraintProvider 中的限制。查看 info 日志文件中的分数计算速度,以评估更改对您的限制的影响。以 debug 模式运行应用程序,以显示应用程序接受的每个步骤或使用 trace 日志记录来记录每个步骤和每次移动。

流程

  1. 为固定时间(如五分钟)运行 button Timetable 应用程序。
  2. 查看日志文件中的分数计算速度,如下例所示:

    ... Solving ended: ..., score calculation speed (29455/sec), ...
  3. 更改约束,再次运行计划应用程序以获得相同时间,并查看日志文件中记录的分数计算速度。
  4. 以 debug 模式运行应用程序,记录应用程序所做的每个步骤:

    • 要从命令行运行调试模式,请使用 -D 系统属性。
    • 要永久启用调试模式,请在 application.properties 文件中添加以下行:

      quarkus.log.category."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}]).
      ...
  5. 使用 trace logging 来显示每个步骤以及每个步骤的每个移动。

13.9. 使用 Micrometer 和 Prometheus 来监控您的协调程序 OptaPlanner Java 应用程序

OptaPlanner 通过 Micrometer (Java 应用程序的指标检测库)公开指标。您可以将 Micrometer 与 Prometheus 搭配使用,以监控 button Timetable 应用程序中的 OptaPlanner solver。

先决条件

  • 您已经使用 Java 创建了 OptaPlanner 和应用。
  • 已安装 Prometheus。有关安装 Prometheus 的详情,请查看 Prometheus 网站。

流程

  1. 将 Micrometer Prometheus 依赖项添加到 series timetable pom.xml 文件中,其中 & lt;MICROMETER_VERSION > 是您安装的 Micrometer 版本:

    <dependency>
     <groupId>io.micrometer</groupId>
     <artifactId>micrometer-registry-prometheus</artifactId>
     <version><MICROMETER_VERSION></version>
    </dependency>
    注意

    还需要 micrometer-core 依赖项。但是,这个依赖项包含在 optaplanner-core 依赖项中,因此您不需要将其添加到 pom.xml 文件中。

  2. 将以下导入语句添加到 TimeTableApp.java 类中。

    import io.micrometer.core.instrument.Metrics;
    import io.micrometer.prometheus.PrometheusConfig;
    import io.micrometer.prometheus.PrometheusMeterRegistry;
  3. 将以下行添加到 TimeTableApp.java 类的主要方法的顶部,以便 Prometheus 可以在解决方案开始前从 com.sun.net.httpserver.HttpServer 中获取数据:

    PrometheusMeterRegistry prometheusRegistry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
    
            try {
                HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
                server.createContext("/prometheus", httpExchange -> {
                    String response = prometheusRegistry.scrape();
                    httpExchange.sendResponseHeaders(200, response.getBytes().length);
                    try (OutputStream os = httpExchange.getResponseBody()) {
                        os.write(response.getBytes());
                    }
                });
    
                new Thread(server::start).start();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
    
            Metrics.addRegistry(prometheusRegistry);
    
            solve();
        }
  4. 添加以下行以控制问题解决时间。通过调整解决时间,您可以看到指标根据解决所花费的时间变化。

    withTerminationSpentLimit(Duration.ofMinutes(5)));
  5. 启动 established Timetable 应用程序。
  6. 在 Web 浏览器中打开 http://localhost:8080/prometheus,以查看 Prometheus 中的可计时应用程序。
  7. 打开监控系统,以查看 OptaPlanner 项目的指标。

    公开以下指标:

    • optaplanner_solver_errors_total :从测量开始解决时发生的错误总数。
    • optaplanner_solver_solve_duration_seconds_active_count :当前正在解决的解决者数量。
    • optaplanner_solver_solve_duration_seconds_max: 运行当前活跃解决器的最长时间。
    • optaplanner_solver_solve_duration_seconds_duration_sum :每个活跃解决者的 solve 持续时间的总和。例如,如果存在两个活动解决者,一个运行三分钟,另一个运行一分钟,则总解决时间为四分钟。

部分 V. 红帽构建的 OptaPlanner 启动程序应用程序

红帽构建的 OptaPlanner 提供了以下启动程序应用程序,您可以在 Red Hat OpenShift Container Platform 上部署开箱即用的应用程序:

  • 员工启动程序应用程序
  • Vechile Route planning starter 应用程序

OptaPlanner 启动程序应用程序比示例和快速启动指南更开发。他们专注于特定用例,并使用可用于构建规划解决方案的最佳技术。

第 14 章 在 IDE 中使用红帽构建的 OptaPlanner:员工名列示例

作为业务规则开发人员,您可以使用 IDE 构建、运行和修改使用红帽构建的 OptaPlanner 功能的 optaweb-employee-rostering 初学者应用程序。

先决条件

  • 您可以使用集成的开发环境,如 Red Hat CodeReady Studio 或 IntelliJ IDEA。
  • 您已了解 Java 语言。
  • 您已了解 React 和 TypeScript。开发 OptaWeb UI 需要此要求。

14.1. 员工入门应用程序概述

员工漫长的入门应用程序为员工分配在组织中各种位置的转变。例如,您可以使用应用程序在国内、保护在多个位置间转移转移,或在工作程序之间的配配线上切换。

最佳员工必须考虑多个变量。例如,不同位置中的改变需要不同的技能。另外,一些员工可能对一些时间插槽不可用,或者可能选择特定的时间插槽。此外,员工可以拥有合同,限制员工在一个时间段内工作的小时数。

对于这个初学者应用程序,红帽构建的 OptaPlanner 规则都使用硬和软限制。在优化过程中,计划引擎可能无法违反硬约束,例如,如果员工不可用(out sick),或者员工无法在单个变化中工作两个点。计划引擎会尝试遵循软限制,如员工优先处理特定变化,但如果最佳解决方案需要,可以违反它们。

14.2. 构建并运行员工漫长入门应用程序

您可以从源代码构建员工 rostering 初学者应用,并将其作为 JAR 文件运行。

或者,您可以使用您的 IDE (包括 Red Hat CodeReady Studio)来构建和运行应用程序。

14.2.1. 准备部署文件

在构建和部署应用程序前,您必须下载并准备部署文件。

流程

  1. 导航到红帽客户门户网站中的 Software Downloads 页面(需要登录),然后从下拉菜单中选择产品和版本:

    • 产品:流程自动化管理器
    • Version: 7.12
  2. 下载 Red Hat Process Automation Manager 7.12.0 Kogito 和 OptaPlanner 8 Decision Services Quickstarts (rhpam-7.12.0-kogito-and-optaplanner-quickstarts.zip)。
  3. 提取 rhpam-7.12.0-kogito-and-optaplanner-quickstarts.zip 文件。
  4. 下载 Red Hat Process Automation Manager 7.12 Maven Repository Kogito 和 OptaPlanner 8 Maven Repository (rhpam-7.12.0-kogito-maven-repository.zip)。
  5. 提取 rhpam-7.12.0-kogito-maven-repository.zip 文件。
  6. rhpam-7.12.0-kogito-maven-repository/maven-repository 子目录的内容复制到 ~/.m2/repository 目录中。
  7. 导航到 optaweb-8.11.1.Final-redhat-00006/optaweb-employee-rostering 目录。此文件夹是本文档的后续部分中的基本文件夹。

    注意

    文件和文件夹名称可能比本文档中具体说明更高的版本号。

14.2.2. 运行 Employee Rostering starter 应用 JAR 文件

您可以从 红帽流程自动化管理器 7.12.0 Kogito 和 OptaPlanner 8 Decision Services Quickstarts 中包含的 JAR 文件运行 Employee Ro stering Starter 应用程序。

先决条件

  • 您已下载并提取 rhpam-7.12.0-kogito-and-optaplanner-quickstarts.zip 文件,如 第 14.2.1 节 “准备部署文件” 所述。
  • 安装了 Java 开发套件。
  • 已安装 Maven。
  • 主机可以访问互联网。构建过程使用互联网从外部存储库下载 Maven 软件包。

流程

  1. 在命令终端中,切换到 rhpam-7.12.0-kogito-and-optaplanner-quickstarts/optaweb-8.11.1.Final-redhat-00006/optaweb-employee-rostering 目录。
  2. 使用以下命令:

    mvn clean install -DskipTests
  3. 等待构建过程完成。
  4. 导航到 rhpam-7.12.0-kogito-and-optaplanner-quickstarts/optaweb-8.11.1.Final-redhat-00006/optaweb-employee-rostering/optaweb-employee-rostering-standalone/target 目录。
  5. 输入以下命令运行 Employee Rostering JAR 文件:

    java -jar quarkus-app/quarkus-run.jar
    注意

    在构建时,quarkus.datasource.db-kind 参数的值默认设置为 H2。要使用其他数据库,您必须重建 standalone 模块,并在命令行中指定数据库类型。例如,要使用 PostgreSQL 数据库,请输入以下命令:

    mvn clean install -DskipTests -Dquarkus.profile=postgres

  6. 要访问应用程序,在 Web 浏览器中输入 http://localhost:8080/

14.2.3. 使用 Maven 构建并运行 Employee Rostering 初学者应用程序

您可以使用命令行来构建和运行员工漫长的初学者应用程序。

如果您使用这个步骤,数据存储在内存中,并在服务器停止时丢失。要使用持久性存储的数据库服务器构建并运行应用程序,请参阅 第 14.2.4 节 “从命令行使用持久数据存储构建并运行员工 rostering 初学者应用程序”

先决条件

  • 您已准备好部署文件,如 第 14.2.1 节 “准备部署文件” 所述。
  • 安装了 Java 开发套件。
  • 已安装 Maven。
  • 主机可以访问互联网。构建过程使用互联网从外部存储库下载 Maven 软件包。

流程

  1. 导航到 optaweb-employee-rostering-backend 目录。
  2. 使用以下命令:

    mvn quarkus:dev
  3. 导航到 optaweb-employee-rostering-frontend 目录。
  4. 使用以下命令:

    npm start
    注意

    如果您使用 npm 启动服务器,npm 会监控代码更改。

  5. 要访问应用程序,在 Web 浏览器中输入 http://localhost:3000/

14.2.4. 从命令行使用持久数据存储构建并运行员工 rostering 初学者应用程序

如果您使用命令行构建员工 rostering 启动程序应用程序并运行它,您可以为持久数据存储提供数据库服务器。

先决条件

  • 您已准备好部署文件,如 第 14.2.1 节 “准备部署文件” 所述。
  • 安装了 Java 开发套件。
  • 已安装 Maven。
  • 主机可以访问互联网。构建过程使用互联网从外部存储库下载 Maven 软件包。
  • 您已部署了 MySQL 或 PostrgeSQL 数据库服务器。

流程

  1. 在命令终端中,导航到 optaweb-employee-rostering-standalone/target 目录。
  2. 输入以下命令运行 Employee Rostering JAR 文件:

    java \
    -Dquarkus.datasource.username=<DATABASE_USER> \
    -Dquarkus.datasource.password=<DATABASE_PASSWORD> \
    -Dquarkus.datasource.jdbc.url=<DATABASE_URL> \
    -jar quarkus-app/quarkus-run.jar

    在本例中,替换以下占位符:

    • <DATABASE_URL > :用于连接数据库的 URL
    • <DATABASE_USER > :连接到数据库的用户
    • <DATABASE_PASSWORD& gt; :< DATABASE_USER>的密码
注意

在构建时,quarkus.datasource.db-kind 参数的值默认设置为 H2。要使用其他数据库,您必须重建 standalone 模块,并在命令行中指定数据库类型。例如,要使用 PostgreSQL 数据库,请输入以下命令:

mvn clean install -DskipTests -Dquarkus.profile=postgres

14.2.5. 使用 IntelliJ IDEA 构建并运行员工 rostering 初学者应用程序

您可以使用 IntelliJ IDEA 来构建和运行员工的初学者应用程序。

先决条件

  • 您已下载了 Employee Rostering 源代码,它可从 Employee Rostering GitHub 页面获得。
  • 已安装 IntelliJ IDEA、Maven 和 Node.js。
  • 主机可以访问互联网。构建过程使用互联网从外部存储库下载 Maven 软件包。

流程

  1. 启动 IntelliJ IDEA。
  2. 在 IntelliJ IDEA 主菜单中选择 FileOpen
  3. 选择应用程序源的根目录,再单击 OK
  4. 在主菜单中选择 RunEdit Configurations
  5. 在显示的窗口中,展开 Templates 并选择 Maven。此时会出现 Maven 侧边栏。
  6. 在 Maven 边栏中,从 Working Directory 菜单中选择 optaweb-employee-rostering-backend
  7. 在命令行中 ,输入 mvn quarkus:dev
  8. 要启动后端,请单击 OK
  9. 在命令终端中,导航到 optaweb-employee-rostering-frontend 目录。
  10. 输入以下命令启动前端:

    npm start
  11. 要访问应用程序,在 Web 浏览器中输入 http://localhost:3000/

14.3. 员工启动程序应用程序的源代码概述

员工的初学者应用程序由以下主要组件组成:

  • 使用红帽构建的 OptaPlanner 并提供 REST API 实现 rostering 逻辑 的后端
  • 使用 React 实现用户界面的 frontend 模块,并通过 REST API 与 backend 模块交互

您可以独立构建并使用这些组件。特别是,您可以实施不同的用户界面,并使用 REST API 来调用服务器。

除了两个主要组件外,员工名列模板还包含随机源数据的生成器(用于演示和测试目的)和基准测试应用。

模块和密钥类

employee rostering 模板的 Java 源代码包含多个 Maven 模块。这些模块各自包含一个单独的 Maven 项目文件(pom.xml),但它们设计为在共同项目中构建。

模块包含多个文件,包括 Java 类。本文档列出所有模块,以及包含员工电动计算密钥信息的类和其他文件。

  • optaweb-employee-rostering-benchmark 模块:包含生成随机数据和基准测试解决方案的附加应用程序。
  • optaweb-employee-rostering-distribution 模块:包含 README 文件。
  • optaweb-employee-rostering-docs 模块:包含文档文件。
  • optaweb-employee-rostering-frontend 模块:包含带有用户界面的客户端应用,在 React 中开发。
  • optaweb-employee-rostering-backend 模块:包含使用 OptaPlanner 执行漫长计算的服务器应用程序。

    • src/main/java/org.optaweb.employeerostering.service.rosterGenerator.java: 生成随机输入数据,用于演示和测试目的。如果您更改了所需的输入数据,请相应地更改生成器。
    • src/main/java/org.optaweb.employeerostering.domain.employee/EmployeeAvailability.java: 定义员工的可用性信息。每次插槽中,员工都可以不可用、可用或时间插槽指定给员工的首选时间插槽。
    • src/main/java/org.optaweb.employeerostering.domain.employee/Employee.java: 定义员工。员工具有名称、技能列表,并在合同下工作。技能由技能对象表示。
    • src/main/java/org.optaweb.employeerostering.domain.roster/Roster.java: 定义计算的漫长信息。
    • src/main/java/org.optaweb.employeerostering.domain.shift/Shift.java: 定义可分配给员工的转换。一个变化是由一个时间插槽和位置定义的。例如,在大约 2 月 20 日 8AM-4PM 时间插槽的 Kitchen spot 中可能会有变化。可以为特定位置和时间插槽定义多个变化。在这种情况下,这个地点和时间插槽需要多个员工。
    • src/main/java/org.optaweb.employeerostering.domain.skill/Skill.java: 定义员工具有的专业技能。
    • src/main/java/org.optaweb.employeerostering.domain.spot/Spot.java: 定义可以放置员工的亮点。例如,S Kitchen 可以是 spot。
    • src/main/java/org.optaweb.employeerostering.domain.contract/Contract.java :定义一个合同,为员工在不同时间段内设置限制。
    • src/main/java/org.optaweb.employeerostering.domain.tenant/Tenant.java: 定义租户。每个租户代表一组独立的数据。一个租户的数据更改不会影响任何其他租户。
    • *view.java: 与域对象相关的类定义从其他信息计算的值集;客户端应用程序可以通过 REST API 读取这些值,但不写入它们。
    • *service.java: Interfaces 位于定义 REST API 的服务软件包中。服务器和客户端应用都单独定义这些接口的实施。
  • optaweb-employee-rostering-standalone 模块:包含独立应用的 assembly 配置。

14.4. 修改员工漫长的入门应用程序

要修改员工漫长的初学者应用程序,您必须更改管理优化过程的规则。您还必须确保数据结构包含所需的数据,并为规则提供所需的计算。如果用户界面中没有所需的数据,还必须修改用户界面。

以下流程概述了修改员工漫长的入门应用程序的一般方法。

先决条件

  • 您有一个成功构建应用的构建环境。
  • 您可以读取和修改 Java 代码。

流程

  1. 规划所需的更改。回答以下问题:

    • 还需要 避免哪些额外场景?这些场景是 硬限制
    • 优化器必须尽量避免的额外场景是什么?这些场景是 软限制
    • 如果可能的解决方案中发生每个场景,则需要计算什么数据?
    • 哪些数据可以从用户在现有版本中输入的信息衍生?
    • 哪些数据可以被硬编码?
    • 用户必须输入哪些数据,且未在当前版本中输入?
  2. 如果任何所需的数据可以从当前数据中计算,或者可以硬编码,请将计算或硬编码添加到现有的视图或实用程序类中。如果数据必须在服务器端计算,请添加 REST API 端点来读取它。
  3. 如果用户必须输入任何所需的数据,请将数据添加到代表数据实体(如 Employee 类)的类中,添加 REST API 端点来读取和写入数据,并修改用户界面以输入数据。
  4. 当所有数据都可用时,修改规则。对于大多数修改,您必须添加一个新规则。规则位于 optaweb-employee-rostering-backend 模块的 src/main/java/org/optaweb/employeerostering/service/solver/Employee-java 文件中。
  5. 修改应用程序后,构建并运行它。

第 15 章 在 Red Hat OpenShift Container Platform 中部署和使用红帽构建的 OptaPlanner:一个员工的入门示例

作为业务规则开发人员,您可以通过快速部署将 optaweb-employee-rostering Starter 项目包括在 Red Hat Process Automation Manager distribution to OpenShift 来测试和与红帽构建的 OptaPlanner 功能进行交互。

先决条件

  • 您可以访问已部署的 OpenShift 环境。详情请参阅您使用的 OpenShift 产品的文档。

15.1. 员工入门应用程序概述

员工漫长的入门应用程序为员工分配在组织中各种位置的转变。例如,您可以使用应用程序在国内、保护在多个位置间转移转移,或在工作程序之间的配配线上切换。

最佳员工必须考虑多个变量。例如,不同位置中的改变需要不同的技能。另外,一些员工可能对一些时间插槽不可用,或者可能选择特定的时间插槽。此外,员工可以拥有合同,限制员工在一个时间段内工作的小时数。

对于这个初学者应用程序,红帽构建的 OptaPlanner 规则都使用硬和软限制。在优化过程中,计划引擎可能无法违反硬约束,例如,如果员工不可用(out sick),或者员工无法在单个变化中工作两个点。计划引擎会尝试遵循软限制,如员工优先处理特定变化,但如果最佳解决方案需要,可以违反它们。

15.2. 在 OpenShift 上安装并启动员工漫长的初学者应用程序

使用 runOnOpenShift.sh 脚本将 Employee Rostering 初学者应用程序部署到 Red Hat OpenShift Container Platform。runOnOpenShift.sh shell 脚本可在 Red Hat Process Automation Manager 7.12.0 Kogito 和 OptaPlanner 8 Decision Services Quickstarts 发行版中找到。

runOnOpenShift.sh 脚本在本地构建并打包应用源代码,并将它上传到 OpenShift 环境以进行部署。此方法需要 Java Development Kit、Apache Maven 和 bash shell 命令行。

15.2.1. 使用提供的脚本部署应用程序

您可以使用提供的脚本将 Employee Rostering Starter 应用程序部署到 Red Hat OpenShift Container Platform。该脚本在本地构建并打包应用源代码,并将它上传到 OpenShift 环境以进行部署。

先决条件

  • 使用 oc 命令行工具登录到目标 OpenShift 环境。有关此工具的更多信息,请参阅 OpenShift Container Platform CLI 参考
  • OpenJDK 11 或更高版本已安装。红帽构建的 Open JDK 可从红帽客户门户中的 Software Downloads 页面获得(需要登录)。
  • 已安装 Apache Maven 3.6 或更高版本。Maven 可从 Apache Maven Project 网站获取。
  • 您的本地系统中提供了 bash shell 环境。

流程

  1. 导航到红帽客户门户网站中的 Software Downloads 页面(需要登录),然后从下拉菜单中选择产品和版本:

    • 产品:流程自动化管理器
    • Version: 7.12.0
  2. 下载 Red Hat Process Automation Manager 7.12 Maven Repository Kogito 和 OptaPlanner 8 Maven Repository (rhpam-7.12.0-kogito-maven-repository.zip)文件。
  3. 提取 rhpam-7.12.0-kogito-maven-repository.zip 文件。
  4. rhpam-7.12.0-kogito-maven-repository/maven-repository 子目录的内容复制到 ~/.m2/repository 目录中。
  5. 从红帽客户门户网站的 Software Downloads 页面下载 rhpam-7.12.0-kogito-and-optaplanner-quickstarts.zip 文件。
  6. 提取下载的存档。
  7. 导航到 optaweb-employee-rostering 文件夹。
  8. 要构建 Employee Rostering 应用程序,请运行以下命令:

    mvn clean install -DskipTests -DskipITs
  9. 登录到 OpenShift 帐户或 Red Hat Code Ready Container 实例。在以下示例中,< account-url& gt; 是 OpenShift 帐户或 Red Hat Code Ready Container 实例的 URL,&lt ;login-token > 是该帐户的登录令牌:

    oc login <account-url> --token <login-token>
  10. 创建一个新项目来托管 Employee Rostering:

    oc new-project optaweb-employee-rostering
  11. 运行 provision 脚本以构建和部署应用程序:

    ./runOnOpenShift.sh

    编译和打包可能需要长达 10 分钟才能完成。这些进程在命令行输出中持续显示进度。

    当操作完成后,会显示以下信息,其中 < URL& gt; 是部署的 URL:

    You can access the application at <URL> once the deployment is done.
  12. 输入您之前在流程中使用的 URL,对于 OpenShift 帐户或 Red Hat Code Ready Container 实例,以访问部署的应用程序。第一次启动最多可能需要一分钟,因为 OpenShift 平台上完成额外的构建。

    注意

    如果在点击链接后应用程序没有打开一分钟,请对浏览器页面执行硬刷新。

15.3. 使用员工漫长的入门应用程序

您可以使用 Web 界面使用 Employee Rostering 初学者应用程序。接口在 ReactJS 中开发。您还可以访问 REST API,以根据需要创建自定义用户界面。

15.3.1. 草案和发布的周期

在任何特定时间里,您可以使用应用程序在一段时间内创建 roster,称为 草案。默认情况下,草案的长度为 3 周。

当 roster 在草案的第一周结束时,您可以 发布 长远。目前,当前草案的第一天会成为一个 已发布 期。在已发布的时间段内,roster 已修复,您无法再自动更改(无论紧急情况手动更改)仍可能。然后,这个特质可向员工分发,以便他们可以规划自己的时间。草案周期将在以后的一个周移。

例如,假设设置了 9 月 1 到 9 月 21 日的草案。您可以在此期间内自动创建员工名册。然后,当您发布 roster 时,直到 9 月 7 日发布。新草案期是 9 月 8 日至 9 月 28 日。

有关发布 roster 的步骤,请参考 第 15.3.12 节 “发布推动力的转变”

15.3.2. 轮转模式

员工漫长应用程序支持变换 员工的旋转模式。

轮转模式是从两天开始的任何时间的"model"期限。模式不与特定日期绑定。

您可以为轮转每天创建存储桶。每次存储桶设置变化的时间。另外,模板可以包括用于转换的默认员工的名称。

当您发布 roster 时,应用会将一个新的周添加到草案中。目前,变动(如果适用)会将默认员工名称从轮转模式复制到草案的新部分。

达到轮转模式的末尾时,它会自动从开始重启。

如果您的组织中的周末转变模式与每周切换模式不同,请使用一周的轮转模式或整个星期数,例如 14、21 或 28 天。默认长度为 28 天。然后,模式始终在同一工作日中重复,您可以为不同的工作日设置转换。

有关编辑轮转模式的步骤,请参考 第 15.3.13 节 “查看和编辑轮转模式”

15.3.3. 员工划分租户

Employee Rostering 应用程序支持 多个租户。每个租户都是独立的数据集,包括输入和输出。更改一个租户的数据不会影响其他租户。您可以在租户间切换以使用多个独立数据集,例如,为不同的位置准备员工。

安装后会存在几个示例租户,它们代表了几个典型的企业类型,如工厂或 provider。您可以选择任何这些租户并修改它们以满足您的需求。您还可以创建新租户,从空白处输入数据。

15.3.3.1. 更改员工 Rostering 租户

您可以更改当前的租户。选择了不同的租户后,所有显示的信息都会引用此租户,您所做的任何更改都仅影响此租户。

流程

  1. 在 Employee Rostering 应用程序 Web 界面中,在浏览器窗口的右上角点 Tenant 列表。
  2. 从列表中选择一个租户。

15.3.3.2. 创建租户

您可以创建新租户,从空白处输入数据。在创建租户时,您可以设置几个参数来确定应用如何为此租户准备输出。

重要

您无法在创建租户后更改租户参数。

流程

  1. 要在 Employee Rostering 应用程序 Web 界面中创建一个新的租户,请在浏览器窗口右上角点击设置(gear)图标,然后点 Add
  2. 设置以下值:

    • 名称 :新租户的名称。此名称显示在租户列表中。
    • 计划开始日期 :初始草案的开始日期。发布 roster 后,此日期将变为已发布期的开始日期。此日期的工作日始终保持启动草案、任何特定公布的周期的工作日,以及首次使用轮转模式。因此,在星期一开始(星期一或周一)设置开始日期最方便。
    • 草案长度(天) :草案的长度。草案期限在租户的生命周期中保持相同的长度。
    • 发布通知(天) :发布通知周期的长度。期望提前至少在一天间发布最终感觉,因此员工有足够的通知来计划其个人生命时间。在当前版本中,此设置不会以任何方式强制执行。
    • 发布长度(天 ):每次发布时发布(固定)的时间段的长度。在当前版本中,此设置固定为 7 天。
    • rotation Length (天 ):轮转模式的长度。
    • timezone :应用到 roster 的环境的时区。此时区用于确定用户界面显示的"当前"日期。
  3. 点击 Save

租户使用空白数据创建。

15.3.4. 提升技能

您可以设置在名册内任意位置所需的全部 技能。例如,除了普通人力资源和休息等技能外,24 小时的校内还需要协调、服务、总线和托管技能。

流程

  1. 在 Employee Rostering 应用程序 Web 界面中,点 Skills 选项卡。

    您可以在浏览器窗口的右上方查看当前可见技能的数量,例如 1-15,34。您可以使用 &lt ;& gt; 按钮显示列表中的其他技能。

    您可以在搜索框中输入任何技能名称,以搜索技能。

  2. 完成以下步骤以添加新技能:

    1. 点击 Add
    2. 在名称下的文本字段中输入新技能 的名称
    3. 点 Save 图标。
  3. 要编辑技能的名称,请单击技能旁边的 Edit Skill 图标(铅笔图标)。
  4. 要删除技能,请单击技术旁边的 Delete Skill 图标(构成)。
注意

在每个租户中,技能名称都必须是唯一的。如果技能与员工或点相关联,则您无法删除技能。

15.3.5. 输入 spot

您必须输入 spots 列表,它代表了业务的不同位置。对于攻击,发现包括栏、公交车站、前端、各种工具包站、服务区域和办公室。

对于每个 spot,您可以从您在 Skills 选项卡中输入的列表中选择一个或多个所需技能。应用程序对员工而言,只有拥有该亮点需要的所有技能的员工。如果位置没有必要的技能,应用程序会将任何员工感觉到位。

流程

  1. 要在 Employee Rostering 应用程序 Web 界面中输入或更改 spots,请点击 Spots 选项卡。您可以在搜索框中输入 spot 名称的任何部分来搜索 spots。
  2. 完成以下步骤以添加新点:

    1. Add Spot
    2. 在名称下的文本字段中输入新 spot 的名称
    3. 可选:从所需技能 集的下拉列表中选择一个或多个技能
    4. 点 Save 图标。
  3. 要编辑 spot 的名称和所需技能,请点击 spot 旁边的 Edit Spot 图标(铅笔图标)。
  4. 要删除 spot,点 spot 旁边的 Delete Spot 图标(遍历)。
注意

在每个租户中,spot 名称必须是唯一的。当为其创建任何切换时,您无法删除它。

15.3.6. 输入合同列表

您必须输入业务用于员工的所有合同列表。

合同决定了员工在一天、日历、日历月份或日历年内可以工作的最长时间。

在创建合同时,您可以设置任何限制,或完全设置 none。例如,一个部分员工可能不允许每周工作超过 20 小时,而全时间员工可能需要一年时间为 10 小时和 1800 小时。另一个合同在工作小时时可能不包含任何限制。

您必须以分钟为单位为合同输入所有工作时间限值。

流程

  1. 要在 Employee Rostering 应用程序 Web 界面中输入或更改合同列表,请点击 Contracts 选项卡。

    您可以在浏览器窗口右上角看到当前可见的合同数,例如 34 的 1-15。您可以使用 &lt ;& gt; 按钮在列表中显示其他合同。

    您可以在搜索框中输入合同名称的任何部分 以搜索合同。

  2. 完成以下步骤以添加新合同:

    1. 点击 Add
    2. 在名称 下的文本字段中,输入合同 名称
    3. Maximum minutes 下输入所需的时间限制:

      • 如果员工不能每天多于某个集合时间,请启用复选框(在 Per Day ),然后在此复选框旁边的字段中输入分钟数。
      • 如果员工不能按日历周多工作,请启用 Per Week 的复选框,并在此复选框旁边的字段中输入分钟数。
      • 如果员工不能超过每个日历月份的设定时间,请在 Per Month 中启用复选框,并在此复选框旁边的字段中输入分钟数。
      • 如果员工不能超过每个日历年的设定时间,请启用 Per Year 的复选框,并在此复选框旁的字段中输入字段的分钟数。
    4. 点 Save 图标。
  3. 要编辑合同的名称和时间限值,请点击合同名称旁边的 Edit Contract 图标(铅笔图标)。
  4. 要删除合同,请点击合同名称旁边的 Delete Contract 图标(形形)。
注意

在每个租户中,合同名称必须是唯一的。如果分配给任何员工,则无法删除合同。

15.3.7. 输入员工列表

您必须输入业务的所有员工、其所掌握技能以及适用合同的列表。应用程序会根据合同中的工作时间限制限制这些员工来发现他们的技能。

流程

  1. 要在 Employee Rostering 应用程序 Web 界面中输入或更改员工列表,请单击 Employees 选项卡。

    您可以看到浏览器窗口右上角的当前可见员工的数量,例如 34 的 1-15。您可以使用 &lt ;& gt; 按钮显示列表中的其他员工。

    您可以在搜索框中输入员工名称 以搜索员工。

  2. 完成以下步骤以添加新员工:

    1. 点击 Add
    2. 在名称下的文本字段中输入员工 名称
    3. 可选:从 Skill set 下的下拉列表中选择一个或多个技能。
    4. 从合同下的下拉列表中选择一个 合同
    5. 点 Save 图标。
  3. 要编辑员工的名称和技能,请单击员工名称旁边的 Edit Employee 图标(铅笔图标)。
  4. 要删除员工,请单击员工名称旁边的 Delete Employee 图标(形成)。
注意

在每个租户中,员工名称必须是唯一的。如果员工正在经历任何变化,则无法删除。

15.3.8. 设置员工可用性

您可以为特定时间范围设置员工的可用性。

如果某个员工对于特定的时间范围 不可用,则不能将员工分配给任何变化(例如,如果员工在 sick 中被调用,还是在 vacation 中被调用)。不正确的 和所需 是员工针对特定时间的首选项;应用程序可以尽可能容纳它们。

流程

  1. 要在 Employee Rostering 应用程序 Web 界面中查看和编辑员工可用性,请单击 Availability Roster 选项卡。

    在窗口的左上方,您可以看到显示 roster 的日期。要查看其他周,您可以使用字段 周旁的 & lt; 和 > 按钮。或者,您可以点击 date 字段,并更改日期来查看包括此日期的星期。

  2. 要为员工创建可用性条目,请单击时间表上的空空格,然后选择员工。最初,整个天都会创建一个 Unavailable 条目。
  3. 要更改可用性条目,请点击该条目。您可以更改以下设置:

    • fromTo date and time :可用条目应用到的时间范围。
    • Status :您可以从下拉列表中选择 UnavailableDesiredUndesired status。

      要保存该条目,请单击 Apply

  4. 要删除可用性条目,请单击 条目,然后单击 Delete availability

    您还可以通过将鼠标指针移到条目上来更改或删除可用性条目,然后点击条目中显示的一个图标:

    • 点击 Unavailable 图标将条目的状态设置为 Unavailable
    • 点击 Undesired 图标将条目的状态设置为 Undesired
    • 点击 Desired 图标将条目的状态设置为 Desired
    • 点击 Delete 图标删除该条目。
重要

如果员工已分配给一个变化,那么您在转换过程中您创建或更改一个可用性条目,则分配不会自动更改。您必须再次创建员工转变,以应用新的或更改的可用性条目。

15.3.9. 查看并编辑转变过程中的转变

Shift Roster 是所有亮点和所有可能的时间跨度的表。

如果一个员工在跨度期间必须存在于某个位置,则必须有针对这个位置的 切换,这个时间跨度必须存在。如果位置同时需要多个员工,则可以为同一点和时间范围创建多个变化。

每个转换都由 spot (行)和时间范围(列)的交集和时间范围(列)的接收方表示。

当在草案中添加新时间时,应用程序会将从轮转模式中的切换(和默认员工)复制到草案的这一新部分。您还可以在草案中手动添加并编辑切换。

流程

  1. 要查看并编辑 Employee Rostering 应用程序 Web 界面中的转变,请单击 Shift 选项卡。

    在窗口的左上方,您可以看到显示 roster 的日期。要查看其他周,您可以使用字段 周旁的 & lt; 和 > 按钮。或者,您可以点击 date 字段,并更改日期来查看包括此日期的星期。

  2. 要添加转移,请单击此时间表的开放区域。应用程序添加了一个切换,决定点的位置自动决定插槽和时间范围。
  3. 要编辑转换,请单击切换。您可以为更改设置以下值:

    • fromTo date and time: 转换的确切时间和持续时间。
    • 员工 :分配给该转变的员工.
    • 固定 :员工是否 固定到 转换中。如果员工被固定,自动员工名列无法更改员工的分配变化。固定员工不会自动复制到任何其他转变。

      要保存更改,请单击 Apply

  4. 要删除转换,请单击转换,然后单击 Delete shift

15.3.10. 创建并查看员工的转变

您可以使用应用程序创建并查看所有员工的最佳转变。

流程

  1. 要查看并编辑 Employee Rostering 应用程序 Web 界面中的转变,请单击 Shift 选项卡。
  2. 要创建最佳转变,请单击 Schedule。应用程序需要 30 秒才能找到最佳解决方案。

结果

操作完成后,Shift Roster 视图包含最佳转变功能。在草案期间创建新的 roster。该操作不会修改发布的周期。

在窗口的左上方,您可以看到显示 roster 的日期。要查看其他周,您可以使用字段 周旁的 & lt; 和 > 按钮。或者,您可以点击 date 字段,并更改日期来查看包括此日期的星期。

在草案中,代表切换的边框订购是点数行。在发布的时间段内,边框没有破坏行。

代表切换的框颜色显示每个转换的约束状态:

  • 强绿色:软约束匹配,例如,对于员工而言,这种转变处于"所需" 次。
  • Pale green:没有约束中断。
  • grey:Soft 约束无法正常工作,例如,对于员工而言,这种变动是"不需要"的。
  • 黄色:中等约束中断,例如,没有员工被分配给转换。
  • 红帽:硬约束损坏,例如,员工同时存在两个变化。

15.3.11. 查看员工的转变

您可以在以员工为中心的表中查看特定员工的分配转变。该信息与 Shift Roster 相同,但查看格式对于通知员工的分配变化更为方便。

流程

要查看员工表并转移 Employee Rostering 应用程序 Web 界面,请单击 Availability Roster 选项卡。

在窗口的左上方,您可以看到显示 roster 的日期。要查看其他周,您可以使用字段 周旁的 & lt; 和 > 按钮。或者,您可以点击 date 字段,并更改日期来查看包括此日期的星期。

您可以看到浏览器窗口右上角的当前可见员工的数量,例如 1-10 of 34。您可以使用数字旁边的 & lt; 和 > 按钮来显示列表中的其他员工。

在草案期间,代表切换的边框订购是点数行。在发布的时间段内,边框没有破坏行。

15.3.12. 发布推动力的转变

当您发布转变时,草案的第一周发布。尽管仍有可能发生紧急手动更改,自动员工对已发布期内的任何变化不再改变。草案期将在以后更改一周。有关草案和发布周期的详情,请参考 第 15.3.1 节 “草案和发布的周期”

流程

  1. 要查看并编辑 Employee Rostering 应用程序 Web 界面中的转变,请单击 Shift 选项卡。
  2. 回顾草案的第一周的转变 scalester,以确保它可以接受。
  3. 单击 Publish

15.3.13. 查看和编辑轮转模式

轮转模式允许您添加、移动和删除转换,以便您可以有效地管理员工资源。它通过时间 bucket 和席位定义。

rotation
  • 一个时间存储桶描述了一个时间插槽(例如,上午 9 点到下午 5 点到下午 5:00),用于特定位置或位置 (A) (例如,分析),超过两个或多个天数,以及任何所需技能(例如,触发培训)。
  • 席位 (B) 是特定时间 bucket 中特定日期的员工分配。
  • employee stub 是一个图标,代表可分配给一个时间 bucket 的员工。员工存根列表 列在 Employee Stub 列表中

有关轮转模式的详情,请参考 第 15.3.2 节 “轮转模式”

流程

  1. Rotation 选项卡查看并编辑轮转模式。
  2. Rotation 菜单中选择 spot。
  3. Add New Time Bucket。此时会显示 Creating Working Time Bucket 对话框。
  4. 指定开始和结束时间,选择任何其他所需技能,选择该时间 bucket 的天数,然后单击保存。该时间存储桶的未分配席位会出现在按时间范围组织的 Rotation 页面中。
  5. 要创建员工存根列表,以便您可以将员工添加到轮转中,请单击 Edit Employee Stub 列表
  6. Edit Employee Stub List 对话框中,点 Add Employee,然后从列表中选择一个员工。
  7. 添加此 stub 列表所需的所有员工,然后单击保存。员工显示在 Rotation 页面中的时间 bucket 以上。
  8. 单击员工图标,从员工存根列表中选择员工。
  9. 单击鼠标并拖到时间存储桶的座位,以将所选员工分配到这些席位。席位填充员工图标。

    注意

    一个时间 bucket 每天只能为其分配一个员工。要将多个员工添加到同一个时间 bucket,请复制时间存储桶并根据需要更改员工名称。

  10. 要置备调度,请点击 Scheduling,然后选择您为创建轮转的 spot。
  11. 单击 Provision 并指定日期范围。
  12. 取消选择您不想在此计划中包含的位置。
  13. 点所选 spot 旁边的箭头,然后取消选择任何您不想在调度中使用的存储桶。
  14. 单击 Provision Shifts。日历填充自时间存储桶生成的转换。
  15. 要修改转换,请单击在日历上产生的更改。

第 16 章 部署和使用红帽构建的 OptaPlanner vehicle 路由计划启动程序应用程序

作为开发人员,您可以使用 OptaWeb Vehicle Routing starter 应用程序来优化您的车辆交付。

先决条件

  • OpenJDK (JDK) 11 已安装。红帽构建的 Open JDK 可从红帽客户门户中的 Software Downloads 页面获得(需要登录)。
  • 已安装 Apache Maven 3.6 或更高版本。Maven 可从 Apache Maven Project 网站获取。

16.1. 什么是 OptaWeb Vehicle 路由?

许多企业的主要目的是传输各种类型的 cargo。这些企业的目标是从加载点向目的地提供一段回车,并以最有效的方式使用其车队。主要目标之一是最大程度减少以时间或距离计算的差旅成本。

这种类型的优化问题被称为 vehicle 路由问题(VRP),其变化有许多。

红帽构建的 OptaPlanner 可以解决许多这样的 vehicle 路由变化并提供解决方案示例。通过 OptaPlanner,开发人员可以专注于对业务规则和需求建模,而不是学习 约束编程。OptaWeb Vehicle Routing 通过提供回答问题(如下所示)来扩展 OptaPlanner 的 vehicle 路由功能:

  • 在何处获得距离和差旅时间?
  • 如何在映射中视觉化解决方案?
  • 如何构建在云中运行的应用程序?

OptaWeb Vehicle Routing 使用 OpenStreetMap (OSM)数据文件。有关 OpenStreetMap 的详情,请查看 OpenStreetMap 网站。

使用 OptaWeb Vehicle Routing 时使用以下定义:

区域 :Earth 映射的任意区域,由 OSM 文件表示。地区可以是国家/地区、城市、一致或一组经常被一起使用的国家/地区。例如,DACH 区域包括德国(DE)、Austria (AT)和瑞士(CH)。

国家/ 地区代码:由 ISO-3166 标准分配给国家/地区的双字母代码。您可以使用国家代码来过滤 geosearch 结果。由于您可以与跨越多个国家(如 DACH 区域)的区域合作,OptaWeb Vehicle Routing 接受了国家代码列表,以便可以将 geosearch 过滤用于此类地区。有关国家代码列表,请参阅 ISO 3166 国家代码

Geosearch :作为搜索关键字提供地址或区域位置名称的查询类型,并因此收到多个 GPS 位置。返回的位置数量取决于搜索关键字的唯一位置。因为大多数位置名称并不是唯一的,因此只包括位于您所在地区或国家/地区中的位置,从而过滤不相关的结果。

16.2. 下载并构建 OptaWeb Vehicle Routing 部署文件

在构建和部署 OptaWeb Vehicle Routing 前,您必须下载并准备部署文件。

流程

  1. 导航到红帽客户门户网站中的 Software Downloads 页面(需要登录),然后从下拉菜单中选择产品和版本:

    • 产品:流程自动化管理器
    • Version: 7.12.0
  2. 下载 Red Hat Process Automation Manager 7.12.0 Kogito 和 OptaPlanner 8 Decision Services Quickstarts (rhpam-7.12.0-kogito-and-optaplanner-quickstarts.zip)。
  3. 提取 rhpam-7.12.0-kogito-and-optaplanner-quickstarts.zip 文件。
  4. 下载 Red Hat Process Automation Manager 7.12 Maven Repository Kogito 和 OptaPlanner 8 Maven Repository (rhpam-7.12.0-kogito-maven-repository.zip)。
  5. 提取 rhpam-7.12.0-kogito-maven-repository.zip 文件。
  6. rhpam-7.12.0-kogito-maven-repository/maven-repository 子目录的内容复制到 ~/.m2/repository 目录中。
  7. 导航到 optaweb-8.11.1.Final-redhat-00006/optaweb-vehicle-routing 目录。
  8. 输入以下命令构建 OptaWeb Vehicle Routing :

    mvn clean package -DskipTests

16.3. 使用 runLocally.sh 脚本在本地运行 OptaWeb Vehicle Routing

Linux 用户可以使用 runLocally.sh Bash 脚本来运行 OptaWeb Vehicle Routing。

注意

runLocally.sh 脚本不会在 macOS 上运行。如果您无法使用 runLocally.sh 脚本,请参阅 第 16.4 节 “手动配置并运行 OptaWeb Vehicle Routing”

runLocally.sh 脚本会自动执行以下设置步骤,否则必须手动执行:

  • 创建数据目录。
  • 从 Geofabrik 下载所选 OpenStreetMap (OSM)文件。
  • 尝试自动将国家代码与每个下载的 OSM 文件关联。
  • 如果 standalone JAR 文件不存在,构建项目。
  • 通过取单个区域参数或以交互方式选择区域,启动 OptaWeb Vehicle Routing。

有关执行 runLocally.sh 脚本的说明,请参见以下部分:

16.3.1. 在快速启动模式下运行 OptaWeb Vehicle Routing runLocally.sh 脚本

使用 OptaWeb Vehicle Routing 开始的最简单方法是在没有任何参数的情况下运行 runLocally.sh 脚本。

先决条件

流程

  1. rhpam-7.12.0-kogito-and-optaplanner-quickstarts/optaweb-8.11.1.Final-redhat-00006/optaweb-vehicle-routing 目录中输入以下命令。

     ./runLocally.sh
  2. 如果提示您创建 .optaweb-vehicle-routing 目录,请输入 y。在第一次运行脚本时,系统会提示您创建此目录。
  3. 如果系统提示您下载 OSM 文件,请输入 y。您第一次运行脚本 OptaWeb Vehicle Routing 会下载 Belgium OSM 文件。

    应用程序在下载 OSM 文件后启动。

  4. 要打开 OptaWeb Vehicle Routing 用户界面,在网页浏览器中输入以下 URL:

    http://localhost:8080
注意

第一次运行脚本时,将需要几分钟才能启动,因为 OSM 文件必须由 GraphHopper 导入,并存储为 road 网络图。下次运行 runlocally.sh 脚本时,加载时间会显著提高。

16.3.2. 以交互模式运行 OptaWeb Vehicle Routing runLocally.sh 脚本

使用交互模式查看分配给每个区域的下载 OSM 文件和国家代码的列表。您可以使用交互模式从 Geofabrik 下载额外的 OSM 文件,而无需访问网站并选择下载目的地。

先决条件

流程

  1. 将目录更改为 rhpam-7.12.0-kogito-and-optaplanner-quickstarts/optaweb-8.11.1.Final-redhat-00006/optaweb-vehicle-routing
  2. 输入以下命令以互动模式运行脚本:

    ./runLocally.sh -i
  3. 在您选择的提示符处,输入 d 以显示下载菜单。之前下载的区域列表后跟您可以下载的区域列表。
  4. 可选:从之前下载的区域列表中选择区域:

    1. 在下载的区域列表中,输入与地区关联的数字。
    2. 按 Enter 键。
  5. 可选:下载区域:

    1. 输入与您要下载的区域关联的数字。例如,要选择欧洲地图,请输入 5
    2. 要下载映射,请输入 d,然后按 Enter 键。
    3. 要在映射中下载特定区域,请输入 e,然后输入与您要下载的区域关联的数字,然后按 Enter 键。

      使用大型 OSM 文件

      为获得最佳用户体验,请使用较小的区域,如欧洲或美国状态。使用大于 1 GB 的 OSM 文件将需要大量的 RAM 大小,并花费大量时间(最多几个小时)进行初始处理。

      应用程序在下载 OSM 文件后启动。

  6. 要打开 OptaWeb Vehicle Routing 用户界面,在网页浏览器中输入以下 URL:

    http://localhost:8080

16.3.3. 在非互动模式下运行 OptaWeb Vehicle Routing runLocally.sh 脚本

在非交互模式中使用 OptaWeb Vehicle Routing,通过包含您之前下载的 OSM 文件的单个命令启动 OptaWeb Vehicle Routing。这在您要快速或进行演示时快速切换时,这非常有用。

先决条件

流程

  1. 将目录更改为 rhpam-7.12.0-kogito-and-optaplanner-quickstarts/optaweb-8.11.1.Final-redhat-00006/optaweb-vehicle-routing
  2. 执行以下命令,其中 &lt ;OSM_FILE_NAME > 是您之前下载的 OSM 文件:

    ./runLocally.sh <OSM_FILE_NAME>

16.3.4. 更新数据目录

如果要使用不同的数据目录,您可以更新 OptaWeb Vehicle Routing 使用的数据目录。默认数据目录为 $HOME/.optaweb-vehicle-routing

先决条件

流程

  • 要使用不同的数据目录,请将目录的绝对路径添加到当前数据目录中的 .DATA_DIR_LAST 文件。
  • 要更改与区域关联的国家代码,请在当前数据目录中编辑 country_codes 目录中相应的文件。

    例如,如果您下载了 Scotland 的 OSM 文件,并且脚本无法猜测国家代码,请将 country_codes/scotland-latest 的内容设置为 GB。

  • 要删除区域,请从数据目录中的 openstreetmap 目录中删除对应的 OSM 文件,并删除 graphhopper 目录中的区域目录。

16.4. 手动配置并运行 OptaWeb Vehicle Routing

运行 OptaWeb Vehicle Routing 的最简单方法是使用 runlocally.sh 脚本。但是,如果系统上没有 Bash,您可以手动完成 runlocally.sh 脚本执行的步骤。

先决条件

流程

  1. 下载路由数据。

    路由引擎需要地理数据来计算在不同位置间传输所需的时间。在运行 OptaWeb Vehicle Routing 前,您必须下载并存储本地文件系统中的 OpenStreetMap (OSM)数据文件。

    注意

    OSM 数据文件通常介于 100 MB 到 1 GB 之间,需要下载,因此在构建或启动 OptaWeb Vehicle Routing 应用程序前下载文件是一个好主意。

    1. 在 Web 浏览器中打开 http://download.geofabrik.de/
    2. 单击 Sub Region 列表中的区域,如 Europe。此时会打开 subregion 页面。
    3. Sub Regions 表中,下载国家的 OSM 文件(.osm.pbf),如 Belgium。
  2. 创建数据目录结构。

    OptaWeb Vehicle Routing 在文件系统中读取和写入几种类型的数据。它从 openstreetmap 目录中读取 OSM (OpenStreetMap)文件,将 road 网络图写入 graphhopper 目录,并将用户数据保存在名为 db 的目录中。创建一个用于存储所有这些数据的新目录,以便更轻松地升级到未来 OptaWeb Vehicle 路由的新版本,并继续使用您之前创建的数据。

    1. 创建 $HOME/.optaweb-vehicle-routing 目录。
    2. $HOME/.optaweb-vehicle-routing 目录中创建 openstreetmap 目录:

      $HOME/.optaweb-vehicle-routing
      └── openstreetmap
    3. 将所有下载的 OSM 文件(带有扩展名 .osm.pbf的文件)移到 openstreetmap 目录中。

      其余的目录结构由 OptaWeb Vehicle Routing 应用程序首次运行时创建。之后,您的目录结构类似以下示例:

      $HOME/.optaweb-vehicle-routing
      
      ├── db
      │   └── vrp.mv.db
      ├── graphhopper
      │   └── belgium-latest
      └── openstreetmap
          └── belgium-latest.osm.pbf
  3. 将目录更改为 rhpam-7.12.0-kogito-and-optaplanner-quickstarts/optaweb-8.11.1.Final-redhat-00006/optaweb-vehicle-routing/optaweb-vehicle-routing-standalone/target
  4. 要运行 OptaWeb Vehicle Routing,请输入以下命令:

    java \
    -Dapp.demo.data-set-dir=$HOME/.optaweb-vehicle-routing/dataset \
    -Dapp.persistence.h2-dir=$HOME/.optaweb-vehicle-routing/db \
    -Dapp.routing.gh-dir=$HOME/.optaweb-vehicle-routing/graphhopper \
    -Dapp.routing.osm-dir=$HOME/.optaweb-vehicle-routing/openstreetmap \
    -Dapp.routing.osm-file=<OSM_FILE_NAME> \
    -Dapp.region.country-codes=<COUNTRY_CODE_LIST> \
    -jar quarkus-app/quarkus-run.jar

    在这个命令中,替换以下变量:

    • <OSM_FILE_NAME > :要使用的区域的 OSM 文件,以及您之前下载的区域
    • <COUNTRY_CODE_LIST > :用于过滤 geosearch 查询的国家代码的逗号分隔列表。有关国家代码列表,请参阅 ISO 3166 国家代码

      应用程序在下载 OSM 文件后启动。

      在以下示例中,OptaWeb Vehicle Routing 下载中央美的 OSM 映射(central-america-latest.osm.pbf),并在国家/地区(BZ)和 Guatemala (GT)中搜索。

      java \
      -Dapp.demo.data-set-dir=$HOME/.optaweb-vehicle-routing/dataset \
      -Dapp.persistence.h2-dir=$HOME/.optaweb-vehicle-routing/db \
      -Dapp.routing.gh-dir=$HOME/.optaweb-vehicle-routing/graphhopper \
      -Dapp.routing.osm-dir=$HOME/.optaweb-vehicle-routing/openstreetmap \
      -Dapp.routing.osm-file=entral-america-latest.osm.pbf \
      -Dapp.region.country-codes=BZ,GT \
      -jar quarkus-app/quarkus-run.jar
  5. 要打开 OptaWeb Vehicle Routing 用户界面,在网页浏览器中输入以下 URL:

    http://localhost:8080

16.5. 在 Red Hat OpenShift Container Platform 上运行 OptaWeb Vehicle Routing

Linux 用户可以使用 runOnOpenShift.sh Bash 脚本在 Red Hat OpenShift Container Platform 上安装 OptaWeb Vehicle Routing。

注意

runOnOpenShift.sh 脚本不会在 macOS 上运行。

先决条件

流程

  1. 登录或启动 Red Hat OpenShift Container Platform 集群。
  1. 输入以下命令,&lt ;PROJECT_NAME > 是新项目的名称:

    oc new-project <PROJECT_NAME>
  2. 如有必要,将目录更改为 rhpam-7.12.0-kogito-and-optaplanner-quickstarts/optaweb-8.11.1.Final-redhat-00006/optaweb-vehicle-routing
  3. 输入以下命令执行 runOnOpenShift.sh 脚本并下载 OpenStreetMap (OSM)文件:

    ./runOnOpenShift.sh <OSM_FILE_NAME> <COUNTRY_CODE_LIST> <OSM_FILE_DOWNLOAD_URL>

    在这个命令中,替换以下变量:

    • <OSM_FILE_NAME > :从 < OSM_FILE_DOWNLOAD_URL> 下载的文件名称
    • <COUNTRY_CODE_LIST > :用于过滤 geosearch 查询的国家代码的逗号分隔列表。有关国家代码列表,请参阅 ISO 3166 国家代码
    • <OSM_FILE_DOWNLOAD _URL> :可从 OpenShift 访问 OSM 数据文件的 URL。该文件将在后端启动过程中下载,并保存为 /deployments/local/<OSM_FILE_NAME>

      在以下示例中,OptaWeb Vehicle Routing 下载中央美的 OSM 映射(central-america-latest.osm.pbf),并在国家/地区(BZ)和 Guatemala (GT)中搜索。

      ./runOnOpenShift.sh central-america-latest.osm.pbf BZ,GT http://download.geofabrik.de/europe/central-america-latest.osm.pbf
注意

如需 运行OnOpenShift.sh 脚本的帮助,请输入 ./runOnOpenShift.sh --help

16.5.1. 使用本地更改更新部署的 OptaWeb Vehicle Routing 应用程序

在 Red Hat OpenShift Container Platform 上部署 OptaWeb Vehicle Routing 应用程序后,您可以更新后端和前端。

先决条件

  • OptaWeb Vehicle Routing 已成功使用 Maven 构建并部署到 OpenShift 中。

流程

  • 要更新后端,请执行以下步骤:

    1. 更改源代码并使用 Maven 构建后端模块。
    2. 将目录更改为 rhpam-7.12.0-kogito-and-optaplanner-quickstarts/optaweb-8.11.1.Final-redhat-00006/optaweb-vehicle-routing
    3. 输入以下命令启动 OpenShift 构建:

      oc start-build backend --from-dir=. --follow
  • 要更新前端,请执行以下步骤:

    1. 更改源代码并使用 npm 实用程序构建前端模块。
    2. 将目录更改为 source/optaweb-vehicle-routing-frontend
    3. 输入以下命令启动 OpenShift 构建:

      oc start-build frontend --from-dir=docker --follow

16.6. 使用 OptaWeb Vehicle Routing

在 OptaWeb Vehicle Routing 应用程序中,您可以在映射中标记多个位置。第一个位置假定为 depot。vehicles 必须向标记的每个其他位置提供相关产品。

您可以设置 vehicles 的数量,以及每个载体的传输容量。但是,路由无法保证使用所有 vehicles。应用程序使用所需数量的 vehicles 来实现最佳路由。

当前版本有一些限制:

  • 每个发送到位置的传输都应该使用一分子容量。例如,容量为 10 的载体可在返回独立前访问最多 10 个位置。
  • 不支持设置 vehicles 和 location 的自定义名称。

16.6.1. 创建路由

要创建最佳路由,请使用 OptaWeb Vehicle Routing 用户界面的 Demo 选项卡。

先决条件

  • OptaWeb Vehicle Routing 正在运行,您可以访问用户界面。

流程

  1. 在 OptaWeb Vehicle Routing 中,点 Demo 打开 Demo 选项卡。
  2. 使用映射上方的 blue minus 和加号按钮来设置 vehicles 的数量。每个载有默认容量为 10。
  3. 根据需要,在地图上的方块中使用加号按钮来缩放。

    注意

    不要双击 缩放。双击也会创建一个位置。

  4. 点 depot 的位置。
  5. 点发送点映射上的其他位置。
  6. 如果要删除位置:

    1. 将鼠标光标悬停在位置上,以查看位置名称。
    2. 在屏幕左侧的列表中找到位置名称。
    3. 单击名称旁边的 X 图标。

每次添加或删除位置或更改 vehicles 数量时,应用程序都会创建并显示新的最佳路由。如果解决方案使用多个 vehicles,则应用程序会以不同的颜色显示每个 vehicle 的路由。

16.6.2. 查看和设置其他详情

您可以使用 OptaWeb Vehicle Routing 用户界面中的其他标签页来查看和设置附加详情。

先决条件

  • OptaWeb Vehicle Routing 正在运行,您可以访问用户界面。

流程

  • Vehicles 选项卡查看、添加和删除 vehicles,并为每个 vehicle 设置容量。
  • Visits 选项卡查看和删除位置。
  • 单击 Route 选项卡,以选择每个载体并查看所选载体的路由。

16.6.3. 使用 OptaWeb Vehicle Routing 创建自定义数据集

有一个内置演示数据集,它由几个大型 Belgian 城市组成。如果要在 Load demo 菜单中有更多演示,您可以准备自己的数据集。

流程

  1. 在 OptaWeb Vehicle Routing 中,点映射或使用 geosearch 来添加 depot 和一个或多个访问。
  2. 单击 Export,并将文件保存到数据集目录中。

    注意

    数据集目录是在 app.demo.data-set-dir 属性中指定的目录。

    如果应用程序通过 runLocally.sh 脚本运行,则数据集目录被设置为 $HOME/.optaweb-vehicle-routing/dataset

    否则,属性从 application.properties 文件中获取,默认为 rhpam-7.12.0-kogito-and-optaplanner-quickstarts/optaweb-8.11.1.Final-redhat-00006/optaweb-vehicle-routing/optaweb-vehicle-routing-standalone/target/local/dataset

    您可以编辑 app.demo.data-set-dir 属性来指定 diffent 数据目录。

  3. 编辑 YAML 文件,并为数据集选择唯一名称。
  4. 重新启动后端。

重新启动后端后,Data set 目录中的文件将出现在 Load demo 菜单中。

16.6.4. 对 OptaWeb Vehicle Routing 进行故障排除

如果 OptaWeb Vehicle Routing 意外行为,请按照以下步骤出现问题。

先决条件

  • OptaWeb Vehicle Routing 正在运行,并意外执行。

流程

  1. 若要识别问题,请查看后端终端输出日志。
  2. 要解决这个问题,请删除后端数据库:

    1. 在后端终端窗口中按 Ctrl+C 来停止后端。
    2. 删除 optaweb-vehicle-routing/optaweb-vehicle-routing-backend/local/db 目录。
    3. 重启 OptaWeb Vehicle Routing。

16.7. OptaWeb Vehicle Routing 开发指南

这部分论述了如何在开发模式中配置和运行后端模块和前端模块。

16.7.1. OptaWeb Vehicle Routing 项目结构

OptaWeb Vehicle Routing 项目是一个多模块 Maven 项目。

图 16.1. 模块依赖项树图

后端和前端模块位于模块树的底部。这些模块包含应用源代码。

standalone 模块是一个装配模块,它将后端和前端合并到单个可执行 JAR 文件中。

distribution 模块代表最终的装配步骤。它采用独立应用和文档,并将它们打包在易于分发的存档中。

后端和前端是您可以单独构建和部署的独立项目。实际上,它们使用完全不同的语言编写,并使用不同工具构建。两个项目都有工具,为现代开发人员体验提供代码更改和正在运行的应用程序之间的快速转变。

下一部分描述了如何在开发模式下运行后端和前端项目。

16.7.2. OptaWeb Vehicle Routing 后端模块

后端模块包含一个服务器端应用程序,它使用红帽构建的 OptaPlanner 来优化 vehicle 路由。优化是一种 CPU 密集型计算,必须避免任何 I/O 操作才能完全执行。因为其中一个首席目标是最大程度降低差旅成本,可以是时间或距离,OptaWeb Vehicle Routing 能够在 RAM 内存中保持旅行成本信息。在解决时,OptaPlanner 需要知道用户输入的每对位置之间的差成本。此信息存储在称为 距离矩阵 的结构中。

当您输入新位置时,OptaWeb Vehicle Routing 计算新位置和目前输入的每个其他位置之间的旅行成本,并在距离距离列表中存储差成本。差旅成本计算由 GraphHopper 路由引擎执行。

后端模块实现以下额外功能:

  • 持久性
  • 前端的 websocket 连接
  • 数据集加载、导出和导入

要了解更多有关后端代码架构的信息,请参阅 第 16.8 节 “OptaWeb Vehicle Routing 后端架构”

下一部分描述了如何在开发模式中配置和运行后端。

16.7.2.1. 运行 OptaWeb Vehicle Routing 后端模块

您可以在 Quarkus 开发模式下运行后端模块。

先决条件

流程

  1. 将目录更改为 rhpam-7.12.0-kogito-and-optaplanner-quickstarts/optaweb-8.11.1.Final-redhat-00006/optaweb-vehicle-routing/optaweb-vehicle-routing-backend
  2. 要在开发模式下运行后端,请输入以下命令:

    mvn compile quarkus:dev

16.7.2.2. 从 IntelliJ IDEA Ultimate 中运行 OptaWeb Vehicle Routing back-end 模块

您可以使用 IntelliJ IDEA Ulitmate 运行 OptaWeb Vehicle Routing 后端模块,以便更轻松地开发项目。IntelliJ IDEA Ultimate 包含 Quarkus 插件,它自动为使用 Quarkus 框架的模块创建运行配置。

流程

使用 optaweb-vehicle-routing-backend 运行配置来运行后端。

其他资源

如需更多信息,请参阅 运行 Quarkus 应用程序

16.7.2.3. Quarkus 开发模式

在开发模式中,如果存在对后端源代码或配置的更改,并且您刷新了前端运行的浏览器标签页,则后端会自动重启。

了解有关 Quarkus 开发模式 的更多信息

16.7.2.4. 更改 OptaWeb Vehicle Routing 后端模块系统属性值

您可以临时或永久覆盖 OptaWeb Vehicle Routing 后端模块的默认系统属性值。

OptaWeb Vehicle Routing 后端模块系统属性存储在 /src/main/resources/application.properties 文件中。此文件受版本控制。使用它永久存储默认配置属性值并定义 Quarkus 配置集。

先决条件

流程

  • 要临时覆盖默认系统属性值,请在运行 mvnjava 命令时包含 -D<PROPERTY>=<VALUE &gt ; 参数,其中 <PROPERTY > 是您要更改的 属性的名称,<VALUE > 是您要临时分配给该属性的值。以下示例演示了如何在使用 Maven 在 dev 模式中编译 Quarkus 项目时,将 quarkus.http.port 系统属性的值临时改为 8181

    mvn compile quarkus:dev -Dquarkus.http.port=8181

    这会临时更改存储在 /src/main/resources/application.properties 文件中的属性值。

  • 要永久更改配置值,例如要存储特定于您的开发环境的配置,请将 env-example 文件的内容复制到 optaweb-vehicle-routing-backend/.env 文件。

    此文件不包括在版本控制中,因此当您克隆存储库时它不存在。您可以在 .env 文件中进行更改,而不影响 Git 树。

其他资源

有关 OptaWeb Vehicle Routing 配置属性的完整列表,请参阅 第 16.9 节 “OptaWeb Vehicle Routing 后端配置属性”

16.7.2.5. OptaWeb Vehicle Routing backend logging

OptaWeb Vehicle Routing 使用 SLF4J API 和 Logback 作为日志框架。如需更多信息,请参阅 Quarkus - 配置日志记录

16.7.3. 使用 OptaWeb Vehicle Routing 前端模块

前端项目使用 Create React App 启动。创建 React 应用程序 提供了很多脚本和依赖项,以帮助开发和为生产环境构建应用程序。

先决条件

流程

  1. 在 Fedora 中,输入以下命令来设置开发环境:

    sudo dnf install npm

    如需有关安装 npm 的更多信息,请参阅下载并安装 Node.js 和 npm

  2. 将目录更改为 rhpam-7.12.0-kogito-and-optaplanner-quickstarts/optaweb-8.11.1.Final-redhat-00006/optaweb-vehicle-routing/optaweb-vehicle-routing-frontend
  3. 安装 npm 依赖项:

    npm install

    与 Maven 不同,npm 软件包管理器在项目目录下的 node_modules 中安装依赖项,并且仅在执行 npm install 时执行此操作。每当 package.json 中列出的依赖项更改时,例如,当您拉取对 master 分支的更改时,您必须在运行开发服务器前执行 npm install

  4. 输入以下命令运行开发服务器:

    npm start
  5. 如果没有自动打开,在 Web 浏览器中打开 http://localhost:3000/

    默认情况下,npm start 命令尝试在默认浏览器中打开此 URL。

    注意

    如果您不希望 npm start 命令在每次运行时打开新的浏览器标签页,请导出 BROWSER=none 环境变量。您可以使用 .env.local 文件使这个首选项具有持久性。要做到这一点,请输入以下命令:

    echo BROWSER=none >> .env.local

    每当您在前端源代码中更改时,浏览器都会刷新页面。终端中运行的开发服务器进程也会获取更改,并将编译和 lint 错误输出到控制台。

  6. 输入以下命令运行测试:

    npm test
  7. 更改 REACT_APP_BACKEND_URL 环境变量的值,以指定 npm startnpm run build 时要由 npm 使用的后端项目的位置,例如:

    REACT_APP_BACKEND_URL=http://10.0.0.123:8081
    注意

    npm 构建过程中,环境变量在 JavaScript 捆绑包中硬编码,因此您必须在构建和部署前端前指定后端位置。

    要了解更多有关 React 环境变量的信息,请参阅 添加自定义环境变量

  8. 要构建前端,请输入以下命令之一:

    ./mvnw install
    mvn install

16.8. OptaWeb Vehicle Routing 后端架构

域模型和使用案例对应用程序至关重要。OptaWeb Vehicle Routing 域模型位于架构中心,并由嵌入用例的应用程序层周围。路由优化、距离计算、持久性和网络通信等功能被视为实施细节,并放置在架构的最顶层。

图 16.2. 应用程序层图

16.8.1. 代码机构

后端代码分为三个层,如上图中所示。

org.optaweb.vehiclerouting
├── domain
├── plugin          # Infrastructure layer
│   ├── persistence
│   ├── planner
│   ├── routing
│   └── rest
└── service         # Application layer
    ├── demo
    ├── distance
    ├── error
    ├── location
    ├── region
    ├── reload
    ├── route
    └── vehicle

service 软件包包含实施用例的应用层。插件 软件包包含基础架构层。

每个层中的代码按功能进一步组织。这意味着每个服务或插件都有自己的软件包。

16.8.2. 依赖项规则

编译时间依赖项仅允许从外部层到中心。遵循此规则有助于保持独立于底层框架的域模型和其他实施细节,以及更精确地对业务实体的行为建模。随着演示和持久性被推送到外围,可以更轻松地测试业务实体和使用案例的行为。

域没有依赖项。

服务仅依赖于域。如果服务需要发送结果(例如,到数据库或客户端),它将使用输出边界接口。其实施由 上下文和依赖项注入 (CDI)容器注入。

插件通过两种方式依赖服务。首先,它们根据用户输入或路由更新等事件调用服务,来自优化引擎。服务注入插件,将构建和依赖项解析的负担移到 IoC 容器中。其次,插件实施服务输出边界接口来处理用例结果,例如保留对数据库的更改或向 Web UI 发送响应。

16.8.3. 域软件包

软件包包含 为此项目的域建模业务对象,如 位置VehicleRoute。这些对象严格面向业务,不得受到任何工具和框架的影响,如对象关系映射工具和 Web 服务框架。

16.8.4. service 软件包

服务 软件包包含实施 用例 的类。用例描述了您要进行的操作,例如添加新位置、更改载体容量或查找地址的协调。监管用例的业务规则使用域对象来表达。

服务通常需要与外部层中的插件交互,如持久性、Web 和优化。为了满足不同层之间的依赖关系规则,服务和插件之间的交互按照定义服务依赖项的接口来表达。插件可以通过提供实现服务的边界接口的 bean 来满足服务依赖项。CDI 容器创建插件 bean 实例,并将其在运行时注入到服务。这是控制原则的 inversion 示例。

16.8.5. 插件软件包

插件 软件包包含基础架构功能,如优化、持久性、路由和网络。

16.9. OptaWeb Vehicle Routing 后端配置属性

您可以设置下表中列出的 OptaWeb Vehicle Routing 应用程序属性。

属性类型示例描述

app.demo.data-set-dir

相对或绝对路径

/home/user/.optaweb-vehicle-routing/dataset

自定义数据集从此目录加载。默认为 local/dataset

app.persistence.h2-dir

相对或绝对路径

/home/user/.optaweb-vehicle-routing/db

H2 用于存储数据库文件的目录。默认为 local/db

app.region.country-codes

ISO 3166-1 alpha-2 国家代码列表

美国GB、IEDE、AT、 CH 可能为空

限制 geosearch 结果。

app.routing.engine

Enumeration

air,graphhopper

路由引擎实施.默认为 graphhopper

app.routing.gh-dir

相对或绝对路径

/home/user/.optaweb-vehicle-routing/graphhopper

GraphHopper 用来存储路线图的目录。默认为 local/graphhopper

app.routing.osm-dir

相对或绝对路径

/home/user/.optaweb-vehicle-routing/openstreetmap

包含 OSM 文件的目录。默认为 local/openstreetmap

app.routing.osm-file

文件名

belgium-latest.osm.pbf

GraphHopper 加载的 OSM 文件的名称。该文件必须放在 app.routing.osm-dir 下。

optaplanner.solver.termination.spent-limit

java.time.Duration

  • 1m
  • 150s
  • P2dT21h (PnDTnHnMn.nS)

解决者应在位置更改后运行多长时间。

server.address

IP 地址或主机名

10.0.0.123, my-vrp.geo-1.openshiftapps.com

将服务器绑定到的网络地址。

server.port

端口号

4000, 8081

服务器 HTTP 端口。

附录 A. 版本控制信息

文档最近对周三更新,2023 年 2 月 1 日。

附录 B. 联系信息

Red Hat Process Automation Manager 文档团队: brms-docs@redhat.com

法律通告

Copyright © 2023 Red Hat, Inc.
The text of and illustrations in this document are licensed by Red Hat under a Creative Commons Attribution–Share Alike 3.0 Unported license ("CC-BY-SA"). An explanation of CC-BY-SA is available at http://creativecommons.org/licenses/by-sa/3.0/. In accordance with CC-BY-SA, if you distribute this document or an adaptation of it, you must provide the URL for the original version.
Red Hat, as the licensor of this document, waives the right to enforce, and agrees not to assert, Section 4d of CC-BY-SA to the fullest extent permitted by applicable law.
Red Hat, Red Hat Enterprise Linux, the Shadowman logo, the Red Hat logo, JBoss, OpenShift, Fedora, the Infinity logo, and RHCE are trademarks of Red Hat, Inc., registered in the United States and other countries.
Linux® is the registered trademark of Linus Torvalds in the United States and other countries.
Java® is a registered trademark of Oracle and/or its affiliates.
XFS® is a trademark of Silicon Graphics International Corp. or its subsidiaries in the United States and/or other countries.
MySQL® is a registered trademark of MySQL AB in the United States, the European Union and other countries.
Node.js® is an official trademark of Joyent. Red Hat is not formally related to or endorsed by the official Joyent Node.js open source or commercial project.
The OpenStack® Word Mark and OpenStack logo are either registered trademarks/service marks or trademarks/service marks of the OpenStack Foundation, in the United States and other countries and are used with the OpenStack Foundation's permission. We are not affiliated with, endorsed or sponsored by the OpenStack Foundation, or the OpenStack community.
All other trademarks are the property of their respective owners.