4.4. Use the Solver

4.4.1. The Solver Interface

A Solver implementation will solve your planning problem.
public interface Solver {
    
    void solve(Solution planningProblem);

    Solution getBestSolution();

    // ...

}
A Solver can only solve one planning problem instance at a time. A Solver should only be accessed from a single thread, except for the methods that are specifically javadocced as being thread-safe. It is built with a SolverFactory, do not implement or build it yourself.

4.4.2. Solving a Problem

Solving a problem is quite easy once you have:
  • A Solver built from a solver configuration
  • A Solution that represents the planning problem instance
Just set the planning problem, solve it and extract the best solution:
    solver.solve(planningProblem);
    Solution bestSolution = solver.getBestSolution();
For example in n queens, the getBestSolution() method will return an NQueens instance with every Queen assigned to a Row.

Figure 4.2. Best Solution for the 4 Queens Puzzle in 8ms (Also an Optimal Solution)

Best Solution for the 4 Queens Puzzle in 8ms (Also an Optimal Solution)
The solve(Solution) method can take a long time (depending on the problem size and the solver configuration). The Solver will remember (actually clone) the best solution it encounters during its solving. Depending on a number factors (including problem size, how much time the Solver has, the solver configuration, ...), that best solution will be a feasible or even an optimal solution.
Note
The Solution instance given to the method solve(Solution) will be changed by the Solver, but do not mistake it for the best solution.
The Solution instance returned by the getBestSolution() method will most likely be a clone of the instance given to the method solve(Solution), which means it is a different instance.
Note
The Solution instance given to the solve(Solution) method does not need to be uninitialized. It can be partially or fully initialized, which is likely to be the case in repeated planning.

4.4.3. Environment Mode: Are There Bugs in my Code?

The environment mode allows you to detect common bugs in your implementation. It does not affect the logging level.
You can set the environment mode in the solver configuration XML file:
<solver>
  <environmentMode>FAST_ASSERT</environmentMode>
  ...
</solver>
A solver has a single Random instance. Some solver configurations use the Random instance a lot more than others. For example Simulated Annealing depends highly on random numbers, while Tabu Search only depends on it to deal with score ties. The environment mode influences the seed of that Random instance.
These are the environment modes:

4.4.3.1. FULL_ASSERT

The FULL_ASSERT mode turns on all assertions (such as assert that the incremental score calculation is uncorrupted for each move) to fail-fast on a bug in a Move implementation, a score rule, the rule engine itself, ...
This mode is reproducible (see the reproducible mode). It is also intrusive because it calls the method calculateScore() more frequently than a non-assert mode.
The FULL_ASSERT mode is horribly slow (because it does not rely on incremental score calculation).

4.4.3.2. NON_INTRUSIVE_FULL_ASSERT

The NON_INTRUSIVE_FULL_ASSERT turns on several assertions to fail-fast on a bug in a Move implementation, a score rule, the rule engine itself, ...
This mode is reproducible (see the reproducible mode). It is non-intrusive because it does not call the method calculateScore() more frequently than a non assert mode.
The NON_INTRUSIVE_FULL_ASSERT mode is horribly slow (because it does not rely on incremental score calculation).

4.4.3.3. FAST_ASSERT

