JBoss.orgCommunity Documentation

Chapter 7. Move and neighborhood selection

7.1. Move and neighborhood introduction
7.1.1. What is a Move?
7.1.2. What is a MoveSelector?
7.1.3. Subselecting of entities, values and other moves
7.2. General Selector features
7.2.1. CacheType: Create moves in advance or Just In Time
7.2.2. SelectionOrder: original, random or shuffled selection
7.2.3. Recommended combinations of CacheType and SelectionOrder
7.2.4. Filtered selection
7.2.5. Sorted selection
7.2.6. Probability selection
7.3. Generic MoveSelectors
7.3.1. changeMoveSelector
7.3.2. swapMoveSelector
7.3.3. pillarSwapMoveSelector
7.3.4. subChainChangeMoveSelector
7.3.5. subChainSwapMoveSelector
7.4. Combining multiple MoveSelectors
7.4.1. unionMoveSelector
7.4.2. cartesianProductMoveSelector
7.5. EntitySelector
7.5.1. TODO
7.6. ValueSelector
7.6.1. TODO
7.7. Custom moves
7.7.1. Custom moves introduction
7.7.2. The interface Move
7.7.3. MoveListFactory: the easy way to generate custom moves
7.7.4. MoveIteratorFactory: generate custom moves just in time
7.7.5. Move generation through DRL

A Move is a change (or set of changes) from a solution A to a solution B. For example, the move below changes queen C from row 0 to row 2:

The new solution is called a neighbor of the original solution, because it can be reached in a single Move. Although a single move can change multiple queens, the neighbors of a solution should always be a very small subset of all possible solutions. For example, on that original solution, these are all possible changeMove's:

If we ignore the 4 changeMove's that have not impact and are therefore not doable, we can see that number of moves is n * (n - 1) = 12. This is far less than the number of possible solutions, which is n ^ n = 256. As the problem scales out, the number of possible moves increases far less than the number of possible solutions.

Yet, in 4 changeMove's or less we can reach any solution. For example we can reach a very different solution in 3 changeMove's:

All optimization algorithms use Move's to transition from one solution to a neighbor solution. Therefor, all the optimization algorithms are confronted with Move selection: the craft of creating and iterating moves efficiently and the art of finding the most promising subset of random moves to evaluate first.

A Selector's cacheType determines when a selection (such as a Move, an entity, a value, ...) is created and how long it lives.

Almost every Selector supports setting a cacheType:


    <changeMoveSelector>
      <cacheType>PHASE</cacheType>
      ...
    </changeMoveSelector>

The following cacheTypes are supported:

A cacheType can be set on composite selectors too:


    <unionMoveSelector>
      <cacheType>PHASE</cacheType>
      <changeMoveSelector/>
      <swapMoveSelector/>
      ...
    </unionMoveSelector>

Nested selectors of a cached selector cannot be configured to be cached themselves, unless it's a higher cacheType. For example: a STEP cached unionMoveSelector can hold a PHASE cached changeMoveSelector, but not a STEP cached changeMoveSelector.

A Selector's selectionOrder determines the order in which the selections (such as Moves, entities, values, ...) are iterated. An optimization algorithm will usually only iterate through a subset of its MoveSelector's selections, starting from the start, so the selectionOrder is critical to decide which Moves are evaluated.

Almost every Selector supports setting a selectionOrder:


    <changeMoveSelector>
      ...
      <selectionOrder>ORIGINAL</selectionOrder>
      ...
    </changeMoveSelector>

The following selectionOrders are supported:

A selectionOrder can be set on composite selectors too.

There are certain moves that you don't want to select, because:

Filtered selection can happen on any Selector in the selector tree, including any MoveSelector, EntitySelector or ValueSelector:

Filtering us the interface SelectionFilter:

public interface SelectionFilter<T> {


    boolean accept(ScoreDirector scoreDirector, T selection);
}

Implement the method accept to return false on a discarded selection. Unaccepted moves will not be selected and will therefore never have their method doMove called.

public class DifferentCourseSwapMoveFilter implements SelectionFilter<SwapMove> {


    public boolean accept(ScoreDirector scoreDirector, SwapMove move) {
        Lecture leftLecture = (Lecture) move.getLeftEntity();
        Lecture rightLecture = (Lecture) move.getRightEntity();
        return !leftLecture.getCourse().equals(rightLecture.getCourse());
    }
}

Apply the filter on the lowest level possible. In most cases, you 'll need to know both the entity and the value involved and you'll have to apply a moveFilterClass on the moveSelector:


    <swapMoveSelector>
      <moveFilterClass>org.drools.planner.examples.curriculumcourse.solver.move.DifferentCourseSwapMoveFilter</moveFilterClass>
    </swapMoveSelector>

But if possible apply it on a lower levels, such as an entityFilterClass on the entitySelector or a valueFilterClass on the valueSelector:


    <changeMoveSelector>
      <entitySelector>
        <entityFilterClass>...EntityFilter</entityFilterClass>
      </entitySelector>
    </changeMoveSelector>

Filtered selection works with any kind of cacheType and selectionOrder. You can configure multiple *FilterClass elements on a single selector.

Your custom moves must implement the Move interface:

public interface Move {


    boolean isMoveDoable(ScoreDirector scoreDirector);
    Move createUndoMove(ScoreDirector scoreDirector);
    void doMove(ScoreDirector scoreDirector);
    Collection<? extends Object> getPlanningEntities();
    Collection<? extends Object> getPlanningValues();
}

