OptaPlanner 约束流:计算规划实体集中的不同值

OptaPlanner Constraint Streams: Count Distinct Values in Planning Entity Set

我正在寻求有关 OptaPlanner 约束流的帮助。问题是作业车间调度的一个变体,我的计划实体 (CandidateAssignment) 围绕着两个决策变量:机器人的选择和分配的时间粒度。每个 CandidateAssignment 也有一个字段(一个集合),表示仓库中的哪些物理容器将通过分配该任务来填充。

我试图强制执行的约束是最小化解决方案中所有 CandidateAssignments 使用的容器总数(目标是引导 OptaPlanner 按容器对任务进行分组......有特定领域的好处这个在仓库里)。如果每个 CandidateAssignment 只能服务于一个容器,这将很容易:

  protected Constraint maximizeContainerCompleteness(ConstraintFactory constraintFactory) {
    return constraintFactory.forEach(CandidateAssignment.class)
        .filter(CandidateAssignment::isAssigned)
        .groupBy(CandidateAssignment::getContainerId, countDistinct())
        .penalizeConfigurable("Group by container");
  }

从单个 ID 移动到集合对我来说似乎不太直接(即,如果 CandidateAssignment:getContainerIds returns 一组整数)。任何帮助将不胜感激。

编辑:感谢 Christopher 和 Lukáš 的回复。克里斯托弗的约束符合我的用例(最小化解决方案服务的容器数量)。然而,这最终成为引导 OptaPlanner 走向(更多)最佳解决方案的一种非常糟糕的方式,因为它是通过迭代本地搜索运行的。给定一个候选解决方案,该解决方案邻域中的大多数邻居对该约束具有相同的值(使用了# unique containers),因此它没有太多的辨别力。

我测试过的合理结果的方法如下:

  protected Constraint maximizeContainerCompleteness(ConstraintFactory constraintFactory) {
    return constraintFactory.forEach(CandidateAssignment.class)
        .filter(CandidateAssignment::isAssigned)
        .join(Container.class, Joiners.filtering(
            (candidate, container) -> candidate.getContainerIds().contains(container.getContainerId())))
        .rewardConfigurable("Group by container", (candidate, container) -> container.getPercentFilledSquared());
  }

这是 Lukáš 回答的修改版本。它通过优先处理“几乎满载”的容器来工作。在现实世界的用例中(我认为我在上面解释得很差),我们希望尽量减少解决方案中使用的容器数量,因为它允许仓库用“更容易”使用的新​​容器替换这些容器fulfill(搜索 space 的限制较少)。我们在后退的时间范围内进行计划,并且有许多部分填充的箱子意味着每个计划范围变得越来越难以安排。通过完成所有相关任务来“关闭”容器意味着我们可以用一个新容器替换那个容器并重新开始。

无论如何,只是一些上下文。这是一个非常特殊的用例,但是如果其他人阅读了这篇文章并想知道如何使用这种类型的约束,希望对您有所帮助。

将您的约束解释为“对每个使用的容器减 1”,这应该有效:

Constraint maximizeContainerCompleteness(ConstraintFactory constraintFactory) {
    return constraintFactory.forEach(CandidateAssignment.class)
            .filter(CandidateAssignment::isAssigned)
            .flattenLast(CandidateAssignment::getContainerIds)
            .distinct()
            .penalizeConfigurable("Group by container");
}

它的作用:对于每个分配的候选分配,展平其容器 ID 集(产生 non-distinct 使用的容器 ID 流),获取该流的不同元素(产生流不同的已用容器 ID),并为每个容器触发一个惩罚调用。

并不是要剥夺 Christopher 的正确答案,但有多种方法可以做到这一点。例如,考虑条件传播 (ifExists()):

return constraintFactory.forEach(Container.class)
    .ifExists(CandidateAssignment.class,
        Joiners.filtering((container, candidate) -> candidate.isAssigned()
            && candidate.getContainerIds().contains(container.getId()))
    .penalizeConfigurable("Penalize assigned containers", 
        container -> 1);

我有预感这种方法会更快,但是 YMMV。我建议您对这两种方法进行基准测试,然后选择性能更好的一种。

这种方法还有一个额外的好处,即 Container 实例出现在约束匹配中,而不是一些匿名的 Integer