JBoss.orgCommunity Documentation

Chapter 5. Score calculation

5.1. Score terminology
5.1.1. What is a score?
5.1.2. Positive and negative constraints
5.1.3. Score constraint weighting
5.1.4. Score level
5.1.5. Pareto scoring (AKA multi-objective optimization scoring)
5.1.6. The Score interface
5.2. Choose a Score definition
5.2.1. SimpleScore
5.2.2. HardAndSoftScore (recommended)
5.2.3. Implementing a custom Score
5.3. Calculate the Score
5.3.1. Score calculation types
5.3.2. Simple Java score calculation
5.3.3. Incremental Java score calculation
5.3.4. Drools score calculation
5.3.5. Detecting invalid scores
5.4. Score calculation performance tricks
5.4.1. Overview
5.4.2. Average calculation count per second
5.4.3. Incremental score calculation (with delta's)
5.4.4. Caching
5.4.5. Unused constraint
5.4.6. Build-in hard constraint
5.4.7. Other performance tricks
5.4.8. Score trap
5.4.9. stepLimit benchmark
5.5. Reusing the score calculation outside the Solver

Sometimes a score constraint outranks another score constraint, no matter how many times the other is broken. In that case, those score constraints are in different levels. For example: a nurse cannot do 2 shifts at the same time (due to the constraints of physical reality), this outranks nurse happiness constraints.

Most use cases have only 2 score levels: hard and soft. When comparing 2 scores, they are compared lexicographically: the first score level gets compared first. If those differ, the others score levels are ignored. For example: a score that breaks 0 hard constraints and 1000000 soft constraints is better than a score that breaks 1 hard constraint and 0 soft constraints.

Score levels often employ score weighting per level. In such case, the hard constraint level usually makes the solution feasible and the soft constraint level maximizes profit by weighting the constraints on price.

Note

Your business will probably tell you that your hard constraints all have the same weight, because they cannot be broken (so their weight does not matter). This is not true and it could create a score trap. For example in cloud balance: if a Computer has 7 CPU too little for its Processes, then it must be weighted 7 times as much as if it had only 1 CPU too little. This way, there is an incentive to move a Process with 6 CPU or less away from that Computer.

3 or more score levels is supported. For example: a company might decide that profit outranks employee satisfaction (or visa versa), while both are outranked by the constraints of physical reality.

Far less common is the use case of pareto optimization, which is also known under the more confusing term multi-objective optimization. In pareto scoring, score constraints are in the same score level, yet they are not weighted against each other. When 2 scores are compared, each of the score constraints are compared individually and the score with the most dominating score constraints wins. Pareto scoring can even be combined with score levels and score constraint weighting.

Consider this example with positive constraints, where we want to get the most apples and oranges. Since it's impossible to compare apples and oranges, we can't weight them against each other. Yet, despite that we can't compare them, we can state that 2 apples are better then 1 apple. Similarly, we can state that 2 apples and 1 orange are better than just 1 orange. So despite our inability to compare some Scores conclusively (at which point we declare them equal), we can find a set of optimal scores. Those are called pareto optimal.

Scores are considered equal far more often. It's left up to a human to choose the better out of a set of best solutions (with equal scores) found by Planner. In the example above, the user must choose between solution A (3 apples and 1 orange) and solution B (1 apples and 6 oranges). It's guaranteed that Planner has not found another solution which has more apples or more oranges or even a better combination of both (such as 2 apples and 3 oranges).

To implement pareto scoring in Planner, implement a custom ScoreDefinition and Score. Future versions will provide out-of-the-box support.

Note

A pareto Score's method compareTo is not transitive because it does a pareto comparison. For example: 2 apples is greater than 1 apple. 1 apples is equal to 1 orange. Yet, 2 apples are not greater than 1 orange (but actually equal). Pareto comparison violates the contract of the interface java.lang.Comparable's method compareTo, but Planner's systems are pareto comparison safe, unless explicitly stated otherwise in this documentation.

A simple way to implement your score calculation in Java.

Just implement one method of the interface SimpleScoreCalculator:

public interface SimpleScoreCalculator<Sol extends Solution> {


    Score calculateScore(Sol solution);
   
}

For example in n queens:

public class NQueensSimpleScoreCalculator implements SimpleScoreCalculator<NQueens> {


    public SimpleScore calculateScore(NQueens nQueens) {
        int n = nQueens.getN();
        List<Queen> queenList = nQueens.getQueenList();
        
        int score = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                Queen leftQueen = queenList.get(i);
                Queen rightQueen = queenList.get(j);
                if (leftQueen.getRow() != null && rightQueen.getRow() != null) {
                    if (leftQueen.getRowIndex() == rightQueen.getRowIndex()) {
                        score--;
                    }
                    if (leftQueen.getAscendingDiagonalIndex() == rightQueen.getAscendingDiagonalIndex()) {
                        score--;
                    }
                    if (leftQueen.getDescendingDiagonalIndex() == rightQueen.getDescendingDiagonalIndex()) {
                        score--;
                    }
                }
            }
        }
        return DefaultSimpleScore.valueOf(score);
    }
}