The FAST_ASSERT mode turns on most assertions (such as assert that an undoMove's score is the same as before the Move) to fail-fast on a bug in a Move implementation, a score rule, the rule engine itself, ...
This mode is reproducible (see the reproducible mode). It is also intrusive because it calls the method calculateScore() more frequently than a non assert mode.
The FAST_ASSERT mode is slow.
It is recommended to write a test case that does a short run of your planning problem with the FAST_ASSERT mode on.

4.4.3.4. REPRODUCIBLE (default)

The reproducible mode is the default mode because it is recommended during development. In this mode, two runs in the same Planner version will execute the same code in the same order. Those two runs will have the same result at every step, except if the note below applies. This enables you to reproduce bugs consistently. It also allows you to benchmark certain refactorings (such as a score constraint performance optimization) fairly across runs.
Note
Despite the reproducible mode, your application might still not be fully reproducible because of:
  • Use of HashSet (or another Collection which has an inconsistent order between JVM runs) for collections of planning entities or planning values (but not normal problem facts), especially in the Solution implementation. Replace it with LinkedHashSet.
  • Combining a time gradient dependent algorithms (most notably Simulated Annealing) together with time spent termination. A sufficiently large difference in allocated CPU time will influence the time gradient values. Replace Simulated Annealing with Late Acceptance. Or instead, replace time spent termination with step count termination.
The reproducible mode is slightly slower than the production mode. If your production environment requires reproducibility, use this mode in production too.
In practice, this mode uses the default, fixed random seed if no seed is specified, and it also disables certain concurrency optimizations (such as work stealing).

4.4.3.5. PRODUCTION

The production mode is the fastest, but it is not reproducible. It is recommended for a production environment, unless reproducibility is required.
In practice, this mode uses no fixed random seed if no seed is specified.

4.4.4. Logging Level: What is the Solver Doing?

The best way to illuminate the black box that is a Solver, is to play with the logging level:
  • error: Log errors, except those that are thrown to the calling code as a RuntimeException.
    Note
    If an error happens, Planner normally fails fast: it throws a subclass of RuntimeException with a detailed message to the calling code. It does not log it as an error itself to avoid duplicate log messages. Except if the calling code explicitly catches and eats that RuntimeException, a Thread's default ExceptionHandler will log it as an error anyway. Meanwhile, the code is disrupted from doing further harm or obfuscating the error.
  • warn: Log suspicious circumstances.
  • info: Log every phase and the solver itself. See scope overview.
  • debug: Log every step of every phase. See scope overview.
  • trace: Log every move of every step of every phase. See scope overview.
    Note
    Turning on trace logging, will slow down performance considerably: it is often four times slower. However, it is invaluable during development to discover a bottleneck.
    Even debug logging can slow down performance considerably for fast stepping algorithms (such as Late Acceptance and Simulated Annealing), but not for slow stepping algorithms (such as Tabu Search).
For example, set it to debug logging, to see when the phases end and how fast steps are taken:
INFO  Solving started: time spent (3), best score (uninitialized/0), random (JDK with seed 0).
DEBUG     CH step (0), time spent (5), score (0), selected move count (1), picked move (Queen-2 {null -> Row-0}).
DEBUG     CH step (1), time spent (7), score (0), selected move count (3), picked move (Queen-1 {null -> Row-2}).
DEBUG     CH step (2), time spent (10), score (0), selected move count (4), picked move (Queen-3 {null -> Row-3}).
DEBUG     CH step (3), time spent (12), score (-1), selected move count (4), picked move (Queen-0 {null -> Row-1}).
INFO  Construction Heuristic phase (0) ended: step total (4), time spent (12), best score (-1).
DEBUG     LS step (0), time spent (19), score (-1),     best score (-1), accepted/selected move count (12/12), picked move (Queen-1 {Row-2 -> Row-3}).
DEBUG     LS step (1), time spent (24), score (0), new best score (0), accepted/selected move count (9/12), picked move (Queen-3 {Row-3 -> Row-2}).
INFO  Local Search phase (1) ended: step total (2), time spent (24), best score (0).
INFO  Solving ended: time spent (24), best score (0), average calculate count per second (1625).
All time spent values are in milliseconds.
Everything is logged to SLF4J, which is a simple logging facade which delegates every log message to Logback, Apache Commons Logging, Log4j or java.util.logging. Add a dependency to the logging adaptor for your logging framework of choice.
If you are not using any logging framework yet, use Logback by adding this Maven dependency (there is no need to add an extra bridge dependency):
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.x</version>
    </dependency>
Configure the logging level on the org.optaplanner package in your logback.xml file:
<configuration>

  <logger name="org.optaplanner" level="debug"/>

  ...

<configuration>
If instead, you are still using Log4J 1.x (and you do not want to switch to its faster successor, Logback), add the bridge dependency:
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.x</version>
    </dependency>
And configure the logging level on the package org.optaplanner in your log4j.xml file:
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">

  <category name="org.optaplanner">
    <priority value="debug" />
  </category>

  ...

</log4j:configuration>
Note
In a multitenant application, multiple Solver instances might be running at the same time. To separate their logging into distinct files, surround the solve() call with an MDC:
        MDC.put("tenant.name",tenantName);
        solver.solve(planningProblem);
        Solution bestSolution = solver.getBestSolution();
        MDC.remove("tenant.name");
Then configure your logger to use different files for each ${tenant.name}. For example in Logback, use a SiftingAppender in logback.xml:
  <appender name="fileAppender" class="ch.qos.logback.classic.sift.SiftingAppender">
    <discriminator>
      <key>tenant.name</key>
      <defaultValue>unknown</defaultValue>
    </discriminator>
    <sift>
      <appender name="fileAppender.${tenant.name}" class="...FileAppender">
        <file>local/log/optaplanner-${tenant.name}.log</file>
        ...
      </appender>
    </sift>
  </appender>

4.4.5. Random Number Generator

Many heuristics and metaheuristics depend on a pseudorandom number generator for move selection, to resolve score ties, probability based move acceptance, ... During solving, the same Random instance is reused to improve reproducibility, performance and uniform distribution of random values.
To change the random seed of that Random instance, specify a randomSeed:
<solver>
  <randomSeed>0</randomSeed>
  ...
</solver>
To change the pseudorandom number generator implementation, specify a randomType:
<solver>
  <randomType>MERSENNE_TWISTER</randomType>
  ...
</solver>
The following types are supported:
  • JDK (default): Standard implementation (java.util.Random).
  • MERSENNE_TWISTER: Implementation by Commons Math.
  • WELL512A, WELL1024A, WELL19937A, WELL19937C, WELL44497A and WELL44497B: Implementation by Commons Math.
For most use cases, the randomType has no significant impact on the average quality of the best solution on multiple datasets. If you want to confirm this on your use case, use the benchmarker.