Search code examples
constraintsschedulingoptaplannertimefold

OptaPlanner - use groupBy for avoidOvertime but only on the Resource creating the overtime


I am trying to solve a Scheduling problem that involves Resources in a work order assigned to a project. When a Resource is assigned at the exact same time as another and the sum of their assigned time is more than their Daily Capacity, the second one (only the second one) should break the Overtime constraint and be penalized.

If ( ResourceTime1 + ResourceTime2 > DailyCapacity ) then Resource2 breaks Overtime Constraint

The following code I wrote penalizes all tuple elements instead. What is the right way?

  private Constraint useOvertimeHard(ConstraintFactory factory) {
        return factory.forEach(WorkOrderAssignment.class)
                // Make sure Resources are the same and planned at the same period
                .join(WorkOrderAssignment.class,
                        Joiners.filtering(this::comparePeriods),
                        Joiners.filtering((woa1, woa2) -> woa1.getResource().getResourceCode().equals(woa2.getResource().getResourceCode())))
                // Group by tuples and calculate their total duration 
                .groupBy((woa1, woa2) -> woa1,
                        ConstraintCollectors.sumLong((woa1, woa2) -> woa1.getPeriod().getDuration() + woa2.getPeriod().getDuration()))
                // Filter to keep only the work order that lead to Overtime
                .filter((woa1, hours) -> hours > woa1.getResource().getTotalDailyCapacity())
                .penalize(HardSoftScore.ONE_HARD)
                .asConstraint("UseOvertimeHard");
    }

Solution

  • I would write the constraint like this:

      private Constraint useOvertimeHard(ConstraintFactory factory) {
            return factory.forEach(WorkOrderAssignment.class)
                    // Make sure Resources are the same and planned at the same period
                    // Group by resource and period, calculating their total duration 
                    .groupBy(WorkOrderAssignment::getResource, WorkOrderAssignment::getPeriod,
                            ConstraintCollectors.sumLong((woa) -> woa.getPeriod().getDuration()))
                    // Filter to keep only the resource that has Overtime
                    .filter((resource, period, hours) -> hours > resource.getTotalDailyCapacity())
                    .penalize(HardSoftScore.ONE_HARD)
                    .asConstraint("UseOvertimeHard");
        }
    

    This assumes comparePeriods check if Period are equal. If comparePeriods instead checks a single field for equality, use that field as the key instead of the period. If it does a more complex calculation, then:

    private Constraint useOvertimeHard(ConstraintFactory factory) {
            return factory.forEach(WorkOrderAssignment.class)
                    // Make sure Resources are the same and planned at the same period
                    .join(WorkOrderAssignment.class,
                            Joiners.equal(WorkOrderAssignment::getResource),
                            Joiners.filtering(this::comparePeriods))
                    // Group by tuples and calculate their total duration 
                    .groupBy((woa1, woa2) -> woa1.getResource(),
                            ConstraintCollectors.sumLong((woa1, woa2) -> woa1.getPeriod().getDuration() + woa2.getPeriod().getDuration()))
                    // Filter to keep only the resource that has Overtime
                    .filter((resource, hours) -> hours > resource.getTotalDailyCapacity())
                    .penalize(HardSoftScore.ONE_HARD)
                    .asConstraint("UseOvertimeHard");
        }