Configure it in your solver configuration:


  <scoreDirectorFactory>
    <scoreDefinitionType>...</scoreDefinitionType>
    <simpleScoreCalculatorClass>org.drools.planner.examples.nqueens.solver.score.NQueensSimpleScoreCalculator</simpleScoreCalculatorClass>
  </scoreDirectorFactory>

Alternatively, build a SimpleScoreCalculator instance at runtime and set it with the programmatic API:

    solverFactory.getSolverConfig().getScoreDirectorFactoryConfig.setSimpleScoreCalculator(simpleScoreCalculator);

A way to implement your score calculation incrementally in Java.

Implement all the methods of the interface IncrementalScoreCalculator and extend the class AbstractIncrementalScoreCalculator:

public interface IncrementalScoreCalculator<Sol extends Solution> {


    void resetWorkingSolution(Sol workingSolution);
    void beforeEntityAdded(Object entity);
    void afterEntityAdded(Object entity);
    void beforeAllVariablesChanged(Object entity);
    void afterAllVariablesChanged(Object entity);
    void beforeVariableChanged(Object entity, String variableName);
    void afterVariableChanged(Object entity, String variableName);
    void beforeEntityRemoved(Object entity);
    void afterEntityRemoved(Object entity);
    Score calculateScore();
    
}

For example in n queens:

public class NQueensAdvancedIncrementalScoreCalculator extends AbstractIncrementalScoreCalculator<NQueens> {


