为什么对链接计划实体的约束会导致以下分数损坏异常?

Why does a constraint on linked planning entities cause the following score corruption exception?

提问人:greyhairredbear 提问时间:11/15/2023 最后编辑:Geoffrey De Smetgreyhairredbear 更新时间:11/17/2023 访问量:41

问:

我目前正在解决带有取货和送货的 VRP。在当前模型中,a 是我的第一个模型,包含 的 ,它可以是 类型 ,也可以是 。每个类都引用了它正在交付的内容,并且该类同时引用了它的 和 。Vehicle@PlanningEntity@PlanningListVariableLoadJobsPICKUPDROPOFFLoadJobLoadLoadPICKUPDROPOFFLoadJob

我编写了以下约束,以确保两者放在相同的位置(当然,尝试交付从未被拾取的负载是没有意义的,并且在不交付的情况下拾取负载也是没有意义的):LoadJobsVehicle

fun pickupAndDropoffOnSameVehicle(constraintFactory: ConstraintFactory): Constraint {
    return constraintFactory
        .forEach(LoadJob::class.java)
        .filter { it.load.pickup.vehicle != it.load.dropoff.vehicle }
        .penalizeConfigurable()
        .asConstraint(PICKUP_AND_DROPOFF_ON_SAME_VEHICLE)
}

当我在模式下运行时,我收到以下异常:FULL_ASSERT

Caused by: java.lang.IllegalStateException: Score corruption (100hard): the workingScore (-19init/-100hard/0medium/-11670soft) is not the uncorruptedScore (-19init/-200hard/0medium/-11670soft) after completedAction (LoadJob(id=DROPOFF-loDKrYTAqF5kIfjFM6n4) {null -> Vehicle(idx=0)[0]}):
Score corruption analysis:
  The corrupted scoreDirector has no ConstraintMatch(s) which are in excess.
  The corrupted scoreDirector has 1 ConstraintMatch(s) which are missing:
    com.cargonexx.vehiclerouting.solver.constraint/pickupAndDropoffOnSameVehicle/[LoadJob(id=PICKUP-loDKrYTAqF5kIfjFM6n4)]=-100hard/0medium/0soft
  Maybe there is a bug in the score constraints of those ConstraintMatch(s).
  Maybe a score constraint doesn't select all the entities it depends on, but finds some through a reference in a selected entity. This corrupts incremental score calculation, because the constraint is not re-evaluated if such a non-selected entity changes.
Shadow variable corruption in the corrupted scoreDirector:
  None
    at org.optaplanner.core.impl.score.director.AbstractScoreDirector.assertScoreFromScratch(AbstractScoreDirector.java:637)
    at org.optaplanner.core.impl.score.director.AbstractScoreDirector.assertWorkingScoreFromScratch(AbstractScoreDirector.java:613)
    at org.optaplanner.core.impl.score.director.AbstractScoreDirector.doAndProcessMove(AbstractScoreDirector.java:204)
    at org.optaplanner.core.impl.heuristic.thread.MoveThreadRunner.run(MoveThreadRunner.java:131)
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:577)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
    at java.base/java.lang.Thread.run(Thread.java:1589)

由于错误消息,我的猜测可能是由类中两者之间的传递关系引起的。在过滤同一辆车以及加入之前,我只尝试过过滤 s,然后在两个加载作业中过滤相同的车辆,然后才过滤同一辆车,但都无济于事。LoadJobsLoadPICKUPLoadJob::class.javaLoad

此异常的原因是什么,如何解决?

Kotlin 异常 Optaplanner 时间折叠

评论


答:

1赞 Geoffrey De Smet 11/15/2023 #1

这是因为增量分数计算如何与

.filter { it.load.pickup.vehicle != it.load.dropoff.vehicle }

两者都没有选择,因此 ConstraintStreams 实现(无论是 OptaPlanner CS Drools 实现,还是 Timefold 的更快实现)都不知道当取货或下车的车辆变量发生变化时,需要重新评估此约束(以增量方式实现可扩展性)。pickupdropoff

溶液

像这样:

fun pickupAndDropoffOnSameVehicle(constraintFactory: ConstraintFactory): Constraint {
    val pickupStream =
        constraintFactory.forEach(LoadJob::class.java).filter { it.type == LoadJobType.PICKUP }

    val dropoffStream =
        constraintFactory.forEach(LoadJob::class.java).filter { it.type == LoadJobType.DROPOFF }

    return pickupStream
        .join(dropoffStream, Joiners.equal { it.load.id })
        .filter { pickup, dropoff -> pickup.vehicle != dropoff.vehicle }
        .penalizeConfigurable()
        .asConstraint(PICKUP_AND_DROPOFF_ON_SAME_VEHICLE)
}

评论

0赞 greyhairredbear 11/15/2023
非常感谢您的回答,这对我帮助很大!但是,我并不真正理解潜在的问题(“既不选择也不被选中”)。从我的预期来看,对于在同一辆车上没有上车和下车的每个负载,应该违反两次约束(一次是 loadJob,一次是 )。要么 or 总是等于 .您能否详细说明为什么没有选择规划实体?还是相关计划实体并不总是与选取的实体一起选择?pickupdropoffPICKUPDROPOFFit.load.pickupit.load.dropoffit
1赞 Geoffrey De Smet 11/17/2023
对于增量分数计算,约束流仅计算每次移动的增量。这使得 Timefold 每秒可以计算 10k-500k 的分数,而不是扩展到大型数据集时的几百个分数。但要做到这一点,它需要知道哪些域类实例发生了变化(它确实更改了),以及哪些约束受哪些域类实例的影响。后者是通过 等完成的。forEach(class)join(class)
1赞 Geoffrey De Smet 11/17/2023
因此,当 没有更改时,约束使用和更改,但拾取从未 forEach()ed 或 join()ed,它没有“看到”更改,delta 是错误的,分数最终损坏。loadload.pickup.vehiclepickup
0赞 greyhairredbear 11/17/2023
谢谢,我认为这为我清除了它 +1
0赞 Geoffrey De Smet 11/18/2023
不客气:)