JBoss.orgCommunity Documentation

Chapter 6. Optimization algorithms

6.1. Search space size in the real world
6.2. Does Planner find the optimal solution?
6.3. Architecture overview
6.4. Optimization algorithms overview
6.5. Which optimization algorithms should I use?
6.6. SolverPhase
6.7. Scope overview
6.8. Termination
6.8.1. TimeMillisSpendTermination
6.8.2. ScoreAttainedTermination
6.8.3. StepCountTermination
6.8.4. UnimprovedStepCountTermination
6.8.5. Combining multiple Terminations
6.8.6. Asynchronous termination from another thread
6.9. SolverEventListener
6.10. Custom SolverPhase

The number of possible solutions for a planning problem can be mind blowing. For example:

For comparison: the minimal number of atoms in the known universe (10^80). As a planning problem gets bigger, the search space tends to blow up really fast. Adding only 1 extra planning entity or planning value can heavily multiply the running time of some algorithms.

An algorithm that checks every possible solution (even with pruning) can easily run for billions of years on a single real-life planning problem. What we really want is to find the best solution in the limited time at our disposal. Planning competitions (such as the International Timetabling Competition) show that local search variations (tabu search, simulated annealing, ...) usually perform best for real-world problems given real-world time limitations.

The business wants the optimal solution, but they also have other requirements:

Given these requirements, and despite the promises of some salesmen, it's usually impossible for anyone or anything to find the optimal solution. Therefore, Planner focuses on finding the best solution in available time. In realistic, independent competitions, Planner often comes out as the best reusable software.

The nature of NP-complete problems make scaling a prime concern. The result quality of a small dataset guarantees nothing about the result quality of a large dataset. Scaling problems cannot be mitigated by hardware purchases. Start testing with a production sized dataset as soon as possible. Don't asses quality on small datasets (unless production encounters such datasets). Instead, solve a production sized dataset and compare with the results of longer execution, different algorithms and - if available - the human planner.

OptaPlanner is the first framework to combine optimization algorithms (metaheuristics, ...) with score calculation by a rule engine such as Drools Expert. This combination turns out to be a very efficient, because:


If you want to learn more about metaheuristics, read the free book Essentials of Metaheuristics or Clever Algorithms.

The best optimization algorithms configuration for your use case depends heavily on your use case. Nevertheless, this vanilla recipe will get you into the game with a pretty good configuration, probably much better than what you're used to.

Start with a quick configuration that involves little or no configuration and optimization code:

Next, implement planning entity difficulty comparison and turn it into:

Next, implement moves and add tabu search behind it:

At this point the free lunch is over. The return on invested time lowers. The result is probably already more than good enough.

But you can do even better, at a lower return on invested time. Use the Benchmarker and try a couple of Simulated Annealing or Late Acceptance configurations, for example:

And combine them with Tabu Search:

If you have time, continue experimenting even further. Blog about your experiments!

A Solver can use multiple optimization algorithms in sequence. Each optimization algorithm is represented by a SolverPhase. There is never more than 1 SolverPhase solving at the same time.

Here's a configuration that runs 3 phases in sequence:


<solver>
  ...
  <constructionHeuristic>
    ... <!-- First phase: First Fit Decreasing -->
  </constructionHeuristic>
  <localSearch>
    ... <!-- Second phase: Simulated Annealing -->
  </localSearch>
  <localSearch>
    ... <!-- Third phase: Tabu Search -->
  </localSearch>
</solver>

The solver phases are run in the order defined by solver configuration. When the first phase terminates, the second phase starts, and so on. When the last phase terminates, the Solver terminates. Usually, a solver will first run a construction heuristic and then run 1 or multiple metaheuristics:

Some phases (especially construction heuristics) will terminate automatically. Other phases (especially metaheuristics) will only terminate if the phase is configured to terminate:


<solver>
  ...
  <termination><!-- Solver termination -->
    <maximumSecondsSpend>90</maximumSecondsSpend>
  </termination>
  <localSearch>
    <termination><!-- Phase termination -->
      <maximumSecondsSpend>60</maximumSecondsSpend><!-- Give the next phase a chance to run too, before the Solver terminates -->
    </termination>
    ...
  </localSearch>
  <localSearch>
    ...
  </localSearch>
</solver>

If the Solver terminates (before the last phase terminates itself), the current phase is terminated and all subsequent phases won't run.

A solver will iteratively run phases. Each phase will usually iteratively run steps. Each step, in turn, usually iteratively runs moves. These form 4 nested scopes: solver, phase, step and move.

Configure logging to display the log messages of each scope.

Not all phases terminate automatically and sometimes you don't want to wait that long anyway. A Solver can be terminated synchronously by up-front configuration or asynchronously from another thread.

Especially metaheuristic phases will need to be told when to stop solving. This can be because of a number of reasons: the time is up, the perfect score has been reached, ... The only thing you can't depend on, is on finding the optimal solution (unless you know the optimal score), because a metaheuristic algorithm generally doesn't know it when it finds the optimal solution. For real-life problems this doesn't turn out to be much of a problem, because finding the optimal solution could take billions of years, so you 'll want to terminate sooner anyway. The only thing that matters is finding the best solution in the available time.

For synchronous termination, configure a Termination on a Solver or a SolverPhase when it needs to stop. You can implement your own Termination, but the build-in implementations should suffice for most needs. Every Termination can calculate a time gradient (needed for some optimization algorithms), which is a ratio between the time already spend solving and the estimated entire solving time of the Solver or SolverPhase.

Each time a new best solution is found, the Solver fires a BestSolutionChangedEvent.

To listen to such events, add a SolverEventListener to the Solver:

public interface Solver {


    // ...
    void addEventListener(SolverEventListener eventListener);
    void removeEventListener(SolverEventListener eventListener);
}

Between phases or before the first phase, you might want to execute a custom action on the Solution to get a better score. Yet you'll still want to reuse the score calculation. For example, to implement a custom construction heuristic without implementing an entire SolverPhase.

Implement the CustomSolverPhaseCommand interface:

public interface CustomSolverPhaseCommand {


    void changeWorkingSolution(ScoreDirector scoreDirector);
}

For example:

public class ExaminationSolutionInitializer implements CustomSolverPhaseCommand {


    public void changeWorkingSolution(ScoreDirector scoreDirector) {
        Examination examination = (Examination) scoreDirector.getWorkingSolution();
        for (Exam exam : examination.getExamList()) {
            Score unscheduledScore = scoreDirector.calculateScore();
            ...
            for (Period period : examination.getPeriodList()) {
                scoreDirector.beforeVariableChanged(exam, "period");
                exam.setPeriod(period)
                scoreDirector.afterVariableChanged(exam, "period");
                Score score = scoreDirector.calculateScore();
                ...
            }
            ...
        }
    }
}

And configure it like this:


<solver>
  ...
  <customSolverPhase>
    <customSolverPhaseCommandClass>org.optaplanner.examples.examination.solver.solution.initializer.ExaminationSolutionInitializer</customSolverPhaseCommandClass>
  </customSolverPhase>
  ... <!-- Other phases -->
</solver>

It's possible to configure multiple customSolverPhaseCommandClass instances, which will be run in sequence.

Important

If the changes of a CustomSolverPhaseCommand don't result in a better score, the best solution won't be changed (so effectively nothing will have changed for the next SolverPhase or CustomSolverPhaseCommand). To force such changes anyway, use forceUpdateBestSolution:


  <customSolverPhase>
    <customSolverPhaseCommandClass>...MyUninitializer</customSolverPhaseCommandClass>
    <forceUpdateBestSolution>true</forceUpdateBestSolution>
  </customSolverPhase>

Note

If the Solver or SolverPhase wants to terminate while a CustomSolverPhaseCommand is still running, it will wait to terminate until the CustomSolverPhaseCommand is done, however long that takes.