Search code examples
optaplanner

OptaPlanner Constraint Streams: Limiting to First Assignment in a Schedule


I'm designing an OptaPlanner service that optimizes the scheduling and assignment of robots in an industrial application. Planning is performed on-line and against a receding time horizon. There are two types of transit time constraints (which constrain the movement of a robot to a machine): one for the very first assignment of a robot to a machine and one for each subsequent pair of assignments (all at the same machine).

Planning is overall looking great in simulation, but there's a bug in the way I enforce the two different transit time constraints that pops up when I have a single bot with multiple assignments. The root cause is the enforcement of the two constraint streams:

  /**
   * This constraint enforces a transit time model to keep us from assigning a task prior the earliest possible arrival
   * time for the assigned bot.  In particular, this method is enforcing arrival time constraints for the very first
   * assignment to a bot.
   */
  protected Constraint violatingFirstArrivalTime(ConstraintFactory constraintFactory) {
    return constraintFactory.forEach(CandidateAssignment.class)
        .filter(CandidateAssignment::isAssigned)
        .filter(assignment -> !assignment.isCommitted())
        .penalizeConfigurable("Respect first arrival time", CandidateAssignment::firstAssignmentTimeliness);
  }

  /**
   * This constraint enforces a transit time model to keep us from assigning a task prior the earliest possible arrival
   * time for the assigned bot.  In particular, this method is enforcing arrival time constraints for the movement of a
   * bot from one task to another.
   */
  protected Constraint violatingArrivalTime(ConstraintFactory constraintFactory) {
    return constraintFactory.forEach(CandidateAssignment.class)
        .filter(CandidateAssignment::isAssigned)
        .join(CandidateAssignment.class, equal(CandidateAssignment::getBot))
        .penalizeConfigurable("Respect arrival time", CandidateAssignment::taskToTaskTimeliness);
  }

In particular, for the case of two assignments against a single bot, I'm incorrectly applying the "violatingFirstArrivalTime" constraint to every assignment (not simply the very first one). Any advice? I find constraint streams great when I immediately see how to implement a constraint and incredibly confusing otherwise..


Solution

  • I think that ifNotExists(...) is your answer here. Modify the constraint so that it only penalizes the assignment if no earlier assignment exists for that bot.

    return constraintFactory.forEach(CandidateAssignment.class)
        .filter(CandidateAssignment::isAssigned)
        .filter(assignment -> !assignment.isCommitted())
        .ifNotExists(CandidateAssignment.class,
            equal(CandidateAssignment::getBot),
            Joiners.filtering((a1, a2) -> {
                // Here go all the filters that specify the "comes before" relationship.
                // If you can replace filtering() with an actual joiner, even better for performance.
            }))
       .penalize(...);