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")
}