3

OptaPlanner 8.36.0.Final

When using ConstraintStreamImplType.DROOLS the following constraint behaves as expected:

return constraintFactory
    .forEach(Ewe.class)
    .groupBy(ConstraintCollectors.count())
    .penalize(level, 
    (count) -> {
        return activeRange.check(count));
    }
    )
    .asConstraint(name);

Ewe is a PlanningEntity that has a nullable PlanningVariable Ram. There are 100 Ewes of which between 50 and 75 should be assigned rams. This constraint penalizes under or over allocation. The constraint produces the expected result.

When I change to ConstraintStreamImplType.BAVET the solver ends with no ewes assigned and the score is zero, so the solution picked is one with none of the Ewes assigned and the constraint is not evaluated, therefore the zero score.

It seems like BAVET does not evaluate this constraint because there are zero entities that match. On the other hand DROOLS evaluates the constraint when there are zero matches.

Is this difference between DROOLS and BAVET expected?

I validated this behavior change by only switching between ConstraintStreamImplType.BAVET and ConstraintStreamImplType.DROOLS with no other change

Edit: Ultimately I want to count entries even including a zero count as a possible answer.

Steven
  • 33
  • 4
  • 1
    I wouldn't expect any function difference between CS-D and CS-B. Especially not on a simple case like this one. Something else could be wrong. Anyway, if you provide me with some code I could run on my machine, I can look into the issue. – Lukáš Petrovický Apr 10 '23 at 14:08
  • 1
    @LukášPetrovický I will create a test with the simplest case possible and provide an update once I have it ready. – Steven Apr 10 '23 at 20:22
  • 1
    @LukášPetrovický Here's a link to a minimal Eclipse project that contains the simplest demo of this behavior. [link](https://storage.googleapis.com/dtnsip/OptaPlanner.zip) . This creates 5 Rams and 10 Ewes. There is one constraint that penalizes if fewer or more than 5 ewes are selected. Running Test.java with `withConstraintStreamImplType(ConstraintStreamImplType.BAVET)` and you get all 10 ewes are unassigned. Run with `.withConstraintStreamImplType(ConstraintStreamImplType.DROOLS)` and you get 5 ewes are assigned. – Steven Apr 10 '23 at 21:04

1 Answers1

1

Based off of the code you submitted in one of your comments, here's my analysis. BAVET behaves correctly, on account of the following:

  • forEach() will not forward any entities without variables assigned.
  • If groupBy has nothing on input, it does not evaluate anything. count() of nothing is not 0, it simply doesn't happen. (In other words: zero count is not possible in groupBy.) Therefore the resulting score for entities with null variables is 0.
  • Once you assign a variable to the first entitiy, the count is suddenly 1, making the total score -4. This is worse than 0, and therefore it is not picked.

In a way, this could be considered a score trap. In over-constrained problems (problems with nullable vars), we typically solve it by introducing another constraint which penalizes the number of unassigned entities. This gives the solver an incentive to actually assign entities. (And that, in turn, would trigger the groupBy(...).)

The question now becomes - why does DROOLS behave differently? This almost certainly looks like a bug. groupBy(count()) violates the general CS contract and produces something out of nothing. However, for backwards compatibility reasons, I do not think we will be fixing that; this behavior is far too exposed by now for us to change it, even if it is not correct.

As a side note, groupBy(groupKey, collector) does not show this behavior; only groupBy(collector) without a group key, as used in your code.

Lukáš Petrovický
  • 3,945
  • 1
  • 11
  • 20