Let's take a look at the Move implementation for 4 queens which moves a queen to a different row:

public class RowChangeMove implements Move {


    private Queen queen;
    private Row toRow;
    public RowChangeMove(Queen queen, Row toRow) {
        this.queen = queen;
        this.toRow = toRow;
    }
    // ... see below
}

An instance of RowChangeMove moves a queen from its current row to a different row.

Planner calls the doMove(ScoreDirector) method to do a move. The Move implementation must notify the ScoreDirector of any changes it make to the planning entities's variables:

    public void doMove(ScoreDirector scoreDirector) {

        scoreDirector.beforeVariableChanged(queen, "row"); // before changes are made to the queen.row
        queen.setRow(toRow);
        scoreDirector.afterVariableChanged(queen, "row"); // after changes are made to the queen.row
    }

You need to call the methods scoreDirector.beforeVariableChanged(Object, String) and scoreDirector.afterVariableChanged(Object, String) directly before and after modifying the entity. Alternatively, you can also call the methods scoreDirector.beforeAllVariablesChanged(Object) and scoreDirector.afterAllVariablesChanged(Object).

Planner automatically filters out non doable moves by calling the isDoable(ScoreDirector) method on a move. A non doable move is:

In the n queens example, a move which moves the queen from its current row to the same row isn't doable:

    public boolean isMoveDoable(ScoreDirector scoreDirector) {

        return !ObjectUtils.equals(queen.getRow(), toRow);
    }

Because we won't generate a move which can move a queen outside the board limits, we don't need to check it. A move that is currently not doable could become doable on the working Solution of a later step.

Each move has an undo move: a move (normally of the same type) which does the exact opposite. In the example above the undo move of C0 to C2 would be the move C2 to C0. An undo move is created from a Move, before the Move has been done on the current solution.

    public Move createUndoMove(ScoreDirector scoreDirector) {

        return new RowChangeMove(queen, queen.getRow());
    }

Notice that if C0 would have already been moved to C2, the undo move would create the move C2 to C2, instead of the move C2 to C0.

A solver phase might do and undo the same Move more than once. In fact, many solver phases will iteratively do an undo a number of moves to evaluate them, before selecting one of those and doing that move again (without undoing it this time).

A Move must implement the getPlanningEntities() and getPlanningValues() methods. They are used by entity tabu and value tabu respectively. When they are called, the Move has already been done.

    public List<? extends Object> getPlanningEntities() {

        return Collections.singletonList(queen);
    }
    public Collection<? extends Object> getPlanningValues() {
        return Collections.singletonList(toRow);
    }

If your Move changes multiple planning entities, return all of them in getPlanningEntities() and return all their values (to which they are changing) in getPlanningValues().

    public Collection<? extends Object> getPlanningEntities() {

        return Arrays.asList(leftCloudProcess, rightCloudProcess);
    }
    public Collection<? extends Object> getPlanningValues() {
        return Arrays.asList(leftCloudProcess.getComputer(), rightCloudProcess.getComputer());
    }

A Move must implement the equals() and hashCode() methods. 2 moves which make the same change on a solution, should be equal.

    public boolean equals(Object o) {

        if (this == o) {
            return true;
        } else if (instanceof RowChangeMove) {
            RowChangeMove other = (RowChangeMove) o;
            return new EqualsBuilder()
                    .append(queen, other.queen)
                    .append(toRow, other.toRow)
                    .isEquals();
        } else {
            return false;
        }
    }
    public int hashCode() {
        return new HashCodeBuilder()
                .append(queen)
                .append(toRow)
                .toHashCode();
    }

Notice that it checks if the other move is an instance of the same move type. This instanceof check is important because a move will be compared to a move with another move type if you're using more then 1 move type.

It's also recommended to implement the toString() method as it allows you to read Planner's logging more easily:

    public String toString() {

        return queen + " => " + toRow;
    }

Now that we can implement a single custom Move, let's take a look at generating such custom moves.

The easiest way to generate custom moves is by implementing the interface MoveListFactory:

public interface MoveListFactory {


    List<Move> createMoveList(Solution solution);
}

For example:

public class RowChangeMoveFactory implements MoveListFactory {


    public List<Move> createMoveList(Solution solution) {
        NQueens nQueens = (NQueens) solution;
        List<Move> moveList = new ArrayList<Move>();
        for (Queen queen : nQueens.getQueenList()) {
            for (Row toRow : nQueens.getRowList()) {
                moveList.add(new RowChangeMove(queen, toRow));
            }
        }
        return moveList;
    }
}

Simple configuration (which can be nested in a unionMoveSelector just like any other MoveSelector):


    <moveListFactory>
      <moveListFactoryClass>org.drools.planner.examples.nqueens.solver.move.factory.RowChangeMoveFactory</moveListFactoryClass>
    </moveListFactory>

Advanced configuration:


    <moveListFactory>
      ... <!-- Normal moveSelector properties -->
      <moveListFactoryClass>org.drools.planner.examples.nqueens.solver.move.factory.RowChangeMoveFactory</moveListFactoryClass>
    </moveListFactory>

Because the MoveListFactory generates all moves at once in a List<Move>, it does not support cacheType JUST_IN_TIME. Therefore, moveListFactory uses cacheType STEP by default and it scales badly in memory footprint.