-1

I have the following 2 constraints in my project:

    fun cpMustUseN(constraintFactory: ConstraintFactory): Constraint {
        return constraintFactory.forEach(MealMenu::class.java)
            .join(CpMustUse::class.java, equal({ mm -> mm.slottedCp!!.id }, CpMustUse::cpId))
            .groupBy({ _, cpMustUse -> cpMustUse.numRequired }, countBi())
            .filter { numRequired, count -> count < numRequired }
            .penalize(HardSoftScore.ONE_HARD)
            .asConstraint("cpMustUseN")
    }

    fun cpMustUseAtLeastOne(constraintFactory: ConstraintFactory): Constraint {
        return constraintFactory.forEach(CpMustUse::class.java)
            .ifNotExists(MealMenu::class.java, equal({ cpMustUse -> cpMustUse.cpId }, { mealMenu -> mealMenu.slottedCp!!.id }))
            .penalize(HardSoftScore.ONE_HARD)
            .asConstraint("cpMustUseAny")
    }

When I run a testcase that I know will involve both of these constraints, OptaPlanner is able to find a feasible solution with scores of 0hard/0soft.

However, when I introduce the third constraint below, which is a soft constraint, it is no longer able to find a feasible solution. The best it can come up with on my testcase is -1hard/-3soft.

    fun cpVariety(constraintFactory: ConstraintFactory): Constraint {
        return constraintFactory.forEachUniquePair(
            MealMenu::class.java,
            equal(MealMenu::slottedCp)
        )
            .penalize(HardSoftScore.ONE_SOFT)
            .asConstraint("cpVariety")
    }

My understanding from the docs is that a feasible solution (ie. no hard constraints broken) will always be chosen if available, regardless of how many soft constraints are broken.

I am certain there is a feasible solution in this case, yet it is not chosen. What could be going on here?

EDIT: For future readers, here are the final constraints that I got working for this, based on Lukas's answer:

    fun cpMustUseN(constraintFactory: ConstraintFactory): Constraint {
        return constraintFactory.forEach(MealMenu::class.java)
            .join(CpMustUse::class.java, equal({ mm -> mm.slottedCp!!.id }, CpMustUse::cpId))
            .groupBy({ _, cpMustUse -> cpMustUse.numRequired }, countBi())
            .filter { numRequired, count -> count < numRequired }
            .penalize(HardSoftScore.ONE_HARD) { numRequired, count -> numRequired - count }
            .asConstraint("cpMustUseN")
    }

    fun cpMustUseAtLeastOne(constraintFactory: ConstraintFactory): Constraint {
        return constraintFactory.forEach(CpMustUse::class.java)
            .ifNotExists(MealMenu::class.java, equal({ cpMustUse -> cpMustUse.cpId }, { mealMenu -> mealMenu.slottedCp!!.id }))
            .penalize(HardSoftScore.ONE_HARD, CpMustUse::numRequired)
            .asConstraint("cpMustUseAtLeastOne")
    }

    fun cpVariety(constraintFactory: ConstraintFactory): Constraint {
        return constraintFactory.forEachUniquePair(
            MealMenu::class.java,
            equal(MealMenu::slottedCp)
        )
            .penalize(HardSoftScore.ONE_SOFT)
            .asConstraint("cpVariety")
    }
Chris
  • 366
  • 3
  • 11

1 Answers1

1

There could be many things going on here. In your case, I suggest you read the documentation on score traps carefully; especially when it comes to the cpMustUseN constraint.

There are also other explanations for OptaPlanner not finding optimal solutions.

  1. It is never guaranteed to; that's the nature of these problems.
  2. Sometimes it needs more CPU time to reach better solutions.
  3. And occasionally, for very small problems - which yours seems to be - OptaPlanner doesn't perform very well. Where it shines is with the growing size of the problem.
Lukáš Petrovický
  • 3,945
  • 1
  • 11
  • 20
  • Thanks - the score trap reading was enlightening. I'm still trying to get the lay of the land with this tool as we evaluate it for a project we are planning. In this, case, to avoid a score trap I am thinking that what I need to do is penalize by the number of missing CPs. In other words, in `cpMustUseN`, I want to penalize with `numRequired - count` hard points, and similarly in `cpMustUseAtLeastOne`, penalize by `cpMustUse.numRequired` hard points. I think this makes sense to me intuitively... but I cannot see how I would do that. – Chris Oct 29 '22 at 14:25
  • 1
    Take a look at the `penalize(...)` overloads. You'll notice one of them allows you to add a "match weight", which does exactly what you want. – Lukáš Petrovický Oct 29 '22 at 14:58