Search code examples
optaplannertimefold

OptaPlanner/TimeFold PlanningEntity containing list of PlanningEntity


I have two PlanningEntity classes like this:

@PlanningEntity
class MyPlanningEntity {
    @PlanningVariable
    MyPlanningVariable myPlanningVariable;
}

@PlanningEntity
class MyOtherPlanningEntity {
    @ShadowVariable(...)
    List<MyPlanningEntity> myPlanningEntities;
    int weight;
}

I want to write constraints that compute penalties based on what's in MyOtherPlanningEntity.myPlanningEntities.

At first I tried this:

Constraint myConstraint(ConstraintFactory constraintFactory) {
    return constraintFactory.forEach(MyOtherPlanningEntity.class)
           .penalize(HardSoftScore.ONE_HARD, myOtherPlanningEntity -> myFunction(myOtherPlanningEntity.weight, myOtherPlanningEntity.myPlanningEntities)
           .asConstraint("My constraint");
}

However, I think this wasn't always working because there wasn't an explicit dependency on the MyPlanningEntity instances in MyOtherPlanningEntity.myPlanningEntities (e.g., they weren't part of a join).

What is the right way to join on the variable number of MyPlanningEntitys in myOtherPlanningEntity.myPlanningEntities? I could build separate constraints for each possible length up to some max, but that seems verbose and inflexible. I thought about maybe using flattenLast but for my constraints I want to keep MyOtherPlanningEntity in the tuple with the MyPlanningEntitys (since it has other information on it I want, like the weight). It's also not clear from the documentation if the elements resulting from flattenLast will be tracked for changes, which is the real issue I'm solving here.

What's the right way to approach this?


Solution

  • The correct way would be to use a join (to declare the dependency on MyPlanningEntity) + filter (to only include MyPlanningEntity inside the myPlanningEntities list) + groupby (So each MyOtherPlanningEntity is only counted once):

    Constraint myConstraint(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(MyOtherPlanningEntity.class)
               .join(MyPlanningEntity.class)
               .filter((container, child) -> container.myPlanningEntities.contains(child))
               .groupby((container, child) -> container, ConstraintCollectors.toList((container, child) -> child))
               .penalize(HardSoftScore.ONE_HARD, (myOtherPlanningEntity, myPlanningEntityList) -> myFunction(myOtherPlanningEntity.weight, myPlanningEntityList))
               .asConstraint("My constraint");
    }
    

    That being said, if it is possible to refactor the penalize so it acts on items of the myPlanningEntityList instead of the entire myPlanningEntityList, please do so, since there will be numerous performance benefits. For instance, if your penalty function look like this:

    int myFunction(int weight, List<MyPlanningEntity> myPlanningEntityList) {
        int out = 0;
        for (MyPlanningEntity entity : myPlanningEntityList) {
            out += weight;
        }
        return out;
    }
    

    then it can be refactored to look like this:

    int myFunction(int weight, MyPlanningEntity myPlanningEntity) {
        return weight;
    }
    

    and the above stream changes to

    Constraint myConstraint(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(MyOtherPlanningEntity.class)
               .join(MyPlanningEntity.class)
               .filter((container, child) -> container.myPlanningEntities.contains(child))
               .penalize(HardSoftScore.ONE_HARD, (myOtherPlanningEntity, myPlanningEntity) -> myFunction(myOtherPlanningEntity.weight, myPlanningEntity))
               .asConstraint("My constraint");
    }