    private Map<Integer, List<Queen>> rowIndexMap;
    private Map<Integer, List<Queen>> ascendingDiagonalIndexMap;
    private Map<Integer, List<Queen>> descendingDiagonalIndexMap;
    private int score;
    public void resetWorkingSolution(NQueens nQueens) {
        int n = nQueens.getN();
        rowIndexMap = new HashMap<Integer, List<Queen>>(n);
        ascendingDiagonalIndexMap = new HashMap<Integer, List<Queen>>(* 2);
        descendingDiagonalIndexMap = new HashMap<Integer, List<Queen>>(* 2);
        for (int i = 0; i < n; i++) {
            rowIndexMap.put(i, new ArrayList<Queen>(n));
            ascendingDiagonalIndexMap.put(i, new ArrayList<Queen>(n));
            descendingDiagonalIndexMap.put(i, new ArrayList<Queen>(n));
            if (!= 0) {
                ascendingDiagonalIndexMap.put(- 1 + i, new ArrayList<Queen>(n));
                descendingDiagonalIndexMap.put((-i), new ArrayList<Queen>(n));
            }
        }
        score = 0;
        for (Queen queen : nQueens.getQueenList()) {
            insert(queen);
        }
    }
    public void beforeEntityAdded(Object entity) {
        // Do nothing
    }
    public void afterEntityAdded(Object entity) {
        insert((Queen) entity);
    }
    public void beforeAllVariablesChanged(Object entity) {
        retract((Queen) entity);
    }
    public void afterAllVariablesChanged(Object entity) {
        insert((Queen) entity);
    }
    public void beforeVariableChanged(Object entity, String variableName) {
        retract((Queen) entity);
    }
    public void afterVariableChanged(Object entity, String variableName) {
        insert((Queen) entity);
    }
    public void beforeEntityRemoved(Object entity) {
        retract((Queen) entity);
    }
    public void afterEntityRemoved(Object entity) {
        // Do nothing
    }
    private void insert(Queen queen) {
        Row row = queen.getRow();
        if (row != null) {
            int rowIndex = queen.getRowIndex();
            List<Queen> rowIndexList = rowIndexMap.get(rowIndex);
            score -= rowIndexList.size();
            rowIndexList.add(queen);
            List<Queen> ascendingDiagonalIndexList = ascendingDiagonalIndexMap.get(queen.getAscendingDiagonalIndex());
            score -= ascendingDiagonalIndexList.size();
            ascendingDiagonalIndexList.add(queen);
            List<Queen> descendingDiagonalIndexList = descendingDiagonalIndexMap.get(queen.getDescendingDiagonalIndex());
            score -= descendingDiagonalIndexList.size();
            descendingDiagonalIndexList.add(queen);
        }
    }
    private void retract(Queen queen) {
        Row row = queen.getRow();
        if (row != null) {
            List<Queen> rowIndexList = rowIndexMap.get(queen.getRowIndex());
            rowIndexList.remove(queen);
            score += rowIndexList.size();
            List<Queen> ascendingDiagonalIndexList = ascendingDiagonalIndexMap.get(queen.getAscendingDiagonalIndex());
            ascendingDiagonalIndexList.remove(queen);
            score += ascendingDiagonalIndexList.size();
            List<Queen> descendingDiagonalIndexList = descendingDiagonalIndexMap.get(queen.getDescendingDiagonalIndex());
            descendingDiagonalIndexList.remove(queen);
            score += descendingDiagonalIndexList.size();
        }
    }
    public SimpleScore calculateScore() {
        return DefaultSimpleScore.valueOf(score);
    }
}

Configure it in your solver configuration:


  <scoreDirectorFactory>
    <scoreDefinitionType>...</scoreDefinitionType>
    <incrementalScoreCalculatorClass>org.drools.planner.examples.nqueens.solver.score.NQueensAdvancedIncrementalScoreCalculator</incrementalScoreCalculatorClass>
  </scoreDirectorFactory>

Optionally, to get better output when the IncrementalScoreCalculator is corrupted in environmentMode DEBUG or TRACE, you can overwrite the method buildScoreCorruptionAnalysis from AbstractIncrementalScoreCalculator.

A ScoreHolder instance is asserted into the WorkingMemory as a global called scoreHolder. Your score rules need to (directly or indirectly) update that instance. Usually you'll make a single rule as an aggregation of the other rules to update the score:

global SimpleScoreHolder scoreHolder;

rule "multipleQueensHorizontal"
    when
        $q1 : Queen($id : id, $y : y);
        $q2 : Queen(id > $id, y == $y);
    then
        insertLogical(new UnweightedConstraintOccurrence("multipleQueensHorizontal", $q1, $q2));
end

// multipleQueensVertical is obsolete because it is always 0

rule "multipleQueensAscendingDiagonal"
    when
        $q1 : Queen($id : id, $ascendingD : ascendingD);
        $q2 : Queen(id > $id, ascendingD == $ascendingD);
    then
        insertLogical(new UnweightedConstraintOccurrence("multipleQueensAscendingDiagonal", $q1, $q2));
end

rule "multipleQueensDescendingDiagonal"
    when
        $q1 : Queen($id : id, $descendingD : descendingD);
        $q2 : Queen(id > $id, descendingD == $descendingD);
    then
        insertLogical(new UnweightedConstraintOccurrence("multipleQueensDescendingDiagonal", $q1, $q2));
end

rule "hardConstraintsBroken"
    when
        $occurrenceCount : Number() from accumulate(
            $unweightedConstraintOccurrence : UnweightedConstraintOccurrence(),
            count($unweightedConstraintOccurrence)
        );
    then
        scoreHolder.setScore(- $occurrenceCount.intValue());
