Search code examples
optaplannervehicle-routing

VRP with score dependens on more than one job in optaplanner


We are trying to solve a VRP with Optaplanner. The score calculation runs via constraint streams.

Now I have two vehicles (A and B) and want to schedule two jobs (J1 and J2). The construction heuristic (FIRST_FIT_DECREASING) schedules J1 to A and J2 to B, what is correct so far.

Now the two jobs also have an attribute "customer", and I want to assign a penalty if the customer of the two jobs is the same but the vehicles are different.

For this purpose, I have created a constraint in the ConstraintProvider that filters all jobs via groupBy that have the same customer but different vehicles.

If I now switch on the FULL_ASSERT_MODE, an IllegalStateException occurs after scheduling J2, because the score that is calculated incrementally is different from the score for the complete calculation. I suspect this is because the VariableListener, which recalculates the times of the jobs, only tells the ScoreDirector about a change to Job J2 for my shadowvariables and therefore only changes the score part that is related to it.

How can I tell Optaplanner that the score for J1 must also be recalculated? I can't get to job J1 via the VariableListener to tell the ScoreDirector that the score has to be changed here.

Or does this problem require a different approach?


Solution

  • This is a problem that is a bit hard to explain fully. TLDR version: constraint streams only react to changes to objects which are coming from either from(), join() or ifExists(). Changes on objects not coming through these statements will not be caught, and therefore causing score corruptions. Longer explanation follows.

    Consider a hypothetical Constraint Stream like this:

    constraintFactory.from(Shift.class)
        .join(Shift.class)
        .filter((shift1, shift2) -> shift1.getEmployee() == shift2.getEmployee())
        ...
    

    This constraint stream will work just fine, because if you change Shift by setting a different employee, the Shifts will be re-evaluated. They enter the stream via from() and join(), which is how CS knows to re-evaluate Shifts when they change.

    Now consider this constraint stream instead:

    constraintFactory.from(Shift.class)
        .filter(shift -> shift.getEmployee().getName() == "Lukas")
        ...
    

    This constraint stream will be re-evaluated, if Shift changes. But when the name of Employee changes, the constraint stream will not be re-evaluated; Employee is neither in from() nor in join(), changes to Employee will not trigger re-evaluation of the constraint stream.

    In your particular situation, you need to ensure several things:

    • Variable listeners mark everything as changed that actually changes.
    • If you modify problem facts, you need to make sure your variable listeners handle that too.
    • Objects that you want your constraint stream to react to are coming in through from() or a join().