Search code examples
optimizationgroup-byconstraintsoptaplanner

OPTAPLANNER: How to apply GroupBy to MultiConstraintStream


I have some doubts about how GroupBy works in a MultiConstraintStream. I need to group entries by two fields at the same time, and I am unsure how to do it.

For context, I am trying to build a constraint in Optaplanner for a job scheduling problem. I want to limit the maximum amount of output that can be done per day for each different type of job.

The constraint would go like this...

    private Constraint MaximumDailyOuput(ConstraintFactory constraintFactory) {
    // Limits maximum output per day.
    return constraintFactory.forEach(TimeSlotOpta.class) // iterate for each timeslot (days)
                 // join time slots with jobs
            .join(JobOpta.class) 
                 // filter if jobs are being done that day
            .filter((timeslot, job) -> job.isActive(timeslot.getDay()))
                 // join with job types, and filter, not sure if this is necessary or optimal
            .join(JobTypeOpta.class)
            .filter((timeSlot, job, jobType) -> job.getJobType() == jobType)
                 // HERE: now I would like to group the jobs that are active
                 // during a time slot and that are of the same type (job.getJobType()).
                 // For each group obtained, I need to sum the outputs of the jobs, 
                 // which can be obtained using job.getDailyOutput().
                 // Therefore, for each day (timeslot) and for each job type,
                 // I should obtain a sum that cannot overcome 
                 // the daily maximum for that job type (jobType.getMaximumDailyOuput())
            .groupBy((timeSlot, job, jobType) -> ...)
            ...
            .penalize("Maximum daily output exceeded", HardMediumSoftScore.ONE_HARD,
                    (timeSlot, jobType, dailyOuput) -> dailyOuput - jobType.getMaximumDailyOutput());
}

Solution

  • You can do this by specifying multiple group key functions in your groupBy

    private Constraint MaximumDailyOuput(ConstraintFactory constraintFactory) {
        // Limits maximum output per day.
        return constraintFactory.forEach(TimeSlotOpta.class) // iterate for each timeslot (days)
                     // join time slots with jobs
                .join(JobOpta.class) 
                     // filter if jobs are being done that day
                .filter((timeslot, job) -> job.isActive(timeslot.getDay()))
                     // join with job types, and filter, not sure if this is necessary or optimal
                .join(JobTypeOpta.class)
                .filter((timeSlot, job, jobType) -> job.getJobType() == jobType)
                // calculate total output for a given timeslot of a given jobType 
                .groupBy((timeSlot, job, jobType) -> timeslot,
                         (timeSlot, job, jobType) -> jobType,
                         ConstraintCollectors.sum((timeSlot, job, jobType) -> job.getDailyOutput()))
                // include only timeslot/jobType pairs where dailyOutput exceeds maximum allowed
                .filter((timeSlot, jobType, dailyOuput) -> dailyOuput > jobType.getMaximumDailyOutput())
                .penalize("Maximum daily output exceeded", HardMediumSoftScore.ONE_HARD,
                        (timeSlot, jobType, dailyOuput) -> dailyOuput - jobType.getMaximumDailyOutput());
    }
    

    For groupBy, you can include up to four group key functions and collectors combined (if you have 1 key function, you can have up to 3 collectors; if you have 2 key functions, you can have up to 2 collectors, etc.). The key functions are always before the collectors.