JBoss.orgCommunity Documentation

Chapter 5. Score calculation

5.1. Score terminology
5.1.1. What is a score?
5.1.2. Negative and positive constraints
5.1.3. Score constraint weighting
5.1.4. Score level
5.1.5. 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.4. Score calculation performance tricks
5.4.1. Overview
5.4.2. Incremental score calculation (with delta's)
5.4.3. Caching
5.4.4. Unused constraint
5.4.5. Build-in hard constraint
5.4.6. Other performance tricks
5.4.7. Score trap
5.4.8. stepLimit benchmark
5.5. Reusing the score calculation outside the Solver

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:

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>

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

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). Switch to ValueRange from planning entity to define that Course A should only be assigned a Room other then 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 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, so you can explain in the GUI what entities are causing which part of the Score. But if you're using the DroolsScoreDirector, it's possible. See the examples.