Optaplanner: Convert DRL to ConstraintStreams

Viewed 47

I have an employee rostering application. Some of the rules are quite similar as in the Nurse Rostering example.

In the last two months I had to convert around 30 rules written in DRL to ConstraintSteams. After struggeling at the beginning I started to like it more and more. At the end I really liked it. Thanks a lot for this awesome work!

I want to mention and ask a few things:

  • ConsecutiveWorkingDays: Here I use the org.optaplanner.examples.common.experimental.ExperimentalConstraintCollectors to solve it. It works perfect. But comparing to the most other rules (I benchmarked the one by one as described in the documentation), it performes not that good (12,150/s), comparing to dayOffRequest (27,384/s) or fairDistributionGroup (21,864/s). But I imagine thats the complexity of the problem.

  • Consecutive Working Days 2: What is the best way to include org.optaplanner.examples.common.experimental.ExperimentalConstraintCollectors and these classes in the project? I copied them from the example to the Project.

  • SleepTime: The law in Switzerland has several rules for how much sleep time are legal. The description here, is minimal simplified:

    • Less then 8 hours sleep is allways illegal
    • Twice less the 11 hours sleep in one week is also allways illegal
    • Once less then eleven hours sleep is illegal, if the average sleep time in the week is less then 11 hours.

    With the drl I did an InsertLogical of Freetime-Objects. A Freetime starts at the end of the last shift of a day and ends with the beginning of the next shift, where nexShift.dayIndex > firstShift.DayIndex. This calculation is quite intensive, because an employee can have more then one shift per day. These Freetime-Objects I also used to calculate the rule TwoDaysOfInARowPerWeek. With the ConstraintStreams the selection of the relevant shifts is done for every of the four rules. This decreased the calculation speed quite a lot. With the drl it was between 3000/s and 4000/s. With ConstraintStreams it decreased to 1500/s to 2000/s. Now I managed to do all the sleep time rules in one rule, so the selection of the shifts has not to be done 4 times, but only 2 times. And now the speed is okay (2700/s to 3500/s). But still, there is no way to do something like InsertLogical? Or what alternatives are there? Here the code, how I select this shifts:

      private BiConstraintStream<Shift, Shift> getEmployeeFreetimeRelevantShifts(ConstraintFactory constraintFactory) {
       // Shift (1): Employee works on the Shift
       return constraintFactory.forEach(Shift.class)
           .filter(shift -> shift.isNotEmptyShift() && shift.getHelperShiftForFreetime() != "last")
           // No later Shift on the same day as Shift (1)
           .ifNotExistsOther(
               Shift.class,
               Joiners.equal(Shift::getEmployee),
               Joiners.equal(Shift::getDayIndex),
               Joiners.filtering((shift_1, shift_2)-> shift_2.isNotHelperShiftForFreetime()),
               Joiners.filtering((shift_1, later_shift_on_same_day)-> shift_1.getEndDatetimeInMinutesOfDay() < later_shift_on_same_day.getEndDatetimeInMinutesOfDay())
           )
           // Shift (2) a shift on a later day than shift 1
           .join(
                Shift.class,
                Joiners.equal(Shift::getEmployee),
                Joiners.filtering((shift_1, shift_2)-> shift_2.getHelperShiftForFreetime() != "first"),
                Joiners.filtering((shift_1, shift_2)-> shift_1.getDayIndex() < shift_2.getDayIndex())
            )
           // There is no shift after Shift 2 on a earlier day between 1 and 2
           .ifNotExists(
               Shift.class,
               Joiners.equal((shift_1, shift_2)-> shift_1.getEmployee().getId(), not_existing_shift -> not_existing_shift.getEmployee().getId()),
               Joiners.filtering((shift_1, shift_2, shift_between_1_and_2 )-> {
                 return shift_between_1_and_2.isNotHelperShiftForFreetime();
               }),
               Joiners.filtering((shift_1, shift_2, shift_between_1_and_2 )-> {
                 return shift_between_1_and_2.getDayIndex() > shift_1.getDayIndex() && shift_between_1_and_2.getDayIndex() < shift_2.getDayIndex();
               })
            )
           // and there is no earlier shift on the same day before Shift 2
           .ifNotExists(
               Shift.class,
               Joiners.equal((shift_1, shift_2)-> shift_1.getEmployee().getId(), not_existing_shift -> not_existing_shift.getEmployee().getId()),
               Joiners.equal((shift_1, shift_2)-> shift_2.getDayIndex(), not_existing_shift -> not_existing_shift.getDayIndex()),
               Joiners.filtering((shift_1, shift_2, not_existing_shift_before_shift_2 )-> {
                 return not_existing_shift_before_shift_2.isNotHelperShiftForFreetime();
               }),
               Joiners.filtering((shift_1, shift_2, not_existing_shift_before_shift_2 )-> {
                 return shift_2.getStartDatetimeInMinutesOfDay() > not_existing_shift_before_shift_2.getStartDatetimeInMinutesOfDay();
               })
            );
     }
    
    
  • Execution of Rules with 0 score: In my case a user can select which rules he wants to be executed, if it should be a hard or a soft constraint and he can change the penalty value. I solve this with a @ConstraintConfiguration. But as far as I can see, also the rules with a penalty value of 0 are executed (but not penaltized). So if I disable all rules except of one rule, the speed is not higher then when I select all rules. is that correct? And is there a possibility to do that in a different way?

Again, thanks a lot for this awesome project!

1 Answers

First of all, thank you for your kind words, we appreciate that. If I may make one suggestion for your next question - ask your questions separately, as this "aggregate question" will make the answer needlessly hard to read and search.

Wrt. the experimental constraint collector - indeed, the performance of it is not ideal. It does a lot of things to give you a nice and useful API, at the expense of runtime performance. Wrt. using it in your own project - until we decide to make it a public API, copying it is how the collector is intended to be used.

Wrt. insertLogical - you are right that there is no such thing in Constraint Streams, and likely never will be. It may be a natural concept to people coming from Drools, and pretty much no one else. :-) The use case you describe (counting hours of sleep) may possibly be accomplished with shadow variables; the line between what should be done in shadow variables and in constraints is somewhat blurry.

Finally, when it comes to disabled constraints - you are right. We have a JIRA filed to eventually address that shortcoming.

Related