end

Most use cases will also weigh their constraints differently, by multiplying the count of each score rule with its weight.

Here's an example from CurriculumCourse, where assigning a Lecture to a Room which is missing 2 seats is weighted equally bad as having 1 isolated Lecture in a Curriculum:

// RoomCapacity: For each lecture, the number of students that attend the course must be less or equal
// than the number of seats of all the rooms that host its lectures.
// Each student above the capacity counts as 1 point of penalty.
rule "roomCapacity"
    when
        ...
    then
        insertLogical(new IntConstraintOccurrence("roomCapacity", ConstraintType.NEGATIVE_SOFT,
                ($studentSize - $capacity),
                ...));
end

// CurriculumCompactness: Lectures belonging to a curriculum should be adjacent
// to each other (i.e., in consecutive periods).
// For a given curriculum we account for a violation every time there is one lecture not adjacent
// to any other lecture within the same day.
// Each isolated lecture in a curriculum counts as 2 points of penalty.
rule "curriculumCompactness"
    when
        ...
    then
        insertLogical(new IntConstraintOccurrence("curriculumCompactness", ConstraintType.NEGATIVE_SOFT,
                2,
                ...));
end


// Accumulate soft constraints
rule "softConstraintsBroken"
        salience -1 // Do the other rules first (optional, for performance)
    when
        $softTotal : Number() from accumulate(
            IntConstraintOccurrence(constraintType == ConstraintType.NEGATIVE_SOFT, $weight : weight),
            sum($weight)
        )
    then
        scoreHolder.setSoftConstraintsBroken($softTotal.intValue());
end

Put the environmentMode in TRACE (or DEBUG) to detect corruption in the incremental score calculation. On the difference between TRACE and DEBUG, see the section about environmentMode. However, that will not detect if your score calculator implements your score constraints as your business actually desires.

A piece of incremental score calculator code can be difficult to write and to review. You can assert its correctness by using a different implementation (for example a SimpleScoreCalculator) to do the assertions trigged by the environmentMode. Just configure it as assertionScoreDirectorFactory:


  <environmentMode>DEBUG</environmentMode>
  ...
  <scoreDirectorFactory>
    <scoreDefinitionType>...</scoreDefinitionType>
    <scoreDrl>/org/drools/planner/examples/nqueens/solver/nQueensScoreRules.drl</scoreDrl>
    <assertionScoreDirectorFactory>
      <scoreDefinitionType>...</scoreDefinitionType>
      <simpleScoreCalculatorClass>org.drools.planner.examples.nqueens.solver.score.NQueensSimpleScoreCalculator</simpleScoreCalculatorClass>
    </assertionScoreDirectorFactory>
  </scoreDirectorFactory>

Instead of implementing a hard constraint, you can sometimes make it build-in too. For example: If Course A should never be assigned to Room X, but it uses ValueRange from Solution, the Solver will often try to assign it to Room X too (only to find out that it breaks a hard constraint). Use filtered selection to define that Course A should only be assigned a Room other than X.

This tends to give a good performance gain, not just because the score calculation is faster, but mainly because most optimization algorithms will spend less time evaluating unfeasible solutions.

Note

Don't go overboard with this. Many optimization algorithms rely on the freedom to break hard constraints when changing planning entities, to get out of local optima. There is a real risk of trading short term benefits for long term harm.

Other parts of your application, for example your webUI, might need to calculate the score too. Do that by reusing the ScoreDirectorFactory of the Solver to build a separate ScoreDirector for that webUI:

ScoreDirectorFactory scoreDirectorFactory = solver.getScoreDirectorFactory();

ScoreDirector guiScoreDirector = scoreDirectorFactory.buildScoreDirector();

Then use it when you need to calculate the Score of a Solution:

guiScoreDirector.setWorkingSolution(solution);

Score score = guiScoreDirector.calculateScore();

Currently it's not officially supported to get the specific constraint occurrences, to explain in the GUI what entities are causing which part of the Score. But if you're using the DroolsScoreDirector, it's possible to extract it. See the examples.