JBoss.org Community Documentation

- Prev
- Next

## Chapter 9. Local search

## 9.1. Overview

Local search starts from an initial solution and evolves that single solution into a mostly better and better solution. It uses a single search path of solutions, not a search tree. At each solution in this path it evaluates a number of moves on the solution and applies the most suitable move to take the step to the next solution. It does that for a high number of iterations until it's terminated (usually because its time has run out).

Local search acts a lot like a human planner: it uses a single search path and moves facts around to find a good feasible solution. Therefore it's pretty natural to implement.

Local search often needs to start from an initialized solution, therefore it's recommended to configure a construction heuristic solver phase before it.

## 9.2. Hill climbing (simple local search)

### 9.2.1. Algorithm description

Hill climbing tries all selected moves and then takes the best move, which is the move which leads to the solution with the highest score. That best move is called the step move. From that new solution, it again tries all selected moves and takes the best move and continues like that iteratively. If multiple selected moves tie for the best move, one of them is randomly chosen as the best move.

Notice that one a queen has moved, it can be moved again later. This is a good thing, because in an NP-complete problem it's impossible to predict what will be the optimal final value for a planning variable.

Hill climbing can easily get stuck in a local optima:

Improvements upon hill climbing (such as tabu search and simulated annealing) address the problem of being stuck in local optima.

## 9.3. Tabu search

### 9.3.1. Algorithm description

Tabu search works like hill climbing, but it maintains a tabu list to avoid getting stuck in local optima. The tabu list holds recently used objects that are taboo to use for now. Moves that involve an object in the tabu list, are not accepted. The tabu list objects can be anything related to the move, such as the planning entity, planning value, move, solution, ... Here's an example with entity tabu for 4 queens, so the queens are put in the tabu list:

See Tabu Search acceptor below.

## 9.4. Simulated annealing

### 9.4.1. Algorithm description

See Simulated Annealing acceptor below.

## 9.5. Late acceptance

### 9.5.1. Algorithm description

See Late Acceptance acceptor below.

## 9.6. About neighborhoods, moves and steps

### 9.6.1. Move generation tips

At each solution, local search will try all possible moves and pick the best move to change to the next solution. It's up to you to generate those moves. Let's take a look at all the possible moves on the starting solution of 4 queens:

It's highly recommended that you verify all solutions are connected by your move set. This means that by combining a finite number of moves you can reach any solution from any solution. Otherwise you're already excluding solutions at the start. Especially if you're using only big moves, you should check it. Just because big moves outperform small moves in a short test run, it doesn't mean that they will outperform them in a long test run.

You can mix different move types. Usually you're better off preferring small (fine-grained) moves over big (course-grained) moves because the score delta calculation will pay off more. However, as the traveling tournament example proves, if you can remove a hard constraint by using a certain set of big moves, you can win performance and scalability. Try it yourself: run both the simple (small moves) and the smart (big moves) version of the traveling tournament example. The smart version evaluates a lot less unfeasible solutions, which enables it to outperform and outscale the simple version.

### 9.6.2. A step

A step is the winning move. The local search solver tries every move on the current solution and picks the best accepted move as the step:

Figure 9.1. Decide the next step at step 0 (4 queens example)

Because the move B0 to B3 has the highest score (`-3`), it is picked as the next step. Notice that C0 to C3 (not shown) could also have been picked because it also has the score `-3`. If multiple moves have the same highest score, one is picked randomly, in this case B0 to B3.

The step is made and from that new solution, the local search solver tries all the possible moves again, to decide the next step after that. It continually does this in a loop, and we get something like this:

Figure 9.2. All steps (4 queens example)

Notice that the local search solver doesn't use a search tree, but a search path. The search path is highlighted by the green arrows. At each step it tries all possible moves, but unless it's the step, it doesn't investigate that solution further. This is one of the reasons why local search is very scalable.

As you can see, the local search solver solves the 4 queens problem by starting with the starting solution and make the following steps sequentially:

1. B0 to B3

2. D0 to B2

3. A0 to B1

If we turn on `DEBUG` logging for the category `org.drools.planner`, then those steps are shown into the log:

    INFO  Solving started: time spend (0), score (-6), new best score (-6), random seed (0).
    DEBUG     Step index (0), time spend (20), score (-3), new best score (-3), accepted/selected move count (12/12) for picked step (col1@row0 => row3).
    DEBUG     Step index (1), time spend (31), score (-1), new best score (-1), accepted/selected move count (12/12) for picked step (col0@row0 => row1).
    DEBUG     Step index (2), time spend (40), score (0), new best score (0), accepted/selected move count (12/12) for picked step (col3@row0 => row2).
    INFO  Phase localSearch ended: step total (3), time spend (41), best score (0).
    INFO  Solving ended: time spend (41), best score (0), average calculate count per second (1780).

Notice that the logging uses the `toString()` method of our `Move` implementation: `col1@row0 => row3`.

The local search solver solves the 4 queens problem in 3 steps, by evaluating only 37 possible solutions (3 steps with 12 moves each + 1 starting solution), which is only fraction of all 256 possible solutions. It solves 16 queens in 31 steps, by evaluating only 7441 out of 18446744073709551616 possible solutions. Note: with construction heuristics it's even a lot more efficient.

### 9.6.3. Getting stuck in local optima

A hill climber always takes improving moves. This may seem like a good thing, but it's not. It suffers from a number of problems:

- It can get stuck in a local optimum. For example if it reaches a solution X with a score -1 and there is no improving move, it is forced to take a next step that leads to a solution Y with score -2, after that however, it's very real that it will pick the step back to solution X with score -1. It will then start looping between solution X and Y.

- It can start walking in its own footsteps, picking the same next step at every step.

Of course Drools Planner implements better local searches, such as tabu search and simulated annealing which can avoid these problems. We recommend to never use a hill climber, unless you're absolutely sure there are no local optima in your planning problem.

## 9.7. Deciding the next step

The local search solver decides the next step with the aid of 3 configurable components:

- A `MoveSelector` which selects the possible moves of the current solution. See the chapter about Move and neighborhood selection.

- An acceptor which filters out unacceptable moves. It can also weigh a move it accepts.

- A forager which gathers all accepted moves and picks the next step from them.

Figure 9.3. Decide the next step at step 0 (4 queens example)

In the above example the selector generated the moves shown with the blue lines, the acceptor accepted all of them and the forager picked the move B0 to B3.

If we turn on `TRACE` logging for the category `org.drools.planner`, then the decision making is shown in the log:

    INFO  Solver started: time spend (0), score (-6), new best score (-6), random seed (0).
    TRACE         Ignoring not doable move (col0@row0 => row0).
    TRACE         Move index (1), score (-4), accepted (true) for move (col0@row0 => row1).
    TRACE         Move index (2), score (-4), accepted (true) for move (col0@row0 => row2).
    TRACE         Move index (3), score (-4), accepted (true) for move (col0@row0 => row3).
    ...
    TRACE         Move index (6), score (-3), accepted (true) for move (col1@row0 => row3).
    ...
    TRACE         Move index (9), score (-3), accepted (true) for move (col2@row0 => row3).
    ...
    TRACE         Move index (12), score (-4), accepted (true) for move (col3@row0 => row3).
    DEBUG     Step index (0), time spend (6), score (-3), new best score (-3), accepted/selected move count (12/12) for picked step (col1@row0 => row3).
    ...

Because the last solution can degrade (especially in tabu search and simulated annealing), the `Solver` remembers the best solution it has encountered through the entire search path. Each time the current solution is better than the last best solution, the current solution is cloned and referenced as the new best solution.

### 9.7.1. Acceptor

An acceptor is used (together with a forager) to active tabu search, simulated annealing, great deluge, ... For each move it checks whether it is accepted or not.

You can implement your own `Acceptor`, although the build-in acceptors should suffice for most needs. You can also combine multiple acceptors.

#### 9.7.1.1. Tabu search acceptor

When tabu search takes steps it creates tabu's. It does not accept a move as the next step if that move breaks tabu. Drools Planner implements several tabu types:

- Solution tabu makes recently visited solutions tabu. It does not accept a move that leads to one of those solutions. If you can spare the memory, don't be cheap on the tabu size.

          <acceptor>

              <solutionTabuSize>1000</solutionTabuSize>

          </acceptor>

- Move tabu makes recent steps tabu. It does not accept a move equal to one of those steps.

          <acceptor>

              <moveTabuSize>7</moveTabuSize>

          </acceptor>

- Undo move tabu makes the undo move of recent steps tabu.

          <acceptor>

              <undoMoveTabuSize>7</undoMoveTabuSize>

          </acceptor>

- Planning entity tabu makes the planning entities of recent steps tabu. For example, for N queens it makes the recently moved queens tabu.

          <acceptor>

              <planningEntityTabuSize>7</planningEntityTabuSize>

          </acceptor>

- Planning value tabu makes the planning values of recent steps tabu. For example, for N queens it makes the recently moved to rows tabu.

          <acceptor>

              <planningValueTabuSize>7</planningValueTabuSize>

          </acceptor>

You can even combine tabu types:

        <acceptor>

            <solutionTabuSize>1000</solutionTabuSize>

            <moveTabuSize>7</moveTabuSize>

        </acceptor>

If you pick a too small tabu size, your solver can still get stuck in a local optimum. On the other hand, with the exception of solution tabu, if you pick a too large tabu size, your solver can get stuck by bouncing of the walls. Use the benchmarker to fine tweak your configuration. Experiments teach us that it is generally best to use a prime number for the move tabu, undo move tabu, entity tabu or value tabu size.

A tabu search acceptor should be combined with a high subset selection, such as `1000`.

#### 9.7.1.2. Simulated annealing acceptor

Simulated annealing does not always pick the move with the highest score, neither does it evaluate many moves per step. At least at first. Instead, it gives non improving moves also a chance to be picked, depending on its score and the time gradient of the `Termination`. In the end, it gradually turns into a hill climber, only accepting improving moves.

In many use cases, simulated annealing surpasses tabu search. By changing a few lines of configuration, you can easily switch from tabu search to simulated annealing and back.

Start with a `simulatedAnnealingStartingTemperature` set to the maximum score delta a single move can cause. Use the `Benchmarker` to tweak the value.

        <acceptor>

          <simulatedAnnealingStartingTemperature>2hard/100soft</simulatedAnnealingStartingTemperature>

        </acceptor>

        <forager>

            <minimalAcceptedSelection>4</minimalAcceptedSelection>

        </forager>

A simulated annealing acceptor should be combined with a low subset selection. The classic algorithm uses a `minimalAcceptedSelection` of `1`, but usually `4` performs better.

You can even combine it with a tabu acceptor at the same time. Use a lower tabu size than in a pure tabu search configuration.

        <acceptor>

          <simulatedAnnealingStartingTemperature>10.0</simulatedAnnealingStartingTemperature>

          <planningEntityTabuSize>5</planningEntityTabuSize>

        </acceptor>

        <forager>

            <minimalAcceptedSelection>4</minimalAcceptedSelection>

        </forager>

This differs from phasing, another powerful technique, where first simulated annealing is used, followed by tabu search.

#### 9.7.1.3. Late acceptance acceptor

Late acceptance accepts any move that has a score which is higher than the best score of a number of steps ago. That number of steps is the `lateAcceptanceSize`.

        <acceptor>

          <lateAcceptanceSize>500</lateAcceptanceSize>

        </acceptor>

        <forager>

            <minimalAcceptedSelection>1000</minimalAcceptedSelection>

        </forager>

### 9.7.2. Forager

A forager gathers all accepted moves and picks the move which is the next step. Normally it picks the accepted move with the highest score. If several accepted moves have the highest score, one is picked randomly.

You can implement your own `Forager`, although the build-in forager should suffice for most needs.

#### 9.7.2.1. Subset selection

When there are many possible moves, it becomes inefficient to evaluate all of them at every step. To evaluate only a random subset of all the moves, use:

- An `minimalAcceptedSelection` integer, which specifies how many accepted moves should have be evaluated during each step. By default it is positive infinity, so all accepted moves are evaluated at every step.

        <forager>

          <minimalAcceptedSelection>1000</minimalAcceptedSelection>

        </forager>

Unlike the n queens problem, real world problems require the use of subset selection. Start from an `minimalAcceptedSelection` that takes a step in less then 2 seconds. Turn on INFO logging to see the step times. Use the `Benchmarker` to tweak the value.

## Important

With a low `minimalAcceptedSelection` it is recommended to avoid using `selectionOrder` SHUFFLED because the shuffling generates a random number for every element in the selector, taking up a lot of time, but only a few are actually selected.

#### 9.7.2.2. Pick early type

A forager can pick a move early during a step, ignoring subsequent selected moves. There are 3 pick early types:

- `NEVER`: A move is never picked early: all accepted moves are evaluated that the selection allows. This is the default.

          <forager>

              <pickEarlyType>NEVER</pickEarlyType>

          </forager>

- `FIRST_BEST_SCORE_IMPROVING`: Pick the first accepted move that improves the best score. If none improve the best score, it behaves exactly like the pickEarlyType NEVER.

          <forager>

              <pickEarlyType>FIRST_BEST_SCORE_IMPROVING</pickEarlyType>

          </forager>

- `FIRST_LAST_STEP_SCORE_IMPROVING`: Pick the first accepted move that improves the last step score. If none improve the last step score, it behaves exactly like the pickEarlyType NEVER.

          <forager>

              <pickEarlyType>FIRST_LAST_STEP_SCORE_IMPROVING</pickEarlyType>

          </forager>

## 9.8. Using a custom Termination, MoveSelector, EntitySelector, ValueSelector or Acceptor

You can plug in a custom `Termination`, `MoveSelector`, `EntitySelector`, `ValueSelector` or `Acceptor` by extending the abstract class and also the related `*Config` class.

For example, to use a custom `MoveSelector`, extend the `AbstractMoveSelector` class, extend the `MoveSelectorConfig` class and configure it in the solver configuration.

## Note

It's not possible to directly inject a `Termination`, ... instance, instead of also extend a `Config` class because:

- A `SolverFactory` can build multiple `Solver` instances, which each require a distinct `Termination`, ... instance.

- A solver configuration needs to be serializable to and from XML. This makes benchmarking with `PlannerBenchmark` particularly easy because you can configure different `Solver` variants in XML.

- A `Config` class is often easier and clearer to configure. For example: `TerminationConfig` translates `maximumMinutesSpend` and `maximumSecondsSpend` into `maximumTimeMillisSpend`.

If you build a better implementation that's not domain specific, consider contributing it back as a pull request on github and we'll optimize it and take it along in future refactors.

- PrevChapter 8. Construction heuristics
- Top of page
- Front page
- NextChapter 10. Evolutionary algorithms