Constraint Streams accumulation

66 views
Skip to first unread message

cbk...@gmail.com

unread,
May 10, 2020, 6:47:17 PM5/10/20
to OptaPlanner development
I've tried to write my constraints with the Constraint Stream API -- it is great to work with compared to Drools.

I'm trying to accumulate all shift assignments for each employee.

My issue is that after the join, if an employee hasn't been assigned, the employee doesn't show up in the groupBy with a count 0, thus isn't being penalized.

    private fun minAndMaxShiftsPerMonth(factory: ConstraintFactory): Constraint {
      return factory
          .from(Employee::class.java)
          .join(ShiftAssignment::class.java, employeeEqualsShiftAssignment())
          .groupBy({ left, _ -> left }, countBi())
          .penalize("Minimum and Maximum number of assignments", HardMediumSoftScore.ONE_HARD) { employee, count ->
              val minShifts = employee.minShiftsInMonth
              val maxShifts = employee.maxShiftsInMonth
              when {
                  count < minShifts -> minShifts - count
                  count > maxShifts -> count - maxShifts
                  else -> 0
              }
          }
    }

    private fun employeeEqualsShiftAssignment()
          = Joiners.equal<Employee, ShiftAssignment?, String>(Employee::code) { it.employee?.code }


What is the best way to do this with the Constraint Streams API? I tried something like this, which works, but maybe not great at scaling. Do you have any recommendations?

    private fun minAndMaxShiftsPerMonth(factory: ConstraintFactory): Constraint {
       return factory
           .from(Employee::class.java)
           .join(ShiftAssignment::class.java)
           .groupBy({ employee, _ -> employee }, countIfBi {
               employee, shiftAssignment -> employee == shiftAssignment.employee
           })
           .penalize("Minimum and Maximum number of assignments", HardMediumSoftScore.ONE_HARD) { employee, count ->
               val minShifts = employee.minShiftsInMonth
               val maxShifts = employee.maxShiftsInMonth
               when {
                   count < minShifts -> minShifts - count
                   count > maxShifts -> count - maxShifts
                   else -> 0
               }
           }
   }

    private fun <A, B> countIfBi(function: (A, B) -> Boolean) = DefaultBiConstraintCollector(
       Supplier { IntArray(1) },
       TriFunction { resultContainer: IntArray, a: A, b: B ->
           val change = if (function(a, b)) 1 else 0
           resultContainer[0] += change
           Runnable { resultContainer[0] -= change }
       },
       Function { resultContainer: IntArray -> resultContainer[0] }
   )




Message has been deleted

Christopher Chianelli

unread,
May 11, 2020, 1:02:23 AM5/11/20
to OptaPlanner development
Greetings,

I deleted my other post as I misread the question (thought it was about including unassigned Shifts into the constraint stream, not including unassigned Employees into a count). Your solution looks good. The issue in your first attempt is if the employee is assigned to no Shifts, there isn't a pair (Employee, ShiftAssignment) such that ShiftAssignment.employee == Employee, meaning the Employee will be absent from the mapping.

There is another way of modeling it: do it as two constraints; one where the employee has shifts, and another for the special case the employee does not have shifts:

    private fun minAndMaxShiftsPerMonth(factory: ConstraintFactory): Constraint {
      return factory
          .from(Employee::class.java)
          .join(ShiftAssignment::class.java, employeeEqualsShiftAssignment())
          .groupBy({ left, _ -> left }, countBi())
          .penalize("Minimum and Maximum number of assignments", HardMediumSoftScore.ONE_HARD) { employee, count ->
              val minShifts = employee.minShiftsInMonth
              val maxShifts = employee.maxShiftsInMonth
              when {
                  count < minShifts -> minShifts - count
                  count > maxShifts -> count - maxShifts
                  else -> 0
              }
          }
    }


 private fun employeeHasNoShifts(factory: ConstraintFactory): Constraint {
      return factory
          .from(Employee::class.java)
          .ifNotExists(ShiftAssignment::class.java, employeeEqualsShiftAssignment())
          .penalize("No assignments", HardMediumSoftScore.ONE_HARD) { employee ->
              val minShifts
= employee.minShiftsInMonth
              when {
                  minShifts != 0 -> minShifts
                  else -> 0
              }
          }
    }


    private fun employeeEqualsShiftAssignment()
          = Joiners.equal<Employee, ShiftAssignment?, String>(Employee::code) { it.employee?.code }

This will be better performance wise, as it does not check all Employee-Shift mappings. But it models the single constraint as two separate cases.

Best,
Christopher Chianelli

cbk...@gmail.com

unread,
May 11, 2020, 7:38:22 AM5/11/20
to OptaPlanner development
Ah, thank you!

I tried this approach but didn't know of the ifNotExists method. Perfect.
Reply all
Reply to author
Forward
0 new messages