0

I am solving an employee rostering problem with optaplanner. Currently I'm rewriting my rules written in DRL to Constraint Streams. I have constraints, where the user can define, if he wants the rule as a hard or a soft constraint. In DRL in the then part I had access to my roster object and with this I could find out if I have to add a hardConstraintMatch or a softConstraintMatch.

How can I achieve that with Constraint Streams?

Here an example:

Constraint holiday(ConstraintFactory constraintFactory) {
    return constraintFactory.forEach(Shift.class)
        .join(
          Absence.class,
          Joiners.equal(Shift::getEmployee, Absence::getEmployee),
          Joiners.greaterThanOrEqual(Shift::getDate, Absence::getStartDate),
          Joiners.lessThanOrEqual(Shift::getDate, Absence::getEndDate)
        ).penalize(
            "holiday",
            // Here I would need something like roster.getRules().get("holiday").getType()
            // to define dynamically if I have to add ONE_HARD or ONE_SOFT
            HardMediumSoftLongScore.ONE_HARD,
            (shift,absence) -> {
              Roster roster = shift.getRoster();
              return roster.getRules().get("holiday").getPenaltyValue();
            }
          );
  }

Has someone an idea how to achieve that?

2 Answers2

0

What you're looking for is called Constraint Configuration.

After introducing @ConstraintConfiguration on the solution, your constraint would then look like so:

Constraint holiday(ConstraintFactory constraintFactory) {
    return constraintFactory.forEach(Shift.class)
        .join(Absence.class,
            Joiners.equal(Shift::getEmployee, Absence::getEmployee),
            Joiners.greaterThanOrEqual(Shift::getDate, Absence::getStartDate),
            Joiners.lessThanOrEqual(Shift::getDate, Absence::getEndDate))
        .penalizeConfigurable(
            "holiday",
            (shift, absence) -> 1);
}

For optimal results, change the match weight (1) to something that reflects the magnitude of the impact. (For example, if the absence length were used as the match weight, then longer absences would have counted for more if broken.)

Lukáš Petrovický
  • 3,945
  • 1
  • 11
  • 20
  • Hey, thank you so much! That's exactly what I need. Just one more small question: Is it also possible to disable a rule dynamically? In my case the user can select which rules he wants to be applied. – Mischa Stone Jul 04 '22 at 06:54
  • Ahh, I found that: https://stackoverflow.com/questions/59751860/optaplanner-add-remove-constraints-dynamically is that the way to do it? – Mischa Stone Jul 04 '22 at 07:32
  • The way to disable a constraint is to set its weight to zero. OptaPlanner will do the rest. – Lukáš Petrovický Jul 04 '22 at 09:46
0

In my ConstraintConfiguration I would then do something like this in the constructor:

public EmployeeRosterConstraintConfiguration(Roster roster) {
    Rule rule = roster.getRules().get("holiday");
    if(rule == null) {
      holidayConflict = HardMediumSoftLongScore.ofSoft(0);
    } else if(rule.getType() == HARD_CONSTRAINT) {
      holidayConflict = HardMediumSoftLongScore.ofHard(rule.getPenaltyValue());
    } else {
      holidayConflict = HardMediumSoftLongScore.ofSoft(rule.getPenaltyValue());
    }
  }
  • The `Rule` part seems redundant. You can code the penalty value directly in `EmployeeRosterConstraintConfiguration`. If you want to have an external source for that, you could deserialize the class from JSON/XML, just like any other part of the planning solution. That said, this is an implementation detail. – Lukáš Petrovický Jul 04 '22 at 09:48