I'm currently working on solving a VRP with pickups and deliveries. In the current model a Vehicle
is my first @PlanningEntity
, containing a @PlanningListVariable
of LoadJobs
, which can either be of type PICKUP
or DROPOFF
. Every LoadJob
has the reference to the Load
it is delivering and the Load
class has references to both its PICKUP
and DROPOFF
LoadJob
.
I've written the following constraint to ensure both LoadJobs
are put onto the same Vehicle
(of course, it would not make sense to try delivering a load that has never been picked up and it also would not make sense to pick up a load without ever delivering it):
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)
}
When I run in FULL_ASSERT
mode, I get the following exception:
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)
Because of the error message, my guess is this might be caused by the transitive relation between two LoadJobs
through the Load
class. I've tried filtering for only PICKUP
s before filtering for the same vehicle as well as joining LoadJob::class.java
, then filtering for the same Load
on both load jobs and only then filtering for the same vehicle, all to no avail.
What is the cause of this exception and how can I fix it?
It's because of how incremental score calculation interacts with
.filter { it.load.pickup.vehicle != it.load.dropoff.vehicle }
Neither pickup
or dropoff
are selected, so the ConstraintStreams implementation (regardless if it's OptaPlanner CS Drools impl, or Timefold's faster impl), doesn't know that when the pickup or dropoff's vehicle variable changes, this constraint needs to be re-evaluated (in a delta manner for scalability).
Solution
Something like:
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)
}