Score
Solver
Move
and Neighborhood SelectionMove
and Neighborhood IntroductionMoveSelector
sSelector
FeaturesCacheType
: Create Moves Ahead of Time or Just In TimeCacheType
and SelectionOrder
Solver
Configurationoptaplanner-benchmark
PlannerBenchmark
OptaPlanner is a lightweight, embeddable constraint satisfaction engine which optimizes planning problems. It solves use cases such as:
Employee shift rostering: timetabling nurses, repairmen, ...
Agenda scheduling: scheduling meetings, appointments, maintenance jobs, advertisements, ...
Educational timetabling: scheduling lessons, courses, exams, conference presentations, ...
Vehicle routing: planning vehicles (trucks, trains, boats, airplanes, ...) with freight and/or people
Bin packing: filling containers, trucks, ships and storage warehouses, but also cloud computers nodes, ...
Job shop scheduling: planning car assembly lines, machine queue planning, workforce task planning, ...
Cutting stock: minimizing waste while cutting paper, steel, carpet, ...
Sport scheduling: planning football leagues, baseball leagues, ...
Financial optimization: investment portfolio optimization, risk spreading, ...
Every organization faces planning problems: provide products or services with a limited set of constrained resources (employees, assets, time and money). OptaPlanner optimizes such planning to do more business with less resources. This is known as Constraint Satisfaction Programming (which is part of the Operations Research discipline).
OptaPlanner helps normal JavaTM programmers solve constraint satisfaction problems efficiently. Under the hood, it combines optimization heuristics and metaheuristics with very efficient score calculation.
OptaPlanner is open source software, released under the Apache Software License 2.0. This license is very liberal and allows reuse for commercial purposes. Read the layman's explanation.
OptaPlanner is 100% pure JavaTM and runs on any JVM 1.6 or higher. It integrates very easily with other JavaTM technologies. OptaPlanner is available in the Maven Central Repository.
All the use cases above are probably NP-complete or harder. In layman's terms, NP-complete means:
It's easy to verify a given solution to a problem in reasonable time.
There is no silver bullet to find the optimal solution of a problem in reasonable time (*).
(*) At least, none of the smartest computer scientists in the world have found such a silver bullet yet. But if they find one for 1 NP-complete problem, it will work for every NP-complete problem.
In fact, there's a $ 1,000,000 reward for anyone that proves if such a silver bullet actually exists or not.
The implication of this is pretty dire: solving your problem is probably harder than you anticipated, because the 2 common techniques won't suffice:
A Brute Force algorithm (even a smarter variant) will take too long.
A quick algorithm, for example in bin packing, putting in the largest items first, will return a solution that is far from optimal.
By using advanced optimization algorithms, OptaPlanner does find a good solution in reasonable time for such planning problems.
Usually, a planning problem has at least 2 levels of constraints:
Download a release zip of OptaPlanner from the OptaPlanner website and unzip it.
Open the directory examples
and run the script.
Linux or Mac:
$ cd examples
$ ./runExamples.sh
Windows:
$ cd examples
$ runExamples.bat
The Examples GUI application will open. Pick an example to try it out:
OptaPlanner itself has no GUI dependencies. It runs just as well on a server or a mobile JVM as it does on the desktop.
Besides the GUI examples, there are also a set of webexamples to try out:
Download a JEE application server, such as JBoss EAP or WildFly and unzip it.
Download a release zip of OptaPlanner from the OptaPlanner website and unzip it.
Open the directory webexamples
and deploy the
optaplanner-webexamples-*.war
file on the JEE application server.
Surf to http://localhost:8080/optaplanner-webexamples-*/
(replace the *
with the actual version).
The webexamples (but not OptaPlanner itself) require several JEE API's (such as Servlet, JAX-RS and CDI)
to run. To successfully deploy optaplanner-webexamples-*.war
on a servlet container (such as
Jetty or Tomcat), instead of on a real JEE application server (such as WildFly), add the missing implementation
libraries (for example RestEasy and Weld) in the war manually.
Pick an example to try it out, such as the Vehicle Routing example:
The OptaPlanner jars are also available in the central maven repository (and also in the JBoss maven repository).
If you use Maven, add a dependency to optaplanner-core
in your project's
pom.xml
:
<dependency>
<groupId>org.optaplanner</groupId>
<artifactId>optaplanner-core</artifactId>
</dependency>
This is similar for Gradle, Ivy and Buildr. To identify the latest version, check the central maven repository.
Because you might end up using other OptaPlanner modules too, it's recommended to import the
optaplanner-bom
in Maven's dependencyManagement
so the OptaPlanner version
is specified only once:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.optaplanner</groupId>
<artifactId>optaplanner-bom</artifactId>
<type>pom</type>
<version>...</version>
<scope>import</scope>
</dependency>
...
</dependencies>
</dependencyManagement>
If you're still using ANT (without Ivy), copy all the jars from the download zip's
binaries
directory in your classpath.
The download zip's binaries
directory contains far more jars then
optaplanner-core
actually uses. It also contains the jars used by other modules, such as
optaplanner-benchmark
.
Check the maven repository pom.xml
files to determine the minimal dependency set of a
specific module (for a specific version).
It's easy to build OptaPlanner from source:
Set up Git and clone
optaplanner
from GitHub (or alternatively, download the zipball):
$ git clone git@github.com:droolsjbpm/optaplanner.git optaplanner
...
If you don't have a GitHub account or your local Git installation isn't configured with it, use this command instead, to avoid an authentication issue:
$ git clone https://github.com/droolsjbpm/optaplanner.git optaplanner
...
Build it with Maven:
$ cd optaplanner
$ mvn clean install -DskipTests
...
The first time, Maven might take a long time, because it needs to download jars.
Run the examples:
$ cd optaplanner-examples
$ mvn exec:java
...
Edit the sources in your favorite IDE.
Optional: use a Java profiler.
OptaPlanner separates its API and implementation:
Public API: All classes in the package namespace org.optaplanner.core.api are 100% backwards compatible
in future releases (especially minor and hotfix releases). In rare circumstances, if the major version number
changes, a few specific classes might have a few backwards incompatible changes, but those will be clearly
documented in the recipe UpgradeFromPreviousVersionRecipe.txt
.
XML configuration: The XML solver configuration is backwards compatible for all elements, except for elements that require the use of non public API classes. The XML solver configuration is defined by the classes in the package namespace org.optaplanner.core.config.
Implementation classes: All classes in the package namespace org.optaplanner.core.impl are not backwards compatible: they will
change in future major or minor releases (but probably not in hotfix releases). The recipe UpgradeFromPreviousVersionRecipe.txt
describes every such relevant change and on how to quickly deal with it when upgrading to a newer version.
That recipe file is included in every release zip.
This documentation covers some impl classes too. Those documented impl classes are reliable and safe to use (unless explicitly marked as experimental in this documentation), but we're just not entirely comfortable yet to write their signatures in stone.
For news and articles, check our blog, Google+ (OptaPlanner, Geoffrey De Smet) and twitter (OptaPlanner, Geoffrey De Smet). If OptaPlanner helps you, help us by blogging or tweeting about it!
Public questions are welcome on our community forum. Bugs and feature requests are welcome in our issue tracker. Pull requests are very welcome on GitHub and get priority treatment! By open sourcing your improvements, you 'll benefit from our peer review and from our improvements made on top of your improvements.
Red Hat sponsors OptaPlanner development by employing the core team. For enterprise support and consulting, take a look at the BRMS and BPMS products (which contain OptaPlanner) or contact Red Hat.
OptaPlanner is part of the KIE group of projects. It releases regularly (often once or twice per month) together with the Drools rule engine and the jBPM workflow engine.
See the architecture overview to learn more about the optional integration with Drools.
The following hard constraints must be fulfilled:
The following soft constraints should be optimized:
Beginning with the domain model:
In the UML class diagram above, the Planner concepts are already annotated:
Try it yourself. Download and configure the examples in your
preferred IDE. Run org.optaplanner.examples.cloudbalancing.app.CloudBalancingHelloWorld
.
By default, it is configured to run for 120 seconds. It will execute this code:
The code example does the following:
Build the Solver
based on a solver configuration (in this case an XML file from the
classpath).
SolverFactory solverFactory = SolverFactory.createFromXmlResource(
"org/optaplanner/examples/cloudbalancing/solver/cloudBalancingSolverConfig.xml");
Solver solver = solverFactory.buildSolver();
Load the problem. CloudBalancingGenerator
generates a random problem: you will
replace this with a class that loads a real problem, for example from a database.
CloudBalance unsolvedCloudBalance = new CloudBalancingGenerator().createCloudBalance(400, 1200);
Solve the problem.
solver.solve(unsolvedCloudBalance);
CloudBalance solvedCloudBalance = (CloudBalance) solver.getBestSolution();
Display the result.
System.out.println("\nSolved cloudBalance with 400 computers and 1200 processes:\n"
+ toDisplayString(solvedCloudBalance));
The only complicated part is building the Solver
, as detailed in the next section.
Take a look at the solver configuration:
This solver configuration consists of three parts:
Domain model configuration: What can Planner change? We need to make
Planner aware of our domain classes. In this configuration, it will automatically scan all classes in your
classpath (for an @PlanningEntity
or @PlanningSolution
annotation):
<scanAnnotatedClasses/>
Score configuration: How should Planner optimize the planning
variables? What is our goal? Since we have hard and soft constraints, we use a
HardSoftScore
. But we also need to tell Planner how to calculate the score, depending on
our business requirements. Further down, we will look into two alternatives to calculate the score: using a
simple Java implementation, or using Drools DRL.
<scoreDirectorFactory>
<scoreDefinitionType>HARD_SOFT</scoreDefinitionType>
<easyScoreCalculatorClass>org.optaplanner.examples.cloudbalancing.solver.score.CloudBalancingEasyScoreCalculator</easyScoreCalculatorClass>
<!--<scoreDrl>org/optaplanner/examples/cloudbalancing/solver/cloudBalancingScoreRules.drl</scoreDrl>-->
<initializingScoreTrend>ONLY_DOWN</initializingScoreTrend>
</scoreDirectorFactory>
Optimization algorithms configuration: How should Planner optimize it and for how long? In this case, we terminate it after 60 seconds and use the default optimization algorithms (because no explicit optimization algorithms are configured). The default algorithms should already easily surpass human planners and most in-house implementations. Use the Benchmarker to power tweak it to get even better results.
<termination>
<secondsSpentLimit>60</secondsSpentLimit>
</termination>
Let's examine the domain model classes and the score configuration.
Let's take a look at two different implementations:
One way to define a score function is to implement the interface EasyScoreCalculator
in
plain Java.
<scoreDirectorFactory>
<scoreDefinitionType>HARD_SOFT</scoreDefinitionType>
<easyScoreCalculatorClass>org.optaplanner.examples.cloudbalancing.solver.score.CloudBalancingEasyScoreCalculator</easyScoreCalculatorClass>
</scoreDirectorFactory>
Just implement the calculateScore(Solution)
method to return a
HardSoftScore
instance.
Even if we optimize the code above to use Map
s to iterate through the
processList
only once, it is still slow because it does not
do incremental score calculation. To fix that, either use an incremental Java score function or a Drools score
function. Let's take a look at the latter.
To use the Drools rule engine as a score function, simply add a scoreDrl
resource in
the classpath:
<scoreDirectorFactory>
<scoreDefinitionType>HARD_SOFT</scoreDefinitionType>
<scoreDrl>org/optaplanner/examples/cloudbalancing/solver/cloudBalancingScoreRules.drl</scoreDrl>
</scoreDirectorFactory>
Next, if those constraints are met, we want to minimize the maintenance cost, so we add that as a soft constraint:
Example 2.8. cloudBalancingScoreRules.drl - Soft Constraints
// ############################################################################
// Soft constraints
// ############################################################################
rule "computerCost"
when
$computer : CloudComputer($cost : cost)
exists CloudProcess(computer == $computer)
then
scoreHolder.addSoftConstraintMatch(kcontext, - $cost);
end
If you use the Drools rule engine for score calculation, you can integrate with other Drools technologies, such as decision tables (XLS or web based), the KIE Workbench, ...
Table 3.1. Examples Overview
Example | Domain | Size | Competition? | Special features used |
---|---|---|---|---|
N queens |
|
|
| None |
Cloud balancing |
|
|
| |
Traveling salesman |
|
|
| |
Dinner party |
|
|
|
|
Tennis club scheduling |
|
|
| |
Course timetabling |
|
|
| |
Machine reassignment |
|
|
| |
Vehicle routing |
|
|
|
|
Vehicle routing with time windows | Extra on Vehicle routing:
|
|
| Extra on Vehicle routing:
|
Project job scheduling |
|
|
| |
Hospital bed planning |
|
|
| |
Exam timetabling |
|
|
|
|
Employee rostering |
|
|
| |
Traveling tournament |
|
|
|
|
Cheap time scheduling |
|
|
| |
Investment |
|
|
|
A realistic competition is an official, independent competition:
that clearly defines a real-word use case
with real-world constraints
with multiple, real-world datasets
that expects reproducible results within a specific time limit on specific hardware
that has had serious participation from the academic and/or enterprise Operations Research community
These realistic competitions provide an objective comparison of Planner with competitive software and academic research.
This documentation heavily uses the 4 queens puzzle as the primary example.
The above solution is wrong because queens A1
and B0
can attack each
other (so can queens B0
and D0
). Removing queen B0
would respect the "no 2 queens can attack each other" constraint, but would break the "place n queens"
constraint.
Below is a correct solution:
All the constraints have been met, so the solution is correct. Note that most n queens puzzles have multiple correct solutions. We'll focus on finding a single correct solution for a given n, not on finding the number of possible correct solutions for a given n.
public class Column {
private int index;
// ... getters and setters
}
public class Row {
private int index;
// ... getters and setters
}
public class Queen {
private Column column;
private Row row;
public int getAscendingDiagonalIndex() {...}
public int getDescendingDiagonalIndex() {...}
// ... getters and setters
}
public class NQueens implements Solution<SimpleScore> {
private int n;
private List<Column> columnList;
private List<Row> rowList;
private List<Queen> queenList;
private SimpleScore score;
// ... getters and setters
}
When 2 queens share the same column, row or diagonal line, such as (*) and (**), they can attack each other.
Given a list of cities, find the shortest tour for a salesman that visits each city exactly once.
The problem is defined by Wikipedia. It is one of the most intensively studied problems in computational mathematics. Yet, in the real world, it's often only part of a planning problem, along with other constraints, such as employee shift rostering constraints.
Miss Manners is throwing another dinner party.
This time she invited 144 guests and prepared 12 round tables with 12 seats each.
Every guest should sit next to someone (left and right) of the opposite gender.
And that neighbour should have at least one hobby in common with the guest.
And the 2 politicians, 2 doctors, 2 coaches and 2 programmers shouldn't be the same kind at a table.
Schedule each lecture into a timeslot and into a room.
The problem is defined by the International Timetabling Competition 2007 track 3.
The problem is defined by the Google ROADEF/EURO Challenge 2012.
Besides the basic case (CVRP), there is also a variant with time windows (CVRPTW).
The capacitated vehicle routing problem (CVRP) and its timewindowed variant (CVRPTW) are defined by the VRP web.
CVRP instances (without time windows):
A-n32-k5 has 1 depots, 5 vehicles and 31 customers with a search space of 10^46.
A-n33-k5 has 1 depots, 5 vehicles and 32 customers with a search space of 10^48.
A-n33-k6 has 1 depots, 6 vehicles and 32 customers with a search space of 10^48.
A-n34-k5 has 1 depots, 5 vehicles and 33 customers with a search space of 10^50.
A-n36-k5 has 1 depots, 5 vehicles and 35 customers with a search space of 10^54.
A-n37-k5 has 1 depots, 5 vehicles and 36 customers with a search space of 10^56.
A-n37-k6 has 1 depots, 6 vehicles and 36 customers with a search space of 10^56.
A-n38-k5 has 1 depots, 5 vehicles and 37 customers with a search space of 10^58.
A-n39-k5 has 1 depots, 5 vehicles and 38 customers with a search space of 10^60.
A-n39-k6 has 1 depots, 6 vehicles and 38 customers with a search space of 10^60.
A-n44-k7 has 1 depots, 7 vehicles and 43 customers with a search space of 10^70.
A-n45-k6 has 1 depots, 6 vehicles and 44 customers with a search space of 10^72.
A-n45-k7 has 1 depots, 7 vehicles and 44 customers with a search space of 10^72.
A-n46-k7 has 1 depots, 7 vehicles and 45 customers with a search space of 10^74.
A-n48-k7 has 1 depots, 7 vehicles and 47 customers with a search space of 10^78.
A-n53-k7 has 1 depots, 7 vehicles and 52 customers with a search space of 10^89.
A-n54-k7 has 1 depots, 7 vehicles and 53 customers with a search space of 10^91.
A-n55-k9 has 1 depots, 9 vehicles and 54 customers with a search space of 10^93.
A-n60-k9 has 1 depots, 9 vehicles and 59 customers with a search space of 10^104.
A-n61-k9 has 1 depots, 9 vehicles and 60 customers with a search space of 10^106.
A-n62-k8 has 1 depots, 8 vehicles and 61 customers with a search space of 10^108.
A-n63-k10 has 1 depots, 10 vehicles and 62 customers with a search space of 10^111.
A-n63-k9 has 1 depots, 9 vehicles and 62 customers with a search space of 10^111.
A-n64-k9 has 1 depots, 9 vehicles and 63 customers with a search space of 10^113.
A-n65-k9 has 1 depots, 9 vehicles and 64 customers with a search space of 10^115.
A-n69-k9 has 1 depots, 9 vehicles and 68 customers with a search space of 10^124.
A-n80-k10 has 1 depots, 10 vehicles and 79 customers with a search space of 10^149.
F-n135-k7 has 1 depots, 7 vehicles and 134 customers with a search space of 10^285.
F-n45-k4 has 1 depots, 4 vehicles and 44 customers with a search space of 10^72.
F-n72-k4 has 1 depots, 4 vehicles and 71 customers with a search space of 10^131.
CVRPTW instances (with time windows):
Solomon_025_C101 has 1 depots, 25 vehicles and 25 customers with a search space of 10^34.
Solomon_025_C201 has 1 depots, 25 vehicles and 25 customers with a search space of 10^34.
Solomon_025_R101 has 1 depots, 25 vehicles and 25 customers with a search space of 10^34.
Solomon_025_R201 has 1 depots, 25 vehicles and 25 customers with a search space of 10^34.
Solomon_025_RC101 has 1 depots, 25 vehicles and 25 customers with a search space of 10^34.
Solomon_025_RC201 has 1 depots, 25 vehicles and 25 customers with a search space of 10^34.
Solomon_100_C101 has 1 depots, 25 vehicles and 100 customers with a search space of 10^200.
Solomon_100_C201 has 1 depots, 25 vehicles and 100 customers with a search space of 10^200.
Solomon_100_R101 has 1 depots, 25 vehicles and 100 customers with a search space of 10^200.
Solomon_100_R201 has 1 depots, 25 vehicles and 100 customers with a search space of 10^200.
Solomon_100_RC101 has 1 depots, 25 vehicles and 100 customers with a search space of 10^200.
Solomon_100_RC201 has 1 depots, 25 vehicles and 100 customers with a search space of 10^200.
Homberger_0200_C1_2_1 has 1 depots, 50 vehicles and 200 customers with a search space of 10^460.
Homberger_0200_C2_2_1 has 1 depots, 50 vehicles and 200 customers with a search space of 10^460.
Homberger_0200_R1_2_1 has 1 depots, 50 vehicles and 200 customers with a search space of 10^460.
Homberger_0200_R2_2_1 has 1 depots, 50 vehicles and 200 customers with a search space of 10^460.
Homberger_0200_RC1_2_1 has 1 depots, 50 vehicles and 200 customers with a search space of 10^460.
Homberger_0200_RC2_2_1 has 1 depots, 50 vehicles and 200 customers with a search space of 10^460.
Homberger_0400_C1_4_1 has 1 depots, 100 vehicles and 400 customers with a search space of 10^1040.
Homberger_0400_C2_4_1 has 1 depots, 100 vehicles and 400 customers with a search space of 10^1040.
Homberger_0400_R1_4_1 has 1 depots, 100 vehicles and 400 customers with a search space of 10^1040.
Homberger_0400_R2_4_1 has 1 depots, 100 vehicles and 400 customers with a search space of 10^1040.
Homberger_0400_RC1_4_1 has 1 depots, 100 vehicles and 400 customers with a search space of 10^1040.
Homberger_0400_RC2_4_1 has 1 depots, 100 vehicles and 400 customers with a search space of 10^1040.
Homberger_0600_C1_6_1 has 1 depots, 150 vehicles and 600 customers with a search space of 10^1666.
Homberger_0600_C2_6_1 has 1 depots, 150 vehicles and 600 customers with a search space of 10^1666.
Homberger_0600_R1_6_1 has 1 depots, 150 vehicles and 600 customers with a search space of 10^1666.
Homberger_0600_R2_6_1 has 1 depots, 150 vehicles and 600 customers with a search space of 10^1666.
Homberger_0600_RC1_6_1 has 1 depots, 150 vehicles and 600 customers with a search space of 10^1666.
Homberger_0600_RC2_6_1 has 1 depots, 150 vehicles and 600 customers with a search space of 10^1666.
Homberger_0800_C1_8_1 has 1 depots, 200 vehicles and 800 customers with a search space of 10^2322.
Homberger_0800_C2_8_1 has 1 depots, 200 vehicles and 800 customers with a search space of 10^2322.
Homberger_0800_R1_8_1 has 1 depots, 200 vehicles and 800 customers with a search space of 10^2322.
Homberger_0800_R2_8_1 has 1 depots, 200 vehicles and 800 customers with a search space of 10^2322.
Homberger_0800_RC1_8_1 has 1 depots, 200 vehicles and 800 customers with a search space of 10^2322.
Homberger_0800_RC2_8_1 has 1 depots, 200 vehicles and 800 customers with a search space of 10^2322.
Homberger_1000_C110_1 has 1 depots, 250 vehicles and 1000 customers with a search space of 10^3000.
Homberger_1000_C210_1 has 1 depots, 250 vehicles and 1000 customers with a search space of 10^3000.
Homberger_1000_R110_1 has 1 depots, 250 vehicles and 1000 customers with a search space of 10^3000.
Homberger_1000_R210_1 has 1 depots, 250 vehicles and 1000 customers with a search space of 10^3000.
Homberger_1000_RC110_1 has 1 depots, 250 vehicles and 1000 customers with a search space of 10^3000.
Homberger_1000_RC210_1 has 1 depots, 250 vehicles and 1000 customers with a search space of 10^3000.
The vehicle routing with timewindows domain model makes heavily use of shadow variables. This allows it to express its constraints more naturally,
because properties such as arrivalTime
and departureTime
, are directly
available on the domain model.
For the optimization algorithm, this doesn't matter much, as long as the distance between 2 points can be looked up (and are preferably precalculated). The road cost doesn't even need to be a distance, it can also be travel time, fuel cost, or a weighted function of those. There are several technologies available to precalculate road costs, such as GraphHopper (embeddable, offline Java engine), Open MapQuest (web service) and Google Maps Client API (web service).
There are also several technologies to render it, such as Leaflet and Google Maps for developers: the
optaplanner-webexamples-*.war
has an example which demonstrates such rendering:
It's even possible to render the actual road routes with GraphHopper or Google Map Directions, but because of route overlaps on highways, it can become harder to see the standstill order:
Take special care that the road costs between 2 points use the same optimization criteria as the one used in Planner. For example, GraphHopper etc will by default return the fastest route, not the shortest route. Don't use the km (or miles) distances of the fastest GPS routes to optimize the shortest trip in Planner: this leads to a suboptimal solution as shown below:
Contrary to popular belief, most users don't want the shortest route: they want the fastest route instead. They prefer highways over normal roads. They prefer normal roads over dirt roads. In the real world, the fastest and shortest route are rarely the same.
This problem features overconstrained datasets.
Hard constraints:
2 patients must not be assigned to the same bed in the same night. Weight: -1000hard *
conflictNightCount
.
A room can have a gender limitation: only females, only males, the same gender in the same night or no
gender limitation at all. Weight: -50hard * nightCount
.
A department can have a minimum or maximum age. Weight: -100hard *
nightCount
.
A patient can require a room with specific equipment(s). Weight: -50hard *
nightCount
.
Medium constraints:
Assign every patient to a bed, unless the dataset is overconstrained. Weight: -1medium *
nightCount
.
Soft constraints:
A patient can prefer a maximum room size, for example if he/she wants a single room. Weight:
-8soft * nightCount
.
A patient is best assigned to a department that specializes in his/her problem. Weight:
-10soft * nightCount
.
A patient is best assigned to a room that specializes in his/her problem. Weight: -20soft *
nightCount
.
That room speciality should be priority 1. Weight: -10soft * (priority - 1) *
nightCount
.
A patient can prefer a room with specific equipment(s). Weight: -20soft *
nightCount
.
The problem is a variant on Kaho's Patient Scheduling and the datasets come from real world hospitals.
Soft constraints (each of which has a parametrized penalty):
Period spread: 2 exams that share students should be a number of periods apart.
Mixed durations: 2 exams that share a room should not have different durations.
Front load: Large exams should be scheduled earlier in the schedule.
Period penalty (specified per dataset): Some periods have a penalty when used.
Room penalty (specified per dataset): Some rooms have a penalty when used.
It uses large test data sets of real-life universities.
The problem is defined by the International Timetabling Competition 2007 track 1. Geoffrey De Smet finished 4th in that competition with a very early version of Planner. Many improvements have been made since then.
Below you can see the main examination domain classes:
Notice that we've split up the exam concept into an Exam
class and a
Topic
class. The Exam
instances change during solving (this is the
planning entity class), when their period or room property changes. The Topic
,
Period
and Room
instances never change during solving (these are problem
facts, just like some other classes).
For each shift, assign a nurse to work that shift.
The problem is defined by the International Nurse Rostering Competition 2010.
Schedule matches between n teams.
The problem is defined on Michael Trick's website (which contains the world records too).
Soft constraints (addendum to the original problem definition):
Decide the relative quantity to invest in each asset class.
Risk maximum: the total standard deviation must not be higher than the standard deviation maximum.
Region maximum: Each region has a quantity maximum.
Sector maximum: Each sector has a quantity maximum.
Soft constraints:
Maximize expected return.
Solving a planning problem with Planner consists out of 5 steps:
SolverFactory solverFactory = SolverFactory.createFromXmlResource(
"org/optaplanner/examples/nqueens/solver/nqueensSolverConfig.xml");
Solver solver = solverFactory.buildSolver();
In a typical project (following the Maven directory structure), that solverConfig XML file would be located
at
$PROJECT_DIR/src/main/resources/org/optaplanner/examples/nqueens/solver/nqueensSolverConfig.xml
.
On some environments (OSGi, JBoss modules, ...), classpath resources in your jars might not be
available in by default to the classes in optaplanner's jars.
Alternatively, a SolverFactory
can be created from a File
, an
InputStream
or a Reader
with methods such as
SolverFactory.createFromXmlFile()
. However, for portability reasons, a classpath resource is
recommended.
A solver configuration file looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<solver>
<!-- Define the model -->
<solutionClass>org.optaplanner.examples.nqueens.domain.NQueens</solutionClass>
<entityClass>org.optaplanner.examples.nqueens.domain.Queen</entityClass>
<!-- Define the score function -->
<scoreDirectorFactory>
<scoreDefinitionType>SIMPLE</scoreDefinitionType>
<scoreDrl>org/optaplanner/examples/nqueens/solver/nQueensScoreRules.drl</scoreDrl>
</scoreDirectorFactory>
<!-- Configure the optimization algorithm(s) -->
<termination>
...
</termination>
<constructionHeuristic>
...
</constructionHeuristic>
<localSearch>
...
</localSearch>
</solver>
Notice the three parts in it:
Define the model
Define the score function
Configure the optimization algorithm(s)
These various parts of a configuration are explained further in this manual.
Planner makes it relatively easy to switch optimization algorithm(s) just by changing
the configuration. There is even a Benchmarker
utility which allows you to play out
different configurations against each other and report the most appropriate configuration for your use
case.
Instead of the declaring the classes that have a @PlanningSolution
or
@PlanningEntity
manually:
<solver>
<!-- Define the model -->
<solutionClass>org.optaplanner.examples.nqueens.domain.NQueens</solutionClass>
<entityClass>org.optaplanner.examples.nqueens.domain.Queen</entityClass>
...
</solver>
Planner can find scan the classpath and find them automatically:
<solver>
<!-- Define the model -->
<scanAnnotatedClasses/>
...
</solver>
<solver>
<!-- Define the model -->
<scanAnnotatedClasses>
<packageInclude>org.optaplanner.examples.cloudbalancing</packageInclude>
</scanAnnotatedClasses>
...
</solver>
This will find all solution and entity classes in the package or subpackages.
This manual focusses on the first manner, but every features supports all 3 manners, even if it's not explicilty mentioned.
In real-time planning, even though the problem itself changes, problem facts do not really change during planning, instead they change between planning (because the Solver temporarily stops to apply the problem fact changes).
A good model can greatly improve the success of your planning implementation. Follow these guidelines to design a good model:
In a many to one relationship, it is normally the many side
that is the planning entity class. The property referencing the other side is then the planning variable. For
example in employee rostering: the planning entity class is ShiftAssignment
, not
Employee
, and the planning variable is ShiftAssignment.getEmployee()
because one Employee
has multiple ShiftAssignment
s but one
ShiftAssignment
has only one Employee
.
A planning entity class should have at least one problem property. A planning entity class with only
planning variables can normally be simplified by converting one of those planning variables into a problem
property. That heavily decreases the search space size. For example in
employee rostering: the ShiftAssignment
's getShift()
is a problem
property and the getEmployee()
is a planning variable. If both were a planning variable,
solving it would be far less efficient.
A surrogate ID does not suffice as the required minimum of one problem property. It needs to be understandable by the business. A business key does suffice. This prevents an unassigned entity from being nameless (unidentifiable by the business).
This way, there is no need to add a hard constraint to assure that two planning entities are different: they are already different due to their problem properties.
In some cases, multiple planning entities have the same problem property. In such cases, it can be
useful to create an extra problem property to distinguish them. For example in employee rostering:
ShiftAssignment
has besides the problem property Shift
also the
problem property indexInShift
.
The number of planning entities is recommended to be fixed during planning. When unsure of which
property should be a planning variable and which should be a problem property, choose it so the number of
planning entities is fixed. For example in employee rostering: if the planning entity class would have been
EmployeeAssignment
with a problem property getEmployee()
and a planning
variable getShift()
, than it is impossible to accurately predict how many
EmployeeAssignment
instances to make per Employee
.
For inspiration, take a look at how the examples modeled their domain:
Vehicle routing is special, because it uses a chained planning variable.
In Planner all problems facts and planning entities are plain old JavaBeans (POJOs). You can load them from a database, an XML file, a data repository, a noSQL cloud, ... (see Integration).
public class Column implements Serializable {
private int index;
// ... getters
}
public class Row implements Serializable {
private int index;
// ... getters
}
A problem fact can reference other problem facts of course:
public class Course implements Serializable {
private String code;
private Teacher teacher; // Other problem fact
private int lectureSize;
private int minWorkingDaySize;
private List<Curriculum> curriculumList; // Other problem facts
private int studentSize;
// ... getters
}
Alternatively, you can sometimes also introduce a cached problem fact to enrich the domain model for planning only.
A planning entity class needs to be annotated with the @PlanningEntity
annotation.
@PlanningEntity
public class Queen {
private Column column;
// Planning variables: changes during planning, between score calculations.
private Row row;
// ... getters and setters
}
@PlanningEntity
public class Lecture {
private Course course;
private int lectureIndexInCourse;
// Planning variables: changes during planning, between score calculations.
private Period period;
private Room room;
// ...
}
Without automated scanning, the solver configuration also needs to declare each planning entity class:
<solver>
...
<entityClass>org.optaplanner.examples.nqueens.domain.Queen</entityClass>
...
</solver>
Some uses cases have multiple planning entity classes. For example: route freight and trains into railway network arcs, where each freight can use multiple trains over its journey and each train can carry multiple freights per arc. Having multiple planning entity classes directly raises the implementation complexity of your use case.
Do not create unnecessary planning entity classes. This leads to difficult
Move
implementations and slower score calculation.
For example, do not create a planning entity class to hold the total free time of a teacher, which needs
to be kept up to date as the Lecture
planning entities change. Instead, calculate the free
time in the score constraints and put the result per teacher into a logically inserted score object.
If historic data needs to be considered too, then create problem fact to hold the total of the historic assignments up to, but not including, the planning window (so that it does not change when a planning entity changes) and let the score constraints take it into account.
Therefore, you can set a difficultyComparatorClass
to the
@PlanningEntity
annotation:
@PlanningEntity(difficultyComparatorClass = CloudProcessDifficultyComparator.class)
public class CloudProcess {
// ...
}
public class CloudProcessDifficultyComparator implements Comparator<CloudProcess> {
public int compare(CloudProcess a, CloudProcess b) {
return new CompareToBuilder()
.append(a.getRequiredMultiplicand(), b.getRequiredMultiplicand())
.append(a.getId(), b.getId())
.toComparison();
}
}
@PlanningEntity(difficultyWeightFactoryClass = QueenDifficultyWeightFactory.class)
public class Queen {
// ...
}
See sorted selection for more information.
Difficulty should be implemented ascending: easy entities are lower, difficult entities are higher. For example, in bin packing: small item < medium item < big item.
Even though some algorithms start with the more difficult entities first, they just reverse the ordering.
None of the current planning variable states should be used to compare planning entity
difficulty. During Construction Heuristics, those variables are likely to be null
anyway. For example, a Queen
's row
variable should not be used.
@PlanningEntity
public class Queen {
...
private Row row;
@PlanningVariable(valueRangeProviderRefs = {"rowRange"})
public Row getRow() {
return row;
}
public void setRow(Row row) {
this.row = row;
}
}
Annotating the field instead of the property works too:
@PlanningEntity
public class Queen {
...
@PlanningVariable(valueRangeProviderRefs = {"rowRange"})
private Row row;
}
To allow an initialized planning variable to be null
, set nullable
to true
:
@PlanningVariable(..., nullable = true)
public Worker getWorker() {
return worker;
}
Repeated planning (especially real-time planning) does not mix well with a nullable planning variable. Every
time the Solver starts or a problem fact change is made, the Construction
Heuristics will try to initialize all the null
variables again, which can be a huge
waste of time. One way to deal with this, is to change when a planning entity should be reinitialized with an
reinitializeVariableEntityFilter
:
@PlanningVariable(..., nullable = true, reinitializeVariableEntityFilter = ReinitializeTaskFilter.class)
public Worker getWorker() {
return worker;
}
@PlanningVariable(valueRangeProviderRefs = {"rowRange"})
public Row getRow() {
return row;
}
@PlanningSolution
public class NQueens implements Solution<SimpleScore> {
// ...
@ValueRangeProvider(id = "rowRange")
public List<Row> getRowList() {
return rowList;
}
}
That Collection
(or ValueRange
) must not contain the value
null
, not even for a nullable planning
variable.
Annotating the field instead of the property works too:
@PlanningSolution
public class NQueens implements Solution<SimpleScore> {
...
@ValueRangeProvider(id = "rowRange")
private List<Row> rowList;
}
@PlanningVariable(valueRangeProviderRefs = {"departmentRoomRange"})
public Room getRoom() {
return room;
}
@ValueRangeProvider(id = "departmentRoomRange")
public List<Room> getPossibleRoomList() {
return getCourse().getTeacher().getDepartment().getRoomList();
}
@ValueRangeProvider(id = "delayRange")
public CountableValueRange<Integer> getDelayRange() {
return ValueRangeFactory.createIntValueRange(0, 5000);
}
@ValueRangeProvider(id = "stockAmountRange")
public CountableValueRange<Integer> getStockAmountRange() {
// Range: 0, 200, 400, 600, ..., 9999600, 9999800, 10000000
return ValueRangeFactory.createIntValueRange(0, 10000000, 200);
}
The ValueRangeFactory
has creation methods for several value class types:
Value range providers can be combined, for example:
@PlanningVariable(valueRangeProviderRefs = {"companyCarRange", "personalCarRange"})
public Car getCar() {
return car;
}
@ValueRangeProvider(id = "companyCarRange")
public List<CompanyCar> getCompanyCarList() {
return companyCarList;
}
@ValueRangeProvider(id = "personalCarRange")
public List<PersonalCar> getPersonalCarList() {
return personalCarList;
}
Therefore, you can set a strengthComparatorClass
to the
@PlanningVariable
annotation:
@PlanningVariable(..., strengthComparatorClass = CloudComputerStrengthComparator.class)
public CloudComputer getComputer() {
// ...
}
public class CloudComputerStrengthComparator implements Comparator<CloudComputer> {
public int compare(CloudComputer a, CloudComputer b) {
return new CompareToBuilder()
.append(a.getMultiplicand(), b.getMultiplicand())
.append(b.getCost(), a.getCost()) // Descending (but this is debatable)
.append(a.getId(), b.getId())
.toComparison();
}
}
@PlanningVariable(..., strengthWeightFactoryClass = RowStrengthWeightFactory.class)
public Row getRow() {
// ...
}
See sorted selection for more information.
Strength should be implemented ascending: weaker values are lower, stronger values are higher. For example in bin packing: small container < medium container < big container.
None of the current planning variable state in any of the planning entities should be used to
compare planning values. During construction heuristics, those variables are likely to be
null
. For example, none of the row
variables of any
Queen
may be used to determine the strength of a Row
.
A planning variable that is chained either:
Here are some example of valid and invalid chains:
For example, in TSP the anchor is a Domicile
(in vehicle routing it is
Vehicle
):
public class Domicile ... implements Standstill {
...
public City getCity() {...}
}
public interface Standstill {
City getCity();
}
@PlanningEntity
public class Visit ... implements Standstill {
...
public City getCity() {...}
@PlanningVariable(graphType = PlanningVariableGraphType.CHAINED, valueRangeProviderRefs = {"domicileRange", "visitRange"})
public Standstill getPreviousStandstill() {
return previousStandstill;
}
public void setPreviousStandstill(Standstill previousStandstill) {
this.previousStandstill = previousStandstill;
}
}
When the customers for a vehicle change, the arrival time for each customer is automatically adjusted. For more information, see the vehicle routing domain model.
From a score calculation perspective, a shadow variable is like any other planning variable. From an optimization perspective, Planner effectively only optimizes the genuine variables (and mostly ignores the shadow variables): it just assures that when a genuine variable changes, any dependent shadow variables are changed accordingly.
There are several build-in shadow variables:
@PlanningEntity
public class CloudProcess {
@PlanningVariable(...)
public CloudComputer getComputer() {
return computer;
}
public void setComputer(CloudComputer computer) {...}
}
@PlanningEntity
public class CloudComputer {
@InverseRelationShadowVariable(sourceVariableName = "computer")
public List<CloudProcess> getProcessList() {
return processList;
}
}
@PlanningEntity
public class Customer ... {
@PlanningVariable(graphType = PlanningVariableGraphType.CHAINED, ...)
public Standstill getPreviousStandstill() {
return previousStandstill;
}
public void setPreviousStandstill(Standstill previousStandstill) {...}
}
And the shadow side looks like this:
@PlanningEntity
public class Standstill {
@InverseRelationShadowVariable(sourceVariableName = "previousStandstill")
public Customer getNextCustomer() {
return nextCustomer;
}
public void setNextCustomer(Customer nextCustomer) {...}
}
An anchor shadow variable is the anchor of a chained variable.
Annotate the anchor property as a @AnchorShadowVariable
annotation:
@PlanningEntity
public class Customer {
@AnchorShadowVariable(sourceVariableName = "previousStandstill")
public Vehicle getVehicle() {...}
public void setVehicle(Vehicle vehicle) {...}
}
The sourceVariableName
property is the name of the chained variable on the same entity
class.
@PlanningVariable(...)
public Standstill getPreviousStandstill() {
return previousStandstill;
}
@CustomShadowVariable(variableListenerClass = VehicleUpdatingVariableListener.class,
sources = {@CustomShadowVariable.Source(variableName = "previousStandstill")})
public Vehicle getVehicle() {
return vehicle;
}
The variableName
is the variable that triggers changes in the shadow
variable(s).
public class VehicleUpdatingVariableListener implements VariableListener<Customer> {
public void afterEntityAdded(ScoreDirector scoreDirector, Customer customer) {
updateVehicle(scoreDirector, customer);
}
public void afterVariableChanged(ScoreDirector scoreDirector, Customer customer) {
updateVehicle(scoreDirector, customer);
}
...
protected void updateVehicle(ScoreDirector scoreDirector, Customer sourceCustomer) {
Standstill previousStandstill = sourceCustomer.getPreviousStandstill();
Vehicle vehicle = previousStandstill == null ? null : previousStandstill.getVehicle();
Customer shadowCustomer = sourceCustomer;
while (shadowCustomer != null && shadowCustomer.getVehicle() != vehicle) {
scoreDirector.beforeVariableChanged(shadowCustomer, "vehicle");
shadowCustomer.setVehicle(vehicle);
scoreDirector.afterVariableChanged(shadowCustomer, "vehicle");
shadowCustomer = shadowCustomer.getNextCustomer();
}
}
}
@PlanningVariable(...)
public Standstill getPreviousStandstill() {
return previousStandstill;
}
@CustomShadowVariable(variableListenerClass = TransportTimeAndCapacityUpdatingVariableListener.class,
sources = {@CustomShadowVariable.Source(variableName = "previousStandstill")})
public Integer getTransportTime() {
return transportTime;
}
@CustomShadowVariable(variableListenerRef = @PlanningVariableReference(variableName = "transportTime"))
public Integer getCapacity() {
return capacity;
}
public interface Solution<S extends Score> {
S getScore();
void setScore(S score);
Collection<? extends Object> getProblemFacts();
}
For example, an NQueens
instance holds a list of all columns, all rows and all
Queen
instances:
@PlanningSolution
public class NQueens implements Solution<SimpleScore> {
private int n;
// Problem facts
private List<Column> columnList;
private List<Row> rowList;
// Planning entities
private List<Queen> queenList;
// ...
}
A planning solution class also needs to be annotated with the @PlanningSolution
annotation. Without automated scanning, the solver
configuration also needs to declare the planning solution class:
<solver>
...
<solutionClass>org.optaplanner.examples.nqueens.domain.NQueens</solutionClass>
...
</solver>
@PlanningSolution
public class NQueens implements Solution<SimpleScore> {
private SimpleScore score;
public SimpleScore getScore() {
return score;
}
public void setScore(SimpleScore score) {
this.score = score;
}
// ...
}
Most use cases use a HardSoftScore
instead:
@PlanningSolution
public class CourseSchedule implements Solution<HardSoftScore> {
private HardSoftScore score;
public HardSoftScore getScore() {
return score;
}
public void setScore(HardSoftScore score) {
this.score = score;
}
// ...
}
See the Score calculation section for more information on the Score
implementations.
public Collection<? extends Object> getProblemFacts() {
List<Object> facts = new ArrayList<Object>();
facts.addAll(columnList);
facts.addAll(rowList);
// Do not add the planning entity's (queenList) because that will be done automatically
return facts;
}
@PlanningSolution
public class NQueens implements Solution<SimpleScore> {
...
private List<Queen> queenList;
@PlanningEntityCollectionProperty
public List<Queen> getQueenList() {
return queenList;
}
}
A planning clone of a Solution
must fulfill these requirements:
Implementing a planning clone method is hard, therefore you do not need to implement it.
This SolutionCloner
is used by default. It works well for most use cases.
@DeepPlanningClone
public class SeatDesignationDependency {
private SeatDesignation leftSeatDesignation; // planning entity
private SeatDesignation rightSeatDesignation; // planning entity
...
}
Alternatively, the @DeepPlanningClone
annotation can also be used on a getter
method.
public interface PlanningCloneable<T> {
T planningClone();
}
public class NQueens implements Solution<...>, PlanningCloneable<NQueens> {
...
/**
* Clone will only deep copy the {@link #queenList}.
*/
public NQueens planningClone() {
NQueens clone = new NQueens();
clone.id = id;
clone.n = n;
clone.columnList = columnList;
clone.rowList = rowList;
List<Queen> clonedQueenList = new ArrayList<Queen>(queenList.size());
for (Queen queen : queenList) {
clonedQueenList.add(queen.planningClone());
}
clone.queenList = clonedQueenList;
clone.score = score;
return clone;
}
}
Cloning an entity with a chained variable is devious: a variable of an entity A might point to another entity B. If A is cloned, then its variable must point to the clone of B, not the original B.
private NQueens createNQueens(int n) {
NQueens nQueens = new NQueens();
nQueens.setId(0L);
nQueens.setN(n);
nQueens.setColumnList(createColumnList(nQueens));
nQueens.setRowList(createRowList(nQueens));
nQueens.setQueenList(createQueenList(nQueens));
return nQueens;
}
private List<Queen> createQueenList(NQueens nQueens) {
int n = nQueens.getN();
List<Queen> queenList = new ArrayList<Queen>(n);
long id = 0L;
for (Column column : nQueens.getColumnList()) {
Queen queen = new Queen();
queen.setId(id);
id++;
queen.setColumn(column);
// Notice that we leave the PlanningVariable properties on null
queenList.add(queen);
}
return queenList;
}
Usually, most of this data comes from your data layer, and your Solution
implementation
just aggregates that data and creates the uninitialized planning entity instances to plan:
private void createLectureList(CourseSchedule schedule) {
List<Course> courseList = schedule.getCourseList();
List<Lecture> lectureList = new ArrayList<Lecture>(courseList.size());
long id = 0L;
for (Course course : courseList) {
for (int i = 0; i < course.getLectureSize(); i++) {
Lecture lecture = new Lecture();
lecture.setId(id);
id++;
lecture.setCourse(course);
lecture.setLectureIndexInCourse(i);
// Notice that we leave the PlanningVariable properties (period and room) on null
lectureList.add(lecture);
}
}
schedule.setLectureList(lectureList);
}
A Solver
implementation will solve your planning problem.
public interface Solver {
void solve(Solution planningProblem);
Solution getBestSolution();
// ...
}
Solving a problem is quite easy once you have:
Just set the planning problem, solve it and extract the best solution:
solver.solve(planningProblem);
Solution bestSolution = solver.getBestSolution();
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.
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.
You can set the environment mode in the solver configuration XML file:
<solver>
<environmentMode>FAST_ASSERT</environmentMode>
...
</solver>
These are the environment modes:
Despite the reproducible mode, your application might still not be fully reproducible because of:
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).
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
.
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.
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>
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>
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:
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.
Score
Solver
Your business will probably tell you that your hard constraints all have the same weight, because they
cannot be broken (so their weight does not matter). This is not true and it could create a score trap. For example in cloud balance: if a Computer
has 7 CPU
too little for its Process
es, then it must be weighted 7 times as much as if it had only 1
CPU too little. This way, there is an incentive to move a Process
with 6 CPU or less away
from that Computer.
Three or more score levels are also supported. For example: a company might decide that profit outranks employee satisfaction (or visa versa), while both are outranked by the constraints of physical reality.
To model fairness or load balancing, there is no need to use lots of score levels (even though Planner can handle many score levels).
To implement pareto scoring in Planner, implement a custom
ScoreDefinition
and Score
(and replace the
BestSolutionRecaller
). Future versions will provide out-of-the-box support.
A pareto Score
's compareTo
method is not transitive because it does
a pareto comparison. For example: having 2 apples is greater than 1 apple. 1 apple is equal to 1 orange. Yet, 2
apples are not greater than 1 orange (but actually equal). Pareto comparison violates the contract of the
interface java.lang.Comparable
's compareTo
method, but Planner's systems
are pareto comparison safe, unless explicitly stated otherwise in this
documentation.
A score is represented by the Score
interface, which naturally extends
Comparable
:
public interface Score<...> extends Comparable<...> {
...
}
<scoreDirectorFactory>
<scoreDefinitionType>HARD_SOFT</scoreDefinitionType>
...
</scoreDirectorFactory>
Avoid the use of float
and double
for score calculation. Use
BigDecimal
instead.
Additionally, floating point number addition is not associative:
System.out.println( ((0.01 + 0.02) + 0.03) == (0.01 + (0.02 + 0.03)) ); // returns false
This leads to score corruption.
A SimpleScore
has a single int
value, for example
-123
. It has a single score level.
<scoreDirectorFactory>
<scoreDefinitionType>SIMPLE</scoreDefinitionType>
...
</scoreDirectorFactory>
Variants of this scoreDefinitionType
:
SIMPLE_LONG
: Uses SimpleLongScore
which has a
long
value instead of an int
value.
SIMPLE_DOUBLE
: Uses SimpleDoubleScore
which has a
double
value instead of an int
value. Not recommended to use.
SIMPLE_BIG_DECIMAL
: Uses SimpleBigDecimalScore
which has a
BigDecimal
value instead of an int
value.
<scoreDirectorFactory>
<scoreDefinitionType>HARD_SOFT</scoreDefinitionType>
...
</scoreDirectorFactory>
Variants of this scoreDefinitionType
:
HARD_SOFT_LONG
: Uses HardSoftLongScore
which has
long
values instead of int
values.
HARD_SOFT_DOUBLE
: Uses HardSoftDoubleScore
which has
double
values instead of int
values. Not recommended to use.
HARD_SOFT_BIG_DECIMAL
: Uses HardSoftBigDecimalScore
which has
BigDecimal
values instead of int
values.
There are several ways to calculate the Score
of a Solution
:
All score calculation types are Object Oriented and can reuse existing Java code.
Planner will not recalculate the score of a Solution
if it can predict it (unless an
environmentMode assertion is enabled). For example, after a winning step
is done, there is no need to calculate the score because that move was done and undone earlier. As a result,
there's no guarantee that such changes applied during score calculation are actually done.
An easy way to implement your score calculation in Java.
Just implement one method of the interface EasyScoreCalculator
:
public interface EasyScoreCalculator<Sol extends Solution> {
Score calculateScore(Sol solution);
}
For example in n queens:
public class NQueensEasyScoreCalculator implements EasyScoreCalculator<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 SimpleScore.valueOf(score);
}
}
Configure it in your solver configuration:
<scoreDirectorFactory>
<scoreDefinitionType>...</scoreDefinitionType>
<easyScoreCalculatorClass>org.optaplanner.examples.nqueens.solver.score.NQueensEasyScoreCalculator</easyScoreCalculatorClass>
</scoreDirectorFactory>
Alternatively, build a EasyScoreCalculator
instance at runtime and set it with the
programmatic API:
solverFactory.getSolverConfig().getScoreDirectorFactoryConfig.setEasyScoreCalculator(easyScoreCalculator);
A way to implement your score calculation incrementally in Java.
public interface IncrementalScoreCalculator<Sol extends Solution> {
void resetWorkingSolution(Sol workingSolution);
void beforeEntityAdded(Object entity);
void afterEntityAdded(Object entity);
void beforeVariableChanged(Object entity, String variableName);
void afterVariableChanged(Object entity, String variableName);
void beforeEntityRemoved(Object entity);
void afterEntityRemoved(Object entity);
Score calculateScore();
}
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>>(n * 2);
descendingDiagonalIndexMap = new HashMap<Integer, List<Queen>>(n * 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 (i != 0) {
ascendingDiagonalIndexMap.put(n - 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 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 SimpleScore.valueOf(score);
}
}
Configure it in your solver configuration:
<scoreDirectorFactory>
<scoreDefinitionType>...</scoreDefinitionType>
<incrementalScoreCalculatorClass>org.optaplanner.examples.nqueens.solver.score.NQueensAdvancedIncrementalScoreCalculator</incrementalScoreCalculatorClass>
</scoreDirectorFactory>
public interface ConstraintMatchAwareIncrementalScoreCalculator<Sol extends Solution> {
void resetWorkingSolution(Sol workingSolution, boolean constraintMatchEnabled);
Collection<ConstraintMatchTotal> getConstraintMatchTotals();
}
There are several ways to define where your score rules live.
<scoreDirectorFactory>
<scoreDefinitionType>...</scoreDefinitionType>
<scoreDrl>org/optaplanner/examples/nqueens/solver/nQueensScoreRules.drl</scoreDrl>
</scoreDirectorFactory>
Add multiple <scoreDrl>
elements if the score rules are split across multiple
DRL files.
<scoreDirectorFactory>
...
<scoreDrl>org/optaplanner/examples/nqueens/solver/nQueensScoreRules.drl</scoreDrl>
<kieBaseConfigurationProperties>
<drools.equalityBehavior>...</drools.equalityBehavior>
</kieBaseConfigurationProperties>
</scoreDirectorFactory>
<scoreDirectorFactory>
<scoreDefinitionType>...</scoreDefinitionType>
<scoreDrlFile>/home/ge0ffrey/tmp/nQueensScoreRules.drl</scoreDrlFile>
</scoreDirectorFactory>
Add multiple <scoreDrlFile>
elements if the score rules are split across
multiple DRL files.
Here's an example of a score constraint implemented as a score rule in a DRL file:
rule "multipleQueensHorizontal"
when
Queen($id : id, row != null, $i : rowIndex)
Queen(id > $id, rowIndex == $i)
then
scoreHolder.addConstraintMatch(kcontext, -1);
end
global SimpleScoreHolder scoreHolder;
rule "multipleQueensHorizontal"
when
Queen($id : id, row != null, $i : rowIndex)
Queen(id > $id, rowIndex == $i)
then
scoreHolder.addConstraintMatch(kcontext, -1);
end
// multipleQueensVertical is obsolete because it is always 0
rule "multipleQueensAscendingDiagonal"
when
Queen($id : id, row != null, $i : ascendingDiagonalIndex)
Queen(id > $id, ascendingDiagonalIndex == $i)
then
scoreHolder.addConstraintMatch(kcontext, -1);
end
rule "multipleQueensDescendingDiagonal"
when
Queen($id : id, row != null, $i : descendingDiagonalIndex)
Queen(id > $id, descendingDiagonalIndex == $i)
then
scoreHolder.addConstraintMatch(kcontext, -1);
end
global HardSoftScoreHolder scoreHolder;
// 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
$room : Room($capacity : capacity)
$lecture : Lecture(room == $room, studentSize > $capacity, $studentSize : studentSize)
then
scoreHolder.addSoftConstraintMatch(kcontext, ($capacity - $studentSize));
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
scoreHolder.addSoftConstraintMatch(kcontext, -2);
end
For for the Score (or each score level separately), specify a trend:
ANY
(default): Initializing an extra variable can change the score positively or
negatively. Gives no performance gain.
ONLY_UP
(rare): Initializing an extra variable can only change the score positively.
Implies that:
There are only positive constraints
And initializing the next variable can not unmatch a positive constraint that was matched by a previous initialized variable.
ONLY_DOWN
: Initializing an extra variable can only change the score negatively.
Implies that:
There are only negative constraints
And initializing the next variable can not unmatch a negative constraint that was matched by a previous initialized variable.
Most use cases only have negative constraints. Many of those have an
InitializingScoreTrend
that only goes down:
<scoreDirectorFactory>
<scoreDefinitionType>HARD_SOFT</scoreDefinitionType>
<scoreDrl>.../cloudBalancingScoreRules.drl</scoreDrl>
<initializingScoreTrend>ONLY_DOWN</initializingScoreTrend>
</scoreDirectorFactory>
Alternatively, you can also specify the trend for each score level separately:
<scoreDirectorFactory>
<scoreDefinitionType>HARD_SOFT</scoreDefinitionType>
<scoreDrl>.../cloudBalancingScoreRules.drl</scoreDrl>
<initializingScoreTrend>ONLY_DOWN/ONLY_DOWN</initializingScoreTrend>
</scoreDirectorFactory>
Put the environmentMode
in FULL_ASSERT
(or
FAST_ASSERT
) to detect corruption in the incremental score calculation. For more information, see the section about environmentMode
. However, that will not
verify that your score calculator implements your score constraints as your business actually desires.
A piece of incremental score calculator code can be difficult to write and to review. Assert its correctness
by using a different implementation (for example a EasyScoreCalculator
) to do the assertions
triggered by the environmentMode
. Just configure the different implementation as a
assertionScoreDirectorFactory
:
<environmentMode>FAST_ASSERT</environmentMode>
...
<scoreDirectorFactory>
<scoreDefinitionType>...</scoreDefinitionType>
<scoreDrl>org/optaplanner/examples/nqueens/solver/nQueensScoreRules.drl</scoreDrl>
<assertionScoreDirectorFactory>
<easyScoreCalculatorClass>org.optaplanner.examples.nqueens.solver.score.NQueensEasyScoreCalculator</easyScoreCalculatorClass>
</assertionScoreDirectorFactory>
</scoreDirectorFactory>
This way, the scoreDrl
will be validated by the
EasyScoreCalculator
.
If some parts of a constraint can be calculated once, when the Solver
starts, and never
change during solving, then turn them into cached problem facts.
Instead of implementing a hard constraint, it can sometimes be built in. For example: If
Lecture
A should never be assigned to Room
X, but it uses
ValueRangeProvider
on Solution, so the Solver
will often try to assign it to
Room
X too (only to find out that it breaks a hard constraint). Use a ValueRangeProvider on the planning entity or filtered selection to define that Course A should only be assigned a
Room
different than X.
This can give a good performance gain in some use cases, not just because the score calculation is faster, but mainly because most optimization algorithms will spend less time evaluating unfeasible solutions. However, usually this not a good idea because there is a real risk of trading short term benefits for long term harm:
Many optimization algorithms rely on the freedom to break hard constraints when changing planning entities, to get out of local optima.
Both implementation approaches have limitations (feature compatiblity, disabling automatic performance optimizations, ...), as explained in their documentation.
Not all score constraints have the same performance cost. Sometimes 1 score constraint can kill the score calculation performance outright. Use the Benchmarker to do a 1 minute run and check what happens to the average calculation count per second if you comment out all but 1 of the score constraints.
Instead of the squared workload, it's also possible to use the variance (squared difference to the average) or the standard deviation (square root of the variance). This has no effect on the score comparison, because the average won't change during planning. It's just more work to implement (because the average needs to be known) and trivially slower (because the calculation is a bit longer).
When the workload is perfect balanced, the user often likes to see a 0
score, instead of
the distracting -34soft
in the image above (for the last solution which is almost perfectly
balanced). To nullify this, either add the average multiplied by the number of entities to the score or instead
show the variance or standard deviation in the UI.
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();
for (ConstraintMatchTotal constraintMatchTotal : guiScoreDirector.getConstraintMatchTotals()) {
String constraintName = constraintMatchTotal.getConstraintName();
Number weightTotal = constraintMatchTotal.getWeightTotalAsNumber();
for (ConstraintMatch constraintMatch : constraintMatchTotal.getConstraintMatchSet()) {
List<Object> justificationList = constraintMatch.getJustificationList();
Number weight = constraintMatch.getWeightAsNumber();
...
}
}
Drools score calculation supports constraint matches automatically, but incremental Java score calculation requires requires implementing an extra interface (see that section).
The number of possible solutions for a planning problem can be mind blowing. For example:
Calculating the number of possible solutions depends on the design of the domain model:
An algorithm that checks every possible solution (even with pruning such as in Branch And Bound) 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, Late Acceptance, ...) usually perform best for real-world problems given real-world time limitations.
The business wants the optimal solution, but they also have other requirements:
Scale out: Large production datasets must not crash and have good results too.
Optimize the right problem: The constraints must match the actual business needs.
Available time: The solution must be found in time, before it becomes useless to execute.
Reliability: Every dataset must have at least a decent result (better than a human planner).
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, it 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 issues cannot be mitigated by hardware purchases later on. Start testing with a production sized dataset as soon as possible. Don't assess quality on small datasets (unless production encounters only such datasets). Instead, solve a production sized dataset and compare the results of longer executions, different algorithms and - if available - the human planner.
Each of these families of algorithms has multiple optimization algorithms:
Table 6.1. Optimization Algorithms Overview
Algorithm | Scalable? | Optimal? | Easy to use? | Tweakable? | Requires CH? |
---|---|---|---|---|---|
Exhaustive Search (ES) | |||||
Brute Force | 0/5 | 5/5 | 5/5 | 0/5 | No |
Branch And Bound | 0/5 | 5/5 | 4/5 | 2/5 | No |
Construction heuristics (CH) | |||||
First Fit | 5/5 | 1/5 | 5/5 | 1/5 | No |
First Fit Decreasing | 5/5 | 2/5 | 4/5 | 2/5 | No |
Weakest Fit | 5/5 | 2/5 | 4/5 | 2/5 | No |
Weakest Fit Decreasing | 5/5 | 2/5 | 4/5 | 2/5 | No |
Strongest Fit | 5/5 | 2/5 | 4/5 | 2/5 | No |
Strongest Fit Decreasing | 5/5 | 2/5 | 4/5 | 2/5 | No |
Cheapest Insertion | 3/5 | 2/5 | 5/5 | 2/5 | No |
Regret Insertion | 3/5 | 2/5 | 5/5 | 2/5 | No |
Metaheuristics (MH) | |||||
Local Search | |||||
Hill Climbing | 5/5 | 2/5 | 4/5 | 3/5 | Yes |
Tabu Search | 5/5 | 4/5 | 3/5 | 5/5 | Yes |
Simulated Annealing | 5/5 | 4/5 | 2/5 | 5/5 | Yes |
Late Acceptance | 5/5 | 4/5 | 3/5 | 5/5 | Yes |
Step Counting Hill Climbing | 5/5 | 4/5 | 3/5 | 5/5 | Yes |
Evolutionary Algorithms | |||||
Evolutionary Strategies | 4/5 | 3/5 | 2/5 | 5/5 | Yes |
Genetic Algorithms | 4/5 | 3/5 | 2/5 | 5/5 | Yes |
If you want to learn more about metaheuristics, read the free books Essentials of Metaheuristics or Clever Algorithms.
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, add Late Acceptance behind it:
First Fit Decreasing
Late Acceptance. A Late Acceptance size of 400 usually works well.
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 different Tabu Search, Simulated Annealing and Late Acceptance configurations, for example:
First Fit Decreasing
Tabu Search. An entity tabu size of 7 usually works well.
Use the Benchmarker to improve the values for those size parameters.
If it's worth your time, continue experimenting further. For example, try combining multiple algorithms together:
First Fit Decreasing
Late Acceptance (relatively long time)
Tabu Search (relatively short time)
The default parameter values are good enough for many cases (and especially for prototypes), but if development time allows, it can be well worth to power tweak them with the benchmarker for better results and scalability on a specific use case. The documentation for each optimization algorithm also declares its advanced configuration for power tweaking.
The default value of parameters will change between minor versions, to improve them for most users (but not necessary for you). To shield yourself from these changes, for better or worse, always use the advanced configuration. This is not recommended.
Here's a configuration that runs 3 phases in sequence:
<solver>
...
<constructionHeuristic>
... <!-- First phase: First Fit Decreasing -->
</constructionHeuristic>
<localSearch>
... <!-- Second phase: Late Acceptance -->
</localSearch>
<localSearch>
... <!-- Third phase: Tabu Search -->
</localSearch>
</solver>
<solver>
...
<termination><!-- Solver termination -->
<secondsSpentLimit>90</secondsSpentLimit>
</termination>
<localSearch>
<termination><!-- Phase termination -->
<secondsSpentLimit>60</secondsSpentLimit><!-- Give the next phase a chance to run too, before the Solver terminates -->
</termination>
...
</localSearch>
<localSearch>
...
</localSearch>
</solver>
If no termination is configured (and a metaheursitic algorithm is used), the Solver
will
run forever, untill terminateEarly() is called from another thread.
This is especially common during real-time planning.
For synchronous termination, configure a Termination
on a Solver
or a
Phase
when it needs to stop. You can implement your own Termination
, but the
built-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 spent solving and the estimated entire solving time of the Solver
or
Phase
.
Terminates when an amount of time has been used:
<termination>
<millisecondsSpentLimit>500</millisecondsSpentLimit>
</termination>
<termination>
<secondsSpentLimit>10</secondsSpentLimit>
</termination>
<termination>
<minutesSpentLimit>5</minutesSpentLimit>
</termination>
<termination>
<hoursSpentLimit>1</hoursSpentLimit>
</termination>
Terminates when the best score hasn't improved in an amount of time:
<localSearch>
<termination>
<unimprovedMillisecondsSpentLimit>500</unimprovedMillisecondsSpentLimit>
</termination>
</localSearch>
<localSearch>
<termination>
<unimprovedSecondsSpentLimit>10</unimprovedSecondsSpentLimit>
</termination>
</localSearch>
<localSearch>
<termination>
<unimprovedMinutesSpentLimit>5</unimprovedMinutesSpentLimit>
</termination>
</localSearch>
<localSearch>
<termination>
<unimprovedHoursSpentLimit>1</unimprovedHoursSpentLimit>
</termination>
</localSearch>
Terminates when a certain score has been reached. You can use this Termination
if you
know the perfect score, for example for 4 queens (which uses a SimpleScore):
<termination>
<bestScoreLimit>0</bestScoreLimit>
</termination>
For a planning problem with a HardSoftScore, it could look like this:
<termination>
<bestScoreLimit>0hard/-5000soft</bestScoreLimit>
</termination>
For a planning problem with a BendableScore with 3 hard levels and 1 soft level, it could look like this:
<termination>
<bestScoreLimit>0/0/0/-5000</bestScoreLimit>
</termination>
To terminate once a feasible solution has been reached, this Termination
isn't practical
because it requires a bestScoreLimit
such as 0hard/-2147483648soft
. Instead,
use the next termination.
<termination>
<bestScoreFeasible>true</bestScoreFeasible>
</termination>
This Termination
is usually combined with other terminations.
Terminates when an amount of steps has been reached:
<localSearch>
<termination>
<stepCountLimit>100</stepCountLimit>
</termination>
</localSearch>
This Termination
can only be used for a Phase
(such as
<localSearch>
), not for the Solver
itself.
Terminates when the best score hasn't improved in a number of steps:
<localSearch>
<termination>
<unimprovedStepCountLimit>100</unimprovedStepCountLimit>
</termination>
</localSearch>
This Termination
can only be used for a Phase
(such as
<localSearch>
), not for the Solver
itself.
<termination>
<terminationCompositionStyle>OR</terminationCompositionStyle>
<stepCountLimit>100</stepCountLimit>
<bestScoreLimit>0</bestScoreLimit>
</termination>
<termination>
<terminationCompositionStyle>AND</terminationCompositionStyle>
<unimprovedStepCountLimit>5</unimprovedStepCountLimit>
<bestScoreLimit>-100</bestScoreLimit>
</termination>
To listen to such events, add a SolverEventListener
to the
Solver
:
public interface Solver {
// ...
void addEventListener(SolverEventListener<? extends Solution> eventListener);
void removeEventListener(SolverEventListener<? extends Solution> eventListener);
}
solver.addEventListener(new SolverEventListener<CloudBalance>() {
public void bestSolutionChanged(BestSolutionChangedEvent<CloudBalance> event) {
// Ignore invalid solutions
if (event.isNewBestSolutionInitialized()
&& event.getNewBestSolution().getScore().isFeasible()) {
...
}
}
});
Most of the time, a custom construction heuristic is not worth the hassle. The supported constructions
heuristics are configurable (use the Benchmarker to tweak them),
Termination
aware and support partially initialized solutions too.
Implement the CustomPhaseCommand
interface:
public interface CustomPhaseCommand {
void changeWorkingSolution(ScoreDirector scoreDirector);
}
For example:
public class ToOriginalMachineSolutionInitializer implements CustomPhaseCommand {
public void changeWorkingSolution(ScoreDirector scoreDirector) {
MachineReassignment machineReassignment = (MachineReassignment) scoreDirector.getWorkingSolution();
for (MrProcessAssignment processAssignment : machineReassignment.getProcessAssignmentList()) {
scoreDirector.beforeVariableChanged(processAssignment, "machine");
processAssignment.setMachine(processAssignment.getOriginalMachine());
scoreDirector.afterVariableChanged(processAssignment, "machine");
}
}
}
Any change on the planning entities in a CustomPhaseCommand
must be notified to the
ScoreDirector
.
Do not change any of the problem facts in a CustomPhaseCommand
. That will corrupt the
Solver
because any previous score or solution was for a different problem. To do that, read
about repeated planning and do it with a ProblemFactChange instead.
Configure your CustomPhaseCommand
like this:
<solver>
...
<customPhase>
<customPhaseCommandClass>org.optaplanner.examples.machinereassignment.solver.solution.initializer.ToOriginalMachineSolutionInitializer</customPhaseCommandClass>
</customPhase>
... <!-- Other phases -->
</solver>
Configure multiple customPhaseCommandClass
instances to run them in sequence.
If the changes of a CustomPhaseCommand
don't result in a better score, the best solution
won't be changed (so effectively nothing will have changed for the next Phase
or
CustomPhaseCommand
). To force such changes anyway, use
forceUpdateBestSolution
:
<customPhase>
<customPhaseCommandClass>...MyUninitializer</customPhaseCommandClass>
<forceUpdateBestSolution>true</forceUpdateBestSolution>
</customPhase>
If the Solver
or a Phase
wants to terminate while a
CustomPhaseCommand
is still running, it will wait to terminate until the
CustomPhaseCommand
is done, however long that takes.
Move
and Neighborhood IntroductionMoveSelector
sSelector
FeaturesCacheType
: Create Moves Ahead of Time or Just In TimeCacheType
and SelectionOrder
Here's an example how to configure a changeMoveSelector
for the optimization algorithm
Local Search:
<localSearch>
<changeMoveSelector/>
...
</localSearch>
<changeMoveSelector/>
If there are multiple entity classes or multiple planning variables for 1 entity class, a simple
configuration will automatically unfold into a union of
ChangeMove
selectors for every planning variable.
Advanced configuration:
<changeMoveSelector>
... <!-- Normal selector properties -->
<entitySelector>
<entityClass>...Lecture</entityClass>
...
</entitySelector>
<valueSelector>
<variableName>room</variableName>
...
<nearbySelection>...</nearbySelection>
</valueSelector>
</changeMoveSelector>
A ChangeMove
is the finest grained move.
Almost every moveSelector
configuration injected into a metaheuristic algorithm should
include a changeMoveSelector or a custom implementation. This guarantees that every possible
Solution
can be reached through applying a number of moves in sequence (not taking score traps into account). Of course, normally it is unioned with other, more coarse
grained move selectors.
<swapMoveSelector/>
If there are multiple entity classes, a simple configuration will automatically unfold into a union of SwapMove
selectors for every entity class.
Advanced configuration:
<swapMoveSelector>
... <!-- Normal selector properties -->
<entitySelector>
<entityClass>...Lecture</entityClass>
...
</entitySelector>
<secondaryEntitySelector>
<entityClass>...Lecture</entityClass>
...
<nearbySelection>...</nearbySelection>
</secondaryEntitySelector>
<variableNameInclude>room</variableNameInclude>
<variableNameInclude>...</variableNameInclude>
</swapMoveSelector>
The secondaryEntitySelector
is rarely needed: if it is not specified, entities from the
same entitySelector
are swapped.
If one or more variableNameInclude
properties are specified, not all planning variables
will be swapped, but only those specified. For example for course scheduling, specifying only
variableNameInclude
room will make it only swap room, not period.
<pillarChangeMoveSelector/>
<pillarSwapMoveSelector>
... <!-- Normal selector properties -->
<pillarSelector>
<entitySelector>
<entityClass>...Lecture</entityClass>
...
</entitySelector>
<subPillarEnabled>true</subPillarEnabled>
<minimumSubPillarSize>1</minimumSubPillarSize>
<maximumSubPillarSize>1000</maximumSubPillarSize>
</pillarSelector>
<valueSelector>
<variableName>room</variableName>
...
</valueSelector>
</pillarSwapMoveSelector>
The number of sub pillars of a pillar is exponential to the size of the pillar. For example a pillar of
size 32 has (2^32 - 1)
subpillars. Therefore a pillarSelector
only
supports JIT random selection (which is the default).
The other properties are explained in changeMoveSelector.
<pillarSwapMoveSelector/>
<pillarSwapMoveSelector>
... <!-- Normal selector properties -->
<pillarSelector>
<entitySelector>
<entityClass>...Lecture</entityClass>
...
</entitySelector>
<subPillarEnabled>true</subPillarEnabled>
<minimumSubPillarSize>1</minimumSubPillarSize>
<maximumSubPillarSize>1000</maximumSubPillarSize>
</pillarSelector>
<secondaryPillarSelector>
<entitySelector>
...
</entitySelector>
...
</secondaryPillarSelector>
<variableNameInclude>room</variableNameInclude>
<variableNameInclude>...</variableNameInclude>
</pillarSwapMoveSelector>
The other properties are explained in swapMoveSelector and pillarChangeMoveSelector.
<tailChainSwapMoveSelector/>
<subChainChangeMoveSelector>
... <!-- Normal selector properties -->
<entitySelector>
<entityClass>...Customer</entityClass>
...
</entitySelector>
<valueSelector>
<variableName>previousStandstill</variableName>
...
<nearbySelection>...</nearbySelection>
</valueSelector>
</subChainChangeMoveSelector>
The entitySelector
selects the start of the tail chain that is being moved. The
valueSelector selects to where that tail chain is moved. If it has a tail chain itself, that is moved to the
location of the original tail chain. It uses a valueSelector
instead of a
secondaryEntitySelector
to be able to include all possible 2opt moves (such as moving to the
end of a tail) and to work correctly with nearby selection (because of
asymmetric distances and also swapped entity distance gives an incorrect selection probability).
Although subChainChangeMoveSelector
and subChainSwapMoveSelector
include almost every possible tailChainSwapMove
, experiments have shown that focusing on
tailChainSwapMove
s increases efficiency.
<subChainChangeMoveSelector/>
<subChainChangeMoveSelector>
... <!-- Normal selector properties -->
<entityClass>...Customer</entityClass>
<subChainSelector>
<valueSelector>
<variableName>previousStandstill</variableName>
...
</valueSelector>
<minimumSubChainSize>2</minimumSubChainSize>
<maximumSubChainSize>40</maximumSubChainSize>
</subChainSelector>
<valueSelector>
<variableName>previousStandstill</variableName>
...
</valueSelector>
<selectReversingMoveToo>true</selectReversingMoveToo>
</subChainChangeMoveSelector>
<subChainSwapMoveSelector/>
<subChainSwapMoveSelector>
... <!-- Normal selector properties -->
<entityClass>...Customer</entityClass>
<subChainSelector>
<valueSelector>
<variableName>previousStandstill</variableName>
...
</valueSelector>
<minimumSubChainSize>2</minimumSubChainSize>
<maximumSubChainSize>40</maximumSubChainSize>
</subChainSelector>
<secondarySubChainSelector>
<valueSelector>
<variableName>previousStandstill</variableName>
...
</valueSelector>
<minimumSubChainSize>2</minimumSubChainSize>
<maximumSubChainSize>40</maximumSubChainSize>
</secondarySubChainSelector>
<selectReversingMoveToo>true</selectReversingMoveToo>
</subChainSwapMoveSelector>
The other properties are explained in subChainChangeMoveSelector.
<unionMoveSelector>
<...MoveSelector/>
<...MoveSelector/>
<...MoveSelector/>
...
</unionMoveSelector>
<unionMoveSelector>
... <!-- Normal selector properties -->
<selectorProbabilityWeightFactoryClass>...ProbabilityWeightFactory</selectorProbabilityWeightFactoryClass>
<changeMoveSelector>
<fixedProbabilityWeight>...</fixedProbabilityWeight>
...
</changeMoveSelector>
<swapMoveSelector>
<fixedProbabilityWeight>...</fixedProbabilityWeight>
...
</swapMoveSelector>
<...MoveSelector>
<fixedProbabilityWeight>...</fixedProbabilityWeight>
...
</...MoveSelector>
...
</unionMoveSelector>
<unionMoveSelector>
<changeMoveSelector>
<fixedProbabilityWeight>1.0</fixedProbabilityWeight>
...
</changeMoveSelector>
<swapMoveSelector>
<fixedProbabilityWeight>2.0</fixedProbabilityWeight>
...
</swapMoveSelector>
</unionMoveSelector>
<unionMoveSelector>
<selectorProbabilityWeightFactoryClass>org.optaplanner.core.impl.heuristic.selector.common.decorator.FairSelectorProbabilityWeightFactory</selectorProbabilityWeightFactoryClass>
<changeMoveSelector/>
<swapMoveSelector/>
</unionMoveSelector>
<cartesianProductMoveSelector>
<...MoveSelector/>
<...MoveSelector/>
<...MoveSelector/>
...
</cartesianProductMoveSelector>
<cartesianProductMoveSelector>
... <!-- Normal selector properties -->
<ignoreEmptyChildIterators>true</ignoreEmptyChildIterators>
<changeMoveSelector>
...
</changeMoveSelector>
<swapMoveSelector>
...
</swapMoveSelector>
<...MoveSelector>
...
</...MoveSelector>
...
</cartesianProductMoveSelector>
<entitySelector/>
<entitySelector>
... <!-- Normal selector properties -->
<entityClass>org.optaplanner.examples.curriculumcourse.domain.Lecture</entityClass>
</entitySelector>
<valueSelector/>
<valueSelector>
... <!-- Normal selector properties -->
<variableName>room</variableName>
</valueSelector>
<valueSelector>
<downcastEntityClass>...LeadingExam</downcastEntityClass>
<variableName>period</variableName>
</valueSelector>
If a selected entity cannot be downcasted, the ValueSelector
is empty for that
entity.
Almost every Selector
supports setting a cacheType
:
<changeMoveSelector>
<cacheType>PHASE</cacheType>
...
</changeMoveSelector>
The following cacheType
s are supported:
A cacheType
can be set on composite selectors too:
<unionMoveSelector>
<cacheType>PHASE</cacheType>
<changeMoveSelector/>
<swapMoveSelector/>
...
</unionMoveSelector>
Almost every Selector
supports setting a selectionOrder
:
<changeMoveSelector>
...
<selectionOrder>RANDOM</selectionOrder>
...
</changeMoveSelector>
The following selectionOrder
s are supported:
SORTED: Select the selections (Move
s, entities, values, ...) in sorted order. Each
selection will be selected only once. Requires cacheType >= STEP
. Mostly used on an
entitySelector
or valueSelector
for construction heuristics. See sorted selection.
For example: A0, B0, C0, ..., A2, B2, C2, ..., A1, B1, C1, ...
RANDOM (default): Select the selections (Move
s, entities, values, ...) in
non-shuffled random order. A selection might be selected multiple times. This scales up well in performance
because it does not require caching.
For example: C2, A3, B1, C2, A0, C0, ...
SHUFFLED: Select the selections (Move
s, entities, values, ...) in shuffled random
order. Each selection will be selected only once. Requires cacheType >= STEP
. This
scales up badly in performance, not just because it requires caching, but also because a random number is
generated for each element, even if it's not selected (which is the grand majority when scaling up).
For example: C2, A3, B1, A0, C0, ...
PROBABILISTIC: Select the selections (Move
s, entities, values, ...) in random order,
based on the selection probability of each element. A selection with a higher probability has a higher chance
to be selected than elements with a lower probability. A selection might be selected multiple times. Requires
cacheType >= STEP
. Mostly used on an entitySelector
or
valueSelector
. See probabilistic
selection.
For example: B1, B1, A1, B2, B1, C2, B1, B1, ...
A selectionOrder
can be set on composite selectors too.
When a Selector
is cached, all of its nested Selector
s will
naturally default to selectionOrder
ORIGINAL
. Avoid overwriting the
selectionOrder
of those nested Selector
s.
<unionMoveSelector>
<cacheType>PHASE</cacheType>
<selectionOrder>SHUFFLED</selectionOrder>
<changeMoveSelector/>
<swapMoveSelector/>
</unionMoveSelector>
Notice that each Move
will only be selected once, even
though they are selected in random order.
<unionMoveSelector>
<cacheType>STEP</cacheType>
<selectionOrder>SHUFFLED</selectionOrder>
<changeMoveSelector>
<cacheType>PHASE</cacheType>
</changeMoveSelector>
<swapMoveSelector/>
<cacheType>PHASE</cacheType>
</swapMoveSelector>
<pillarSwapMoveSelector/><!-- Does not support cacheType PHASE -->
</unionMoveSelector>
There can be certain moves that you don't want to select, because:
Doing the move would break a built-in hard constraint, so the solution would be infeasible but the score function doesn't check built-in hard constraints (for performance gain). For example, don't change a gym lecture to a room which is not a gym room.
Any built-in hard constraint must probably be filtered on every move type of every solver phase. For example if it's filters the change move of Local Search, it must also filter the swap move that swaps the room of a gym lecture with another lecture for which the other lecture's original room isn't a gym room. Furthermore, it must also filter the change moves of the Construction Heuristics (which requires an advanced configuration).
Filtered selection can happen on any Selector in the selector tree, including any
MoveSelector
, EntitySelector
or ValueSelector
. It works
with any cacheType
and selectionOrder
.
Filtering uses the interface SelectionFilter
:
public interface SelectionFilter<T> {
boolean accept(ScoreDirector scoreDirector, T selection);
}
Implement the accept
method to return false
on a discarded
selection
. Unaccepted moves will not be selected and will therefore never have their
doMove
method 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 filterClass
on the
moveSelector
:
<swapMoveSelector>
<filterClass>org.optaplanner.examples.curriculumcourse.solver.move.DifferentCourseSwapMoveFilter</filterClass>
</swapMoveSelector>
But if possible, apply it on a lower levels, such as a filterClass
on the
entitySelector
or valueSelector
:
<changeMoveSelector>
<entitySelector>
<filterClass>...EntityFilter</filterClass>
</entitySelector>
</changeMoveSelector>
You can configure multiple filterClass
elements on a single selector.
It's mostly used in construction heuristics.
Some Selector
types implement a SorterManner
out of the box:
DECREASING_DIFFICULTY
: Sorts the planning entities according to decreasing
planning entity difficulty. Requires that planning
entity difficulty is annotated on the domain model.
<entitySelector>
<cacheType>PHASE</cacheType>
<selectionOrder>SORTED</selectionOrder>
<sorterManner>DECREASING_DIFFICULTY</sorterManner>
</entitySelector>
ValueSelector
supports:
INCREASING_STRENGTH
: Sorts the planning values according to increasing planning value strength. Requires that planning value strength is
annotated on the domain model.
<valueSelector>
<cacheType>PHASE</cacheType>
<selectionOrder>SORTED</selectionOrder>
<sorterManner>INCREASING_STRENGTH</sorterManner>
</valueSelector>
An easy way to sort a Selector
is with a plain old
Comparator
:
public class CloudProcessDifficultyComparator implements Comparator<CloudProcess> {
public int compare(CloudProcess a, CloudProcess b) {
return new CompareToBuilder()
.append(a.getRequiredMultiplicand(), b.getRequiredMultiplicand())
.append(a.getId(), b.getId())
.toComparison();
}
}
<entitySelector>
<cacheType>PHASE</cacheType>
<selectionOrder>SORTED</selectionOrder>
<sorterComparatorClass>...CloudProcessDifficultyComparator</sorterComparatorClass>
<sorterOrder>DESCENDING</sorterOrder>
</entitySelector>
If you need the entire Solution
to sort a Selector
, use a
SelectionSorterWeightFactory
instead:
public interface SelectionSorterWeightFactory<Sol extends Solution, T> {
Comparable createSorterWeight(Sol solution, T selection);
}
public class QueenDifficultyWeightFactory implements SelectionSorterWeightFactory<NQueens, Queen> {
public Comparable createSorterWeight(NQueens nQueens, Queen queen) {
int distanceFromMiddle = calculateDistanceFromMiddle(nQueens.getN(), queen.getColumnIndex());
return new QueenDifficultyWeight(queen, distanceFromMiddle);
}
// ...
public static class QueenDifficultyWeight implements Comparable<QueenDifficultyWeight> {
private final Queen queen;
private final int distanceFromMiddle;
public QueenDifficultyWeight(Queen queen, int distanceFromMiddle) {
this.queen = queen;
this.distanceFromMiddle = distanceFromMiddle;
}
public int compareTo(QueenDifficultyWeight other) {
return new CompareToBuilder()
// The more difficult queens have a lower distance to the middle
.append(other.distanceFromMiddle, distanceFromMiddle) // Decreasing
// Tie breaker
.append(queen.getColumnIndex(), other.queen.getColumnIndex())
.toComparison();
}
}
}
<entitySelector>
<cacheType>PHASE</cacheType>
<selectionOrder>SORTED</selectionOrder>
<sorterWeightFactoryClass>...QueenDifficultyWeightFactory</sorterWeightFactoryClass>
<sorterOrder>DESCENDING</sorterOrder>
</entitySelector>
Alternatively, you can also use the interface SelectionSorter
directly:
public interface SelectionSorter<T> {
void sort(ScoreDirector scoreDirector, List<T> selectionList);
}
<entitySelector>
<cacheType>PHASE</cacheType>
<selectionOrder>SORTED</selectionOrder>
<sorterClass>...MyEntitySorter</sorterClass>
</entitySelector>
Each selection has a probabilityWeight
, which determines the chance that selection will
be selected:
public interface SelectionProbabilityWeightFactory<T> {
double createProbabilityWeight(ScoreDirector scoreDirector, T selection);
}
<entitySelector>
<cacheType>PHASE</cacheType>
<selectionOrder>PROBABILISTIC</selectionOrder>
<probabilityWeightFactoryClass>...MyEntityProbabilityWeightFactoryClass</probabilityWeightFactoryClass>
</entitySelector>
Selecting all possible moves sometimes does not scale well enough, especially for construction heuristics (which don't support acceptedCountLimit).
To limit the number of selected selection per step, apply a selectedCountLimit
on the
selector:
<changeMoveSelector>
<selectedCountLimit>100</selectedCountLimit>
</changeMoveSelector>
To scale Local Search, setting acceptedCountLimit is usually
better than using selectedCountLimit
.
<cartesianProductMoveSelector>
<changeMoveSelector>
<entitySelector id="entitySelector"/>
<valueSelector>
<variableName>period</variableName>
</valueSelector>
</changeMoveSelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="entitySelector"/>
<valueSelector>
<variableName>room</variableName>
</valueSelector>
</changeMoveSelector>
</cartesianProductMoveSelector>
Mimic selection is useful to create a composite move from 2 moves that affect the same entity.
public interface NearbyDistanceMeter<O, D> {
double getNearbyDistance(O origin, D destination);
}
It returns a double
which represents the distance:
public class CustomerNearbyDistanceMeter implements NearbyDistanceMeter<Customer, Standstill> {
public double getNearbyDistance(Customer origin, Standstill destination) {
return origin.getDistanceTo(destination);
}
}
To configure nearby selection, add a nearbySelection
element in the
entitySelector
or valueSelector
and use mimic
selection to specify which entity should be near by the selection.
<unionMoveSelector>
<changeMoveSelector>
<entitySelector id="entitySelector1"/>
<valueSelector>
<nearbySelection>
<originEntitySelector mimicSelectorRef="entitySelector1"/>
<nearbyDistanceMeterClass>...CustomerNearbyDistanceMeter</nearbyDistanceMeterClass>
<parabolicDistributionSizeMaximum>40</parabolicDistributionSizeMaximum>
</nearbySelection>
</valueSelector>
</changeMoveSelector>
<swapMoveSelector>
<entitySelector id="entitySelector2"/>
<secondaryEntitySelector>
<nearbySelection>
<originEntitySelector mimicSelectorRef="entitySelector2"/>
<nearbyDistanceMeterClass>...CustomerNearbyDistanceMeter</nearbyDistanceMeterClass>
<parabolicDistributionSizeMaximum>40</parabolicDistributionSizeMaximum>
</nearbySelection>
</secondaryEntitySelector>
</swapMoveSelector>
<tailChainSwapMoveSelector>
<entitySelector id="entitySelector3"/>
<valueSelector>
<nearbySelection>
<originEntitySelector mimicSelectorRef="entitySelector3"/>
<nearbyDistanceMeterClass>...CustomerNearbyDistanceMeter</nearbyDistanceMeterClass>
<parabolicDistributionSizeMaximum>40</parabolicDistributionSizeMaximum>
</nearbySelection>
</valueSelector>
</tailChainSwapMoveSelector>
</unionMoveSelector>
To allow every element to be selected, regardless of the number of entities, only set the distribution type without the size maximum parameter:
<nearbySelection>
<nearbySelectionDistributionType>PARABOLIC_DISTRIBUTION</nearbySelectionDistributionType>
</nearbySelection>
The following NearbySelectionDistributionType
s are supported:
BLOCK_DISTRIBUTION
: Only the n nearest are selected, with an equal probability. For
example, select the 20 nearest:
<nearbySelection>
<blockDistributionSizeMaximum>20</blockDistributionSizeMaximum>
</nearbySelection>
LINEAR_DISTRIBUTION
: Nearest elements are selected with a higher probability. The
probability decreases linearly.
<nearbySelection>
<linearDistributionSizeMaximum>40</linearDistributionSizeMaximum>
</nearbySelection>
PARABOLIC_DISTRIBUTION
(recommended): Nearest elements are selected with a higher
probability.
<nearbySelection>
<parabolicDistributionSizeMaximum>80</parabolicDistributionSizeMaximum>
</nearbySelection>
BETA_DISTRIBUTION
: Selection according to a beta distribution. Slows down the solver
significantly.
<nearbySelection>
<betaDistributionAlpha>1</betaDistributionAlpha>
<betaDistributionBeta>5</betaDistributionBeta>
</nearbySelection>
As always, use the Benchmarker to tweak values if desired.
To determine which move types might be missing in your implementation, run a Benchmarker for a short amount of time and configure it to write the best solutions to disk. Take a look at such a best solution: it will likely be a local optima. Try to figure out if there's a move that could get out of that local optima faster.
If you find one, implement that coarse-grained move, mix it with the existing moves and benchmark it against the previous configurations to see if you want to keep it.
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 extends AbstractMove {
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.
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
}
A Move
can only change/add/remove planning entities, it must not change any of the
problem facts.
public boolean isMoveDoable(ScoreDirector scoreDirector) {
return !ObjectUtils.equals(queen.getRow(), toRow);
}
public Move createUndoMove(ScoreDirector scoreDirector) {
return new RowChangeMove(queen, queen.getRow());
}
public List<? extends Object> getPlanningEntities() {
return Collections.singletonList(queen);
}
public Collection<? extends Object> getPlanningValues() {
return Collections.singletonList(toRow);
}
public Collection<? extends Object> getPlanningEntities() {
return Arrays.asList(leftCloudProcess, rightCloudProcess);
}
public Collection<? extends Object> getPlanningValues() {
return Arrays.asList(leftCloudProcess.getComputer(), rightCloudProcess.getComputer());
}
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (o 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();
}
Implement the toString()
method to keep Planner's logs readable:
public String toString() {
return queen + " {" + queen.getRow() + " -> " + 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<S extends Solution> {
List<Move> createMoveList(S solution);
}
public class RowChangeMoveFactory implements MoveListFactory<NQueens> {
public List<Move> createMoveList(NQueens nQueens) {
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.optaplanner.examples.nqueens.solver.move.factory.RowChangeMoveFactory</moveListFactoryClass>
</moveListFactory>
<moveListFactory>
... <!-- Normal moveSelector properties -->
<moveListFactoryClass>org.optaplanner.examples.nqueens.solver.move.factory.RowChangeMoveFactory</moveListFactoryClass>
</moveListFactory>
Use this advanced form to generate custom moves by implementing the MoveIteratorFactory
interface:
public interface MoveIteratorFactory {
long getSize(ScoreDirector scoreDirector);
Iterator<Move> createOriginalMoveIterator(ScoreDirector scoreDirector);
Iterator<Move> createRandomMoveIterator(ScoreDirector scoreDirector, Random workingRandom);
}
Simple configuration (which can be nested in a unionMoveSelector
just like any other
MoveSelector
):
<moveIteratorFactory>
<moveIteratorFactoryClass>...</moveIteratorFactoryClass>
</moveIteratorFactory>
<moveIteratorFactory>
... <!-- Normal moveSelector properties -->
<moveIteratorFactoryClass>...</moveIteratorFactoryClass>
</moveIteratorFactory>
Notice that Branch And Bound (much like Brute Force) creates a search tree that explodes exponentially as the problem size increases. So it hits the same scalability wall, only a little bit later.
Branch And Bound is mostly unusable for a real-world problem due to time limitations, as shown in scalability of Exhaustive Search.
Simplest configuration of Branch And Bound:
<solver>
...
<exhaustiveSearch>
<exhaustiveSearchType>BRANCH_AND_BOUND</exhaustiveSearchType>
</exhaustiveSearch>
</solver>
For the pruning to work with the default ScoreBounder
, the InitializingScoreTrend should be set. Especially an InitializingScoreTrend of ONLY_DOWN
(or at least has
ONLY_DOWN
in the leading score levels) prunes a lot.
Advanced configuration:
<exhaustiveSearch>
<exhaustiveSearchType>BRANCH_AND_BOUND</exhaustiveSearchType>
<nodeExplorationType>DEPTH_FIRST</nodeExplorationType>
<entitySorterManner>DECREASING_DIFFICULTY_IF_AVAILABLE</entitySorterManner>
<valueSorterManner>INCREASING_STRENGTH_IF_AVAILABLE</valueSorterManner>
</exhaustiveSearch>
The nodeExplorationType
options are:
DEPTH_FIRST
(default): Explore deeper nodes first (and then a better score and then a
better optimistic bound). Deeper nodes (especially leaf nodes) often improve the pessimistic bound. A better
pessimistic bound allows pruning more nodes to reduce the search space.
<exhaustiveSearch>
<exhaustiveSearchType>BRANCH_AND_BOUND</exhaustiveSearchType>
<nodeExplorationType>DEPTH_FIRST</nodeExplorationType>
</exhaustiveSearch>
BREADTH_FIRST
(not recommended): Explore nodes layer by layer (and then a better
score and then a better optimistic bound). Scales terribly in memory (and usually in performance too).
<exhaustiveSearch>
<exhaustiveSearchType>BRANCH_AND_BOUND</exhaustiveSearchType>
<nodeExplorationType>BREADTH_FIRST</nodeExplorationType>
</exhaustiveSearch>
SCORE_FIRST
: Explore nodes with a better score first (and then a better optimistic
bound and then deeper nodes first). Might scale as terribly as BREADTH_FIRST
in some
cases.
<exhaustiveSearch>
<exhaustiveSearchType>BRANCH_AND_BOUND</exhaustiveSearchType>
<nodeExplorationType>SCORE_FIRST</nodeExplorationType>
</exhaustiveSearch>
OPTIMISTIC_BOUND_FIRST
: Explore nodes with a better optimistic bound first (and then
a better score and then deeper nodes first). Might scale as terribly as BREADTH_FIRST
in
some cases.
<exhaustiveSearch>
<exhaustiveSearchType>BRANCH_AND_BOUND</exhaustiveSearchType>
<nodeExplorationType>OPTIMISTIC_BOUND_FIRST</nodeExplorationType>
</exhaustiveSearch>
The entitySorterManner
options are:
DECREASING_DIFFICULTY
: Initialize the more difficult planning entities first. This
usually increases pruning (and therefore improves scalability). Requires the model to support planning entity difficulty comparison.
DECREASING_DIFFICULTY_IF_AVAILABLE
(default): If the model supports planning entity difficulty comparison, behave like
DECREASING_DIFFICULTY
, else like NONE
.
NONE
: Initialize the planning entities in original order.
The valueSorterManner
options are:
INCREASING_STRENGTH
: Evaluate the planning values in increasing strength. Requires
the model to support planning value strength comparison.
INCREASING_STRENGTH_IF_AVAILABLE
(default): If the model supports planning value strength comparison, behave like
INCREASING_STRENGTH
, else like NONE
.
DECREASING_STRENGTH
: Evaluate the planning values in decreasing strength. Requires
the model to support planning value strength comparison.
DECREASING_STRENGTH_IF_AVAILABLE
: If the model supports planning value strength comparison, behave like
DECREASING_STRENGTH
, else like NONE
.
NONE
: Try the planning values in original order.
Exhaustive Search variants suffer from 2 big scalability issues:
As shown in these time spent graphs from the Benchmarker, Brute Force and Branch And Bound both hit a performance scalability wall. For example, on N queens it hits wall at a few dozen queens:
In most use cases, such as Cloud Balancing, the wall appears out of thin air:
Exhaustive Search hits this wall on small datasets already, so in production these optimizations algorithms are mostly useless. Use Construction Heuristics with Local Search instead: those can handle thousands of queens/computers easily.
Throwing hardware at these scalability issues has no noticeable impact. Newer and more hardware are just a drop in the ocean. Moore's law cannot win against the onslaught of a few more planning entities in the dataset.
<constructionHeuristic>
<constructionHeuristicType>FIRST_FIT</constructionHeuristicType>
</constructionHeuristic>
If the InitializingScoreTrend is
ONLY_DOWN
, this algorithm is faster: for an entity, it picks the first move for which the
score does not deteriorate the last step score, ignoring all subsequent moves.
For advanced configuration, see Allocate Entity From Queue.
Requires the model to support planning entity difficulty comparison.
One would expect that this algorithm has better results than First Fit. That's usually the case, but not always.
<constructionHeuristic>
<constructionHeuristicType>FIRST_FIT_DECREASING</constructionHeuristicType>
</constructionHeuristic>
If the InitializingScoreTrend is
ONLY_DOWN
, this algorithm is faster: for an entity, it picks the first move for which the
score does not deteriorate the last step score, ignoring all subsequent moves.
For advanced configuration, see Allocate Entity From Queue.
Requires the model to support planning value strength comparison.
Do not presume that this algorithm has better results than First Fit. That's often not the case.
<constructionHeuristic>
<constructionHeuristicType>WEAKEST_FIT</constructionHeuristicType>
</constructionHeuristic>
If the InitializingScoreTrend is
ONLY_DOWN
, this algorithm is faster: for an entity, it picks the first move for which the
score does not deteriorate the last step score, ignoring all subsequent moves.
For advanced configuration, see Allocate Entity From Queue.
Requires the model to support planning entity difficulty comparison and planning value strength comparison.
Do not presume that this algorithm has better results than First Fit Decreasing. That's often not the case. However, it is usually better than Weakest Fit.
<constructionHeuristic>
<constructionHeuristicType>WEAKEST_FIT_DECREASING</constructionHeuristicType>
</constructionHeuristic>
If the InitializingScoreTrend is
ONLY_DOWN
, this algorithm is faster: for an entity, it picks the first move for which the
score does not deteriorate the last step score, ignoring all subsequent moves.
For advanced configuration, see Allocate Entity From Queue.
Requires the model to support planning value strength comparison.
Do not presume that this algorithm has better results than First Fit or Weakest Fit. That's often not the case.
<constructionHeuristic>
<constructionHeuristicType>STRONGEST_FIT</constructionHeuristicType>
</constructionHeuristic>
If the InitializingScoreTrend is
ONLY_DOWN
, this algorithm is faster: for an entity, it picks the first move for which the
score does not deteriorate the last step score, ignoring all subsequent moves.
For advanced configuration, see Allocate Entity From Queue.
Requires the model to support planning entity difficulty comparison and planning value strength comparison.
Do not presume that this algorithm has better results than First Fit Decreasing or Weakest Fit Decreasing. That's often not the case. However, it is usually better than Strongest Fit.
<constructionHeuristic>
<constructionHeuristicType>STRONGEST_FIT_DECREASING</constructionHeuristicType>
</constructionHeuristic>
If the InitializingScoreTrend is
ONLY_DOWN
, this algorithm is faster: for an entity, it picks the first move for which the
score does not deteriorate the last step score, ignoring all subsequent moves.
For advanced configuration, see Allocate Entity From Queue.
Allocate Entity From Queue is a versatile, generic form of First Fit, First Fit Decreasing, Weakest Fit and Weakest Fit Decreasing. It works like this:
Put all entities in a queue.
Assign the first entity (from that queue) to the best value.
Repeat until all entities are assigned.
<constructionHeuristic>
<constructionHeuristicType>ALLOCATE_ENTITY_FROM_QUEUE</constructionHeuristicType>
</constructionHeuristic>
<constructionHeuristic>
<constructionHeuristicType>ALLOCATE_ENTITY_FROM_QUEUE</constructionHeuristicType>
<entitySorterManner>DECREASING_DIFFICULTY_IF_AVAILABLE</entitySorterManner>
<valueSorterManner>INCREASING_STRENGTH_IF_AVAILABLE</valueSorterManner>
</constructionHeuristic>
The entitySorterManner
options are:
DECREASING_DIFFICULTY
: Initialize the more difficult planning entities first. This
usually increases pruning (and therefore improves scalability). Requires the model to support planning entity difficulty comparison.
DECREASING_DIFFICULTY_IF_AVAILABLE
(default): If the model supports planning entity difficulty comparison, behave like
DECREASING_DIFFICULTY
, else like NONE
.
NONE
: Initialize the planning entities in original order.
The valueSorterManner
options are:
INCREASING_STRENGTH
: Evaluate the planning values in increasing strength. Requires
the model to support planning value strength comparison.
INCREASING_STRENGTH_IF_AVAILABLE
(default): If the model supports planning value strength comparison, behave like
INCREASING_STRENGTH
, else like NONE
.
DECREASING_STRENGTH
: Evaluate the planning values in decreasing strength. Requires
the model to support planning value strength comparison.
DECREASING_STRENGTH_IF_AVAILABLE
: If the model supports planning value strength comparison, behave like
DECREASING_STRENGTH
, else like NONE
.
NONE
: Try the planning values in original order.
Advanced detailed configuration. For example, a Weakest Fit Decreasing configuration for a single entity class with a single variable:
<constructionHeuristic>
<queuedEntityPlacer>
<entitySelector id="placerEntitySelector">
<cacheType>PHASE</cacheType>
<selectionOrder>SORTED</selectionOrder>
<sorterManner>DECREASING_DIFFICULTY</sorterManner>
</entitySelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="placerEntitySelector"/>
<valueSelector>
<cacheType>PHASE</cacheType>
<selectionOrder>SORTED</selectionOrder>
<sorterManner>INCREASING_STRENGTH</sorterManner>
</valueSelector>
</changeMoveSelector>
</queuedEntityPlacer>
</constructionHeuristic>
Per step, the QueuedEntityPlacer
selects 1 uninitialized entity from the
EntitySelector
and applies the winning Move
(out of all the moves for that
entity generated by the MoveSelector
). The mimic
selection ensures that the winning Move
changes (only) the selected entity.
To customize the entity or value sorting, see sorted selection. Other
Selector
customization (such as filtering and limiting) is supported too.
There are 2 ways to deal with multiple variables, depending on how their ChangeMove
s are
combined:
For example, presume a course scheduling example with 200 rooms and 40 periods.
This First Fit configuration for a single entity class with 2 variables, using a cartesian product of their ChangeMove
s, will
select 8000 moves per entity:
<constructionHeuristic>
<queuedEntityPlacer>
<entitySelector id="placerEntitySelector">
<cacheType>PHASE</cacheType>
</entitySelector>
<cartesianProductMoveSelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="placerEntitySelector"/>
<valueSelector>
<variableName>room</variableName>
</valueSelector>
</changeMoveSelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="placerEntitySelector"/>
<valueSelector>
<variableName>period</variableName>
</valueSelector>
</changeMoveSelector>
</cartesianProductMoveSelector>
</queuedEntityPlacer>
...
</constructionHeuristic>
With 3 variables of 1000 values each, a cartesian product selects 1000000000 values per entity, which will take far too long.
This First Fit configuration for a single entity class with 2 variables, using sequential
ChangeMove
s, will select 240 moves per entity:
<constructionHeuristic>
<queuedEntityPlacer>
<entitySelector id="placerEntitySelector">
<cacheType>PHASE</cacheType>
</entitySelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="placerEntitySelector"/>
<valueSelector>
<variableName>period</variableName>
</valueSelector>
</changeMoveSelector>
<changeMoveSelector>
<entitySelector mimicSelectorRef="placerEntitySelector"/>
<valueSelector>
<variableName>room</variableName>
</valueSelector>
</changeMoveSelector>
</queuedEntityPlacer>
...
</constructionHeuristic>
Especially for sequential ChangeMove
s, the order of the variables is important. In the
example above, it's better to select the period first (instead of the other way around), because there are more
hard constraints that do not involve the room (for example: no teacher should teach 2 lectures at the same
time). Let the Benchmarker guide you.
With 3 or more variables, it's possible to combine the cartesian product and sequential techniques:
<constructionHeuristic>
<queuedEntityPlacer>
...
<cartesianProductMoveSelector>
<changeMoveSelector>...</changeMoveSelector>
<changeMoveSelector>...</changeMoveSelector>
</cartesianProductMoveSelector>
<changeMoveSelector>...</changeMoveSelector>
</queuedEntityPlacer>
...
</constructionHeuristic>
There are several pick early types for Construction Heuristics:
NEVER
: Evaluate all the selected moves to initialize the variable(s). This is the
default if the InitializingScoreTrend is not
ONLY_DOWN
.
<constructionHeuristic>
...
<forager>
<pickEarlyType>NEVER</pickEarlyType>
</forager>
</constructionHeuristic>
FIRST_NON_DETERIORATING_SCORE
: Initialize the variable(s) with the first move that
doesn't deteriorate the score, ignore the remaining selected moves. This is the default if the InitializingScoreTrend is ONLY_DOWN
.
<constructionHeuristic>
...
<forager>
<pickEarlyType>FIRST_NON_DETERIORATING_SCORE</pickEarlyType>
</forager>
</constructionHeuristic>
If there are only negative constraints, but the InitializingScoreTrend is strictly not ONLY_DOWN
,
it can sometimes make sense to apply FIRST_NON_DETERIORATING_SCORE. Use the Benchmarker to decide if the score quality loss is worth the time gain.
FIRST_FEASIBLE_SCORE
: Initialize the variable(s) with the first move that has a
feasible score.
<constructionHeuristic>
...
<forager>
<pickEarlyType>FIRST_FEASIBLE_SCORE</pickEarlyType>
</forager>
</constructionHeuristic>
If the InitializingScoreTrend is
ONLY_DOWN
, use FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD
instead,
because that's faster without any disadvantages.
FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD
: Initialize the variable(s) with the
first move that doesn't deteriorate the feasibility of the score any further.
<constructionHeuristic>
...
<forager>
<pickEarlyType>FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD</pickEarlyType>
</forager>
</constructionHeuristic>
Simplest configuration of Cheapest Insertion:
<constructionHeuristic>
<constructionHeuristicType>CHEAPEST_INSERTION</constructionHeuristicType>
</constructionHeuristic>
If the InitializingScoreTrend is
ONLY_DOWN
, this algorithm is faster: for an entity, it picks the first move for which the
score does not deteriorate the last step score, ignoring all subsequent moves.
For advanced configuration, see Allocate from pool.
Allocate From Pool is a versatile, generic form of Cheapest Insertion and Regret Insertion. It works like this:
Put all entity-value combinations in a pool.
Assign the best entity to best value.
Repeat until all entities are assigned.
<constructionHeuristic>
<constructionHeuristicType>ALLOCATE_FROM_POOL</constructionHeuristicType>
</constructionHeuristic>
<constructionHeuristic>
<constructionHeuristicType>ALLOCATE_FROM_POOL</constructionHeuristicType>
<entitySorterManner>DECREASING_DIFFICULTY_IF_AVAILABLE</entitySorterManner>
<valueSorterManner>INCREASING_STRENGTH_IF_AVAILABLE</valueSorterManner>
</constructionHeuristic>
The entitySorterManner
and valueSorterManner
options are described in
Allocate Entity From Queue.
Advanced detailed configuration. For example, a Cheapest Insertion configuration for a single entity class with a single variable:
<constructionHeuristic>
<pooledEntityPlacer>
<changeMoveSelector>
<entitySelector id="placerEntitySelector">
<cacheType>PHASE</cacheType>
<selectionOrder>SORTED</selectionOrder>
<sorterManner>DECREASING_DIFFICULTY</sorterManner>
</entitySelector>
<valueSelector>
<cacheType>PHASE</cacheType>
<selectionOrder>SORTED</selectionOrder>
<sorterManner>INCREASING_STRENGTH</sorterManner>
</valueSelector>
</changeMoveSelector>
</pooledEntityPlacer>
</constructionHeuristic>
Per step, the PooledEntityPlacer
applies the winning Move
(out of all
the moves for that entity generated by the MoveSelector
).
To customize the entity or value sorting, see sorted selection. Other
Selector
customization (such as filtering and limiting) is supported too.
Because the move B0 to B3 has the highest score (-3
), it is picked
as the next step. If multiple moves have the same highest score, one is picked randomly, in this case B0
to B3. Note that C0 to C3 (not shown) could also have been picked because it also
has the score -3
.
The step is applied on the solution. From that new solution, Local Search tries every move again, to decide the next step after that. It continually does this in a loop, and we get something like this:
Notice that Local Search 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 selected 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 shown above, Local Search solves the 4 queens problem by starting with the starting solution and make the following steps sequentially:
B0 to B3
D0 to B2
A0 to B1
Turn on debug
logging for the category org.optaplanner
to show those
steps in the log:
INFO Solving started: time spent (0), best score (-6), random (JDK with seed 0).
DEBUG LS step (0), time spent (20), score (-3), new best score (-3), accepted/selected move count (12/12), picked move (Queen-1 {Row-0 -> Row-3}).
DEBUG LS step (1), time spent (31), score (-1), new best score (-1), accepted/selected move count (12/12), picked move (Queen-3 {Row-0 -> Row-2}).
DEBUG LS step (2), time spent (40), score (0), new best score (0), accepted/selected move count (12/12), picked move (Queen-0 {Row-0 -> Row-1}).
INFO Local Search phase (0) ended: step total (3), time spent (41), best score (0).
INFO Solving ended: time spent (41), best score (0), average calculate count per second (1780).
Notice that a log message includes the toString()
method of the Move
implementation which returns for example "Queen-1 {Row-0 -> Row-3}
".
A naive Local Search configuration 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. By using a Construction Heuristics phase first, it's even a lot more efficient.
Local Search 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 move and neighborhood selection.
An Acceptor
which filters out unacceptable moves.
A Forager
which gathers accepted moves and picks the next step from them.
The solver phase configuration looks like this:
<localSearch>
<unionMoveSelector>
...
</unionMoveSelector>
<acceptor>
...
</acceptor>
<forager>
...
</forager>
</localSearch>
In the example below, the MoveSelector
generated the moves shown with the blue lines, the
Acceptor
accepted all of them and the Forager
picked the move B0
to B3.
Turn on trace
logging to show the decision making in the
log:
INFO Solver started: time spent (0), score (-6), new best score (-6), random (JDK with seed 0).
TRACE Move index (0) not doable, ignoring move (Queen-0 {Row-0 -> Row-0}).
TRACE Move index (1), score (-4), accepted (true), move (Queen-0 {Row-0 -> Row-1}).
TRACE Move index (2), score (-4), accepted (true), move (Queen-0 {Row-0 -> Row-2}).
TRACE Move index (3), score (-4), accepted (true), move (Queen-0 {Row-0 -> Row-3}).
...
TRACE Move index (6), score (-3), accepted (true), move (Queen-1 {Row-0 -> Row-3}).
...
TRACE Move index (9), score (-3), accepted (true), move (Queen-2 {Row-0 -> Row-3}).
...
TRACE Move index (12), score (-4), accepted (true), move (Queen-3 {Row-0 -> Row-3}).
DEBUG LS step (0), time spent (6), score (-3), new best score (-3), accepted/selected move count (12/12), picked move (Queen-1 {Row-0 -> Row-3}).
...
Because the last solution can degrade (for example in Tabu Search), 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.
If an earlier move is better than a later move with the same score, the score calculator should add an extra softer score level to score the first move as slightly better. Don't rely on move selection order to enforce that.
Random tie breaking does not affect reproducibility.
Unlike the n queens problem, real world problems require the use of acceptedCountLimit
.
Start from an acceptedCountLimit
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.
With a low acceptedCountLimit
(so a fast stepping algorithm), 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 elements are actually selected.
<localSearch>
<localSearchType>TABU_SEARCH</localSearchType>
</localSearch>
<localSearch>
...
<acceptor>
<entityTabuSize>7</entityTabuSize>
</acceptor>
<forager>
<acceptedCountLimit>1000</acceptedCountLimit>
</forager>
</localSearch>
Planner implements several tabu types:
<acceptor>
<entityTabuSize>7</entityTabuSize>
</acceptor>
<acceptor>
<entityTabuRatio>0.02</entityTabuRatio>
</acceptor>
<acceptor>
<valueTabuSize>7</valueTabuSize>
</acceptor>
<acceptor>
<valueTabuRatio>0.02</valueTabuRatio>
</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>
<acceptor>
<solutionTabuSize>1000</solutionTabuSize>
</acceptor>
For non-trivial cases, solution tabu is usually useless because the search space size makes it statistically highly unlikely to reach the same solution twice. Therefore its use is not recommended, except for small datasets.
Sometimes it's useful to combine tabu types:
<acceptor>
<entityTabuSize>7</entityTabuSize>
<valueTabuSize>3</valueTabuSize>
</acceptor>
If the tabu size is too small, the solver can still get stuck in a local optimum. On the other hand, if the tabu size is too large, the solver can be inefficient by bouncing of the walls. Use the Benchmarker to fine tweak your configuration.
Start with a simulatedAnnealingStartingTemperature
set to the maximum score delta a
single move can cause. Use the Benchmarker to tweak the value. Advanced
configuration:
<localSearch>
...
<acceptor>
<simulatedAnnealingStartingTemperature>2hard/100soft</simulatedAnnealingStartingTemperature>
</acceptor>
<forager>
<acceptedCountLimit>1</acceptedCountLimit>
</forager>
</localSearch>
Simulated Annealing should use a low acceptedCountLimit
. The classic algorithm uses an
acceptedCountLimit
of 1
, but often 4
performs
better.
Simulated Annealing can be combined with a tabu acceptor at the same time. That gives Simulated Annealing salted with a bit of Tabu. Use a lower tabu size than in a pure Tabu Search configuration.
<localSearch>
...
<acceptor>
<simulatedAnnealingStartingTemperature>2hard/100soft</simulatedAnnealingStartingTemperature>
<entityTabuSize>5</entityTabuSize>
</acceptor>
<forager>
<acceptedCountLimit>1</acceptedCountLimit>
</forager>
</localSearch>
<localSearch>
<localSearchType>LATE_ACCEPTANCE</localSearchType>
</localSearch>
<localSearch>
...
<acceptor>
<lateAcceptanceSize>400</lateAcceptanceSize>
</acceptor>
<forager>
<acceptedCountLimit>1</acceptedCountLimit>
</forager>
</localSearch>
Late Acceptance should use a low acceptedCountLimit
.
<localSearch>
...
<acceptor>
<lateAcceptanceSize>400</lateAcceptanceSize>
<entityTabuSize>5</entityTabuSize>
</acceptor>
<forager>
<acceptedCountLimit>1</acceptedCountLimit>
</forager>
</localSearch>
Scientific paper: An initial study of a novel Step Counting Hill Climbing heuristic applied to timetabling problems by Yuri Bykov, Sanja Petrovic (2013)
<localSearch>
...
<acceptor>
<stepCountingHillClimbingSize>400</stepCountingHillClimbingSize>
</acceptor>
<forager>
<acceptedCountLimit>1</acceptedCountLimit>
</forager>
</localSearch>
Step Counting Hill Climbing should use a low acceptedCountLimit
.
Step Counting Hill Climbing can be combined with a tabu acceptor at the same time, similar as shown in the Late Acceptance section.
Strategic Oscillation is an add-on, which works especially well with Tabu Search. Instead of picking the accepted move with the highest score, it employs a different mechanism: If there's an improving move, it picks it. If there's no improving move however, it prefers moves which improve a softer score level, over moves which break a harder score level less.
Configure a finalistPodiumType
, for example in a Tabu Search configuration:
<localSearch>
...
<acceptor>
<entityTabuSize>7</entityTabuSize>
</acceptor>
<forager>
<acceptedCountLimit>1000</acceptedCountLimit>
<finalistPodiumType>STRATEGIC_OSCILLATION</finalistPodiumType>
</forager>
</localSearch>
This algorithm has not been implemented yet.
A good Genetic Algorithms prototype in Planner was written some time ago, but it wasn't practical to merge and support it at the time. The results of Genetic Algorithms were consistently and seriously inferior to all the Local Search variants (except Hill Climbing) on all use cases tried. Nevertheless, a future version of Planner will add support for Genetic Algorithms, so you can easily benchmark Genetic Algorithms on your use case too.
A hyperheuristic automates the decision which heuristic(s) to use on a specific data set.
A future version of Planner will have native support for hyperheuristics. Meanwhile, it's pretty easy to implement it yourself: Based on the size or difficulty of a data set (which is a criterion), use a different Solver configuration (or adjust the default configuration using the Solver configuration API). The Benchmarker can help to identify such criteria.
Solver
Configurationoptaplanner-benchmark
PlannerBenchmark
The benchmarker is in a separate artifact called optaplanner-benchmark
.
If you use Maven, add a dependency in your pom.xml
file:
<dependency>
<groupId>org.optaplanner</groupId>
<artifactId>optaplanner-benchmark</artifactId>
</dependency>
PlannerBenchmarkFactory plannerBenchmarkFactory = PlannerBenchmarkFactory.createFromXmlResource(
"org/optaplanner/examples/nqueens/benchmark/nqueensBenchmarkConfig.xml");
PlannerBenchmark plannerBenchmark = benchmarkFactory.buildPlannerBenchmark();
plannerBenchmark.benchmark();
A benchmark configuration file looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<plannerBenchmark>
<benchmarkDirectory>local/data/nqueens</benchmarkDirectory>
<inheritedSolverBenchmark>
<problemBenchmarks>
...
<inputSolutionFile>data/nqueens/unsolved/32queens.xml</inputSolutionFile>
<inputSolutionFile>data/nqueens/unsolved/64queens.xml</inputSolutionFile>
</problemBenchmarks>
<solver>
...<!-- Common solver configuration -->
</solver>
</inheritedSolverBenchmark>
<solverBenchmark>
<name>Tabu Search</name>
<solver>
...<!-- Tabu Search specific solver configuration -->
</solver>
</solverBenchmark>
<solverBenchmark>
<name>Simulated Annealing</name>
<solver>
...<!-- Simulated Annealing specific solver configuration -->
</solver>
</solverBenchmark>
<solverBenchmark>
<name>Late Acceptance</name>
<solver>
...<!-- Late Acceptance specific solver configuration -->
</solver>
</solverBenchmark>
</plannerBenchmark>
Every <solverBenchmark>
element contains a solver configuration and one or more
<inputSolutionFile>
elements. It will run the solver configuration on each of those
unsolved solution files. The element name
is optional, because it is generated if absent. The
inputSolutionFile
is read by a SolutionFileIO (relative
to the working directory).
Use a forward slash (/
) as the file separator (for example in the element
<inputSolutionFile>
). That will work on any platform (including Windows).
Do not use backslash (\
) as the file separator: that breaks portability because it does
not work on Linux and Mac.
To lower verbosity, the common parts of multiple <solverBenchmark>
elements are
extracted to the <inheritedSolverBenchmark>
element. Every property can still be
overwritten per <solverBenchmark>
element. Note that inherited solver phases such as
<constructionHeuristic>
or <localSearch>
are not overwritten but
instead are added to the tail of the solver phases list.
The benchmark report will be written in the directory specified the
<benchmarkDirectory>
element (relative to the working directory).
It's recommended that the benchmarkDirectory
is a directory ignored for source control
and not cleaned by your build system. This way the generated files are not bloating your source control and they
aren't lost when doing a build. Usually that directory is called local
.
If an Exception
or Error
occurs in a single benchmark, the entire
Benchmarker will not fail-fast (unlike everything else in Planner). Instead, the Benchmarker will continue to run
all other benchmarks, write the benchmark report and then fail (if there is at least 1 failing single benchmark).
The failing benchmarks will be clearly marked as such in the benchmark report.
By default, a benchmarker uses a XStreamSolutionFileIO
instance to read and write
solutions.
<problemBenchmarks>
<xStreamAnnotatedClass>org.optaplanner.examples.nqueens.domain.NQueens</xStreamAnnotatedClass>
<inputSolutionFile>data/nqueens/unsolved/32queens.xml</inputSolutionFile>
...
</problemBenchmarks>
For each dataset, create a txt file that holds the unique id of the dataset. Write a custom SolutionFileIO
that reads that identifier,
connects to the database and extract the problem identified by that id. Configure those txt files as
<inputSolutionFile>
elements.
Local files are always faster and don't require a network connection.
<plannerBenchmark>
...
<warmUpSecondsSpentLimit>30</warmUpSecondsSpentLimit>
...
</plannerBenchmark>
To write those solutions in the benchmarkDirectory
, enable
writeOutputSolutionEnabled
:
<problemBenchmarks>
...
<writeOutputSolutionEnabled>true</writeOutputSolutionEnabled>
...
</problemBenchmarks>
Benchmark logging is configured like the Solver
logging.
To separate the log messages of each single benchmark run into a separate file, use the MDC with key singleBenchmark.name
in
a sifting appender. For example with Logback in logback.xml
:
<appender name="fileAppender" class="ch.qos.logback.classic.sift.SiftingAppender">
<discriminator>
<key>singleBenchmark.name</key>
<defaultValue>app</defaultValue>
</discriminator>
<sift>
<appender name="fileAppender.${singleBenchmark.name}" class="...FileAppender">
<file>local/log/optaplannerBenchmark-${singleBenchmark.name}.log</file>
...
</appender>
</sift>
</appender>
The HTML report will use your default locale to format numbers. If you share the benchmark report with
people from another country, consider overwriting the locale
accordingly:
<plannerBenchmark>
...
<benchmarkReport>
<locale>en_US</locale>
</benchmarkReport>
...
</plannerBenchmark>
However, there are different ways of ranking the solvers. Configure it like this:
<plannerBenchmark>
...
<benchmarkReport>
<solverRankingType>TOTAL_SCORE</solverRankingType>
</benchmarkReport>
...
</plannerBenchmark>
The following solverRankingType
s are supported:
All of the supported rankings do not rank solvers which have at least one failed single benchmark.
<benchmarkReport>
<solverRankingComparatorClass>...TotalScoreSolverRankingComparator</solverRankingComparatorClass>
</benchmarkReport>
Or by implementing a weight factory:
<benchmarkReport>
<solverRankingWeightFactoryClass>...TotalRankSolverRankingWeightFactory</solverRankingWeightFactoryClass>
</benchmarkReport>
Shows the best score per inputSolutionFile
for each solver configuration.
Useful for visualizing the best solver configuration.
Shows the best score per problem scale for each solver configuration.
Useful for visualizing the scalability of each solver configuration.
<plannerBenchmark>
<benchmarkDirectory>local/data/nqueens/solved</benchmarkDirectory>
<inheritedSolverBenchmark>
<problemBenchmarks>
...
<problemStatisticType>BEST_SCORE</problemStatisticType>
<problemStatisticType>CALCULATE_COUNT_PER_SECOND</problemStatisticType>
</problemBenchmarks>
...
</inheritedSolverBenchmark>
...
</plannerBenchmark>
To see how the best score evolves over time, add:
<problemBenchmarks>
...
<problemStatisticType>BEST_SCORE</problemStatisticType>
</problemBenchmarks>
The best score over time statistic is very useful to detect abnormalities, such as a potential score trap.
A time gradient based algorithm (such as Simulated Annealing) will have a different statistic if it's run with a different time limit configuration. That's because this Simulated Annealing implementation automatically determines its velocity based on the amount of time that can be spent. On the other hand, for the Tabu Search and Late Annealing, what you see is what you'd get.
To see how the step score evolves over time, add:
<problemBenchmarks>
...
<problemStatisticType>STEP_SCORE</problemStatisticType>
</problemBenchmarks>
Compare the step score statistic with the best score statistic (especially on parts for which the best score flatlines). If it hits a local optima, the solver should take deteriorating steps to escape it. But it shouldn't deteriorate too much either.
The step score statistic has been seen to slow down the solver noticeably due to GC stress, especially for fast stepping algorithms (such as Simulated Annealing and Late Acceptance).
To see how fast the scores are calculated, add:
<problemBenchmarks>
...
<problemStatisticType>CALCULATE_COUNT_PER_SECOND</problemStatisticType>
</problemBenchmarks>
The initial high calculate count is typical during solution initialization: it's far easier to calculate the score of a solution if only a handful planning entities have been initialized, than when all the planning entities are initialized.
After those few seconds of initialization, the calculate count is relatively stable, apart from an occasional stop-the-world garbage collector disruption.
<problemBenchmarks>
...
<problemStatisticType>BEST_SOLUTION_MUTATION</problemStatisticType>
</problemBenchmarks>
Use Tabu Search - an algorithm that behaves like a human - to get an estimation on how difficult it would be for a human to improve the previous best solution to that new best solution.
To see how the selected and accepted move count per step evolves over time, add:
<problemBenchmarks>
...
<problemStatisticType>MOVE_COUNT_PER_STEP</problemStatisticType>
</problemBenchmarks>
This statistic has been seen to slow down the solver noticeably due to GC stress, especially for fast stepping algorithms (such as Simulated Annealing and Late Acceptance).
<plannerBenchmark>
<benchmarkDirectory>local/data/nqueens/solved</benchmarkDirectory>
<inheritedSolverBenchmark>
<problemBenchmarks>
...
<problemStatisticType>...</problemStatisticType>
<singleStatisticType>PICKED_MOVE_TYPE_BEST_SCORE_DIFF</singleStatisticType>
</problemBenchmarks>
...
</inheritedSolverBenchmark>
...
</plannerBenchmark>
To see which constraints are matched in the best score (and how much) over time, add:
<problemBenchmarks>
...
<singleStatisticType>CONSTRAINT_MATCH_TOTAL_BEST_SCORE</singleStatisticType>
</problemBenchmarks>
Requires the score calculation to support constraint matches. Drools score calculation supports constraint matches automatically, but incremental Java score calculation requires requires more work.
The constraint match total statistics has been seen to affect the solver noticeably.
To see which constraints are matched in the step score (and how much) over time, add:
<problemBenchmarks>
...
<singleStatisticType>CONSTRAINT_MATCH_TOTAL_STEP_SCORE</singleStatisticType>
</problemBenchmarks>
Requires the score calculation to support constraint matches. Drools score calculation supports constraint matches automatically, but incremental Java score calculation requires requires more work.
The constraint match total statistics has been seen to affect the solver noticeably.
To see which move types improve the best score (and how much) over time, add:
<problemBenchmarks>
...
<singleStatisticType>PICKED_MOVE_TYPE_BEST_SCORE_DIFF</singleStatisticType>
</problemBenchmarks>
<plannerBenchmark>
...
<parallelBenchmarkCount>AUTO</parallelBenchmarkCount>
...
</plannerBenchmark>
The following parallelBenchmarkCount
s are supported:
To reduce the verbosity of such a benchmark configuration, you can use a Freemarker template for the benchmark configuration instead:
<plannerBenchmark>
...
<inheritedSolverBenchmark>
...
</inheritedSolverBenchmark>
<#list [5, 7, 11, 13] as entityTabuSize>
<#list [500, 1000, 2000] as acceptedCountLimit>
<solverBenchmark>
<name>entityTabuSize ${entityTabuSize} acceptedCountLimit ${acceptedCountLimit}</name>
<solver>
<localSearch>
<unionMoveSelector>
<changeMoveSelector/>
<swapMoveSelector/>
</unionMoveSelector>
<acceptor>
<entityTabuSize>${entityTabuSize}</entityTabuSize>
</acceptor>
<forager>
<acceptedCountLimit>${acceptedCountLimit}</acceptedCountLimit>
</forager>
</localSearch>
</solver>
</solverBenchmark>
</#list>
</#list>
</plannerBenchmark>
And build it with the class PlannerBenchmarkFactory
:
PlannerBenchmarkFactory plannerBenchmarkFactory = PlannerBenchmarkFactory.createFromFreemarkerXmlResource(
"org/optaplanner/examples/cloudbalancing/benchmark/cloudBalancingBenchmarkConfigTemplate.xml.ftl");
PlannerBenchmark plannerBenchmark = benchmarkFactory.buildPlannerBenchmark();
To use it, provide a PlannerBenchmarkFactory
to the
BenchmarkAggregatorFrame
to display the GUI:
public static void main(String[] args) {
PlannerBenchmarkFactory plannerBenchmarkFactory = PlannerBenchmarkFactory.createFromXmlResource(
"org/optaplanner/examples/nqueens/benchmark/nqueensBenchmarkConfig.xml");
BenchmarkAggregatorFrame.createAndDisplay(plannerBenchmarkFactory);
}
In the GUI, select the interesting benchmarks and click the button to generate the report.
Add a additional score level (usually a medium level between the hard and soft level) by switching ScoreDefinition.
Make the planning variable nullable.
Add a score constraint on the new level (so usually a medium constraint) to penalize the number of unassigned entities (or a weighted sum of them).
public class MovableShiftAssignmentSelectionFilter implements SelectionFilter<ShiftAssignment> {
public boolean accept(ScoreDirector scoreDirector, ShiftAssignment shiftAssignment) {
ShiftDate shiftDate = shiftAssignment.getShift().getShiftDate();
NurseRoster nurseRoster = (NurseRoster) scoreDirector.getWorkingSolution();
return nurseRoster.getNurseRosterInfo().isInPlanningWindow(shiftDate);
}
}
@PlanningEntity(movableEntitySelectionFilter = MovableShiftAssignmentSelectionFilter.class)
public class ShiftAssignment {
...
}
@PlanningEntity(...)
public class ProcessAssignment {
private MrProcess process;
private Machine originalMachine;
private Machine machine;
public Machine getOriginalMachine() {...}
@PlanningVariable(...)
public Machine getMachine() {...}
public boolean isMoved() {
return originalMachine != null && originalMachine != machine;
}
...
}
rule "processMoved"
when
ProcessAssignment(moved == true)
then
scoreHolder.addSoftConstraintMatch(kcontext, -1000);
end
To do real-time planning, first combine backup planning and continuous planning with short planning windows to lower the burden of real-time planning. As time passes, the problem itself changes:
In the example above, 3 customers are added at different times (07:56
,
08:02
and 08:45
), after the original customer set finished solving at
07:55
and in some cases after the vehicles already left. Planner can handle such scenario's with
ProblemFactChange
(in combination with immovable
planning entities).
public interface Solver {
...
boolean addProblemFactChange(ProblemFactChange problemFactChange);
boolean isEveryProblemFactChangeProcessed();
...
}
public interface ProblemFactChange {
void doChange(ScoreDirector scoreDirector);
}
public void deleteComputer(final CloudComputer computer) {
solver.addProblemFactChange(new ProblemFactChange() {
public void doChange(ScoreDirector scoreDirector) {
CloudBalance cloudBalance = (CloudBalance) scoreDirector.getWorkingSolution();
// First remove the problem fact from all planning entities that use it
for (CloudProcess process : cloudBalance.getProcessList()) {
if (ObjectUtils.equals(process.getComputer(), computer)) {
scoreDirector.beforeVariableChanged(process, "computer");
process.setComputer(null);
scoreDirector.afterVariableChanged(process, "computer");
}
}
// A SolutionCloner does not clone problem fact lists (such as computerList)
// Shallow clone the computerList so only workingSolution is affected, not bestSolution or guiSolution
cloudBalance.setComputerList(new ArrayList<CloudComputer>(cloudBalance.getComputerList()));
// Next remove it the problem fact itself
for (Iterator<CloudComputer> it = cloudBalance.getComputerList().iterator(); it.hasNext(); ) {
CloudComputer workingComputer = it.next();
if (ObjectUtils.equals(workingComputer, computer)) {
scoreDirector.beforeProblemFactRemoved(workingComputer);
it.remove(); // remove from list
scoreDirector.beforeProblemFactRemoved(workingComputer);
break;
}
}
}
});
}
To write a ProblemFactChange
correctly, it's important to understand the behaviour of
a planning clone:
Any change in a ProblemFactChange
must be done on the Solution
instance of scoreDirector.getWorkingSolution()
. The workingSolution
is
a planning clone of the
BestSolutionChangedEvent
's bestSolution
. So the
workingSolution
in the Solver
is never the same instance as the
Solution
in the rest of your application.
A planning clone also clones the planning entities and planning entity collections. So any change on
the planning entities must happen on the instances hold by
scoreDirector.getWorkingSolution()
.
A planning clone does not clone the problem facts, nor the problem fact collections.
Therefore the workingSolution
and the bestSolution
share the
same problem fact instances and the same problem fact list instances.
Any problem fact or problem fact list changed by a ProblemFactChange
must be
problem cloned first (which can imply rerouting references in other problem facts and planning entities).
Otherwise, if the workingSolution
and bestSolution
are used in
different threads (for example a solver thread and a GUI event thread), a race condition can occur.
Many types of changes can leave a planning entity uninitialized, resulting in a partially initialized solution. That's fine, as long as the first solver phase can handle it. All construction heuristics solver phases can handle that, so it's recommended to configure such a solver phase as the first phase.
In essence, the Solver
stops, runs the ProblemFactChange
and restarts. This is a warm start because its initial solution is the
adjusted best solution of the previous run. Each solver phase runs again. This implies the construction heuristic
runs again, but because little or no planning variables are uninitialized (unless you have a nullable planning variable), it finishes much quicker than in a cold
start.
Each configured Termination
resets (both in solver and phase configuration), but a
previous call to terminateEarly()
is not undone. Normally however, you won't configure any
Termination
(except in daemon mode), just call Solver.terminateEarly()
when
the results are needed. Alternatively, do configure a Termination
and use the daemon mode in
combination with BestSolutionChangedEvent
as
described below.
<solver>
<daemon>true</daemon>
...
</solver>
Subscribe to the BestSolutionChangedEvent
to
process new best solutions found by the solver thread. A BestSolutionChangedEvent
doesn't
guarantee that every ProblemFactChange
has been processed already, nor that the solution is
initialized and feasible. To ignore BestSolutionChangedEvent
s with such invalid solutions, do
this:
public void bestSolutionChanged(BestSolutionChangedEvent<CloudBalance> event) {
// Ignore invalid solutions
if (event.isEveryProblemFactChangeProcessed()
&& event.isNewBestSolutionInitialized()
&& event.getNewBestSolution().getScore().isFeasible()) {
...
}
}
Camel is an enterprise integration framework which includes support for Planner (starting from Camel 2.13). It can expose a use case as a REST service, a SOAP service, a JMS service, ...
Read the documentation for the camel-optaplanner component. That component works in Karaf too.
Create the file module.xml
in that new directory. Give it this content:
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.3" name="org.optaplanner">
<resources>
...
<resource-root path="kie-api-${version}.jar"/>
...
<resource-root path="optaplanner-core-${version}.jar"/>
...
<resource-root path="."/>
</resources>
<dependencies>
<module name="javaee.api"/>
</dependencies>
</module>
Navigate to the deployed war
file.
Create the file jboss-deployment-structure.xml
in the
WEB-INF/lib
directory. Give it this content:
<?xml version="1.0" encoding="UTF-8" ?>
<jboss-deployment-structure>
<deployment>
<dependencies>
<module name="org.optaplanner" export="true"/>
</dependencies>
</deployment>
</jboss-deployment-structure>
Android is not a complete JVM (because some JDK libraries are missing), but Planner works on Android with easy Java or incremental Java score calculation. The Drools rule engine does not work on Android yet, so Drools score calculation doesn't work on Android and its dependencies need to be excluded.
Workaround to use Planner on Android:
Add a dependency to the build.gradle
file in your Android project to exclude
org.drools
and xmlpull
dependencies:
dependencies {
...
compile('org.optaplanner:optaplanner-core:...') {
exclude group: 'xmlpull'
exclude group: 'org.drools'
}
...
}
But despite that, both can benefit if the human planner acts as supervisor to Planner:
The human planner defines and validates the score function.
The human planner is always in control of Planner.
As shown in the course scheduling example, the human planner can lock 1 or more planning variables to a specific planning value and make those immovable. Because they are immovable, Planner does not change them: it optimizes the planning around the enforcements made by the human. If the human planner locks all planning variables, he/she sidelines Planner completely.
In a prototype implementation, the human planner might use this occasionally. But as the implementation matures, it must become obsolete. But do keep the feature alive: as a reassurance for the humans. Or in case that one day the business changes dramatically before the score constraints can be adjusted.
Therefore, it's often a good idea to involve the human planner in your project.
The diagram below explains the overall structure of the OptaPlanner source code:
Fail fast. There are several levels of fail fast, from better to worse:
Exception
messages
The Exception
message must include the name and state of each relevant variable.
For example:
if (fooSize < 0) {
throw new IllegalArgumentException("The fooSize (" + fooSize + ") of bar (" + this + ") must be positive.");
}
Notice that the output clearly explains what's wrong:
Exception in thread "main" java.lang.IllegalArgumentException: The fooSize (-5) must be positive.
at ...
Whenever possible, the Exception
message must include context.
Whenever the fix is not obvious, the Exception
message must include advice.
Exception in thread "main" java.lang.IllegalArgumentException: UndoMove corruption: ...
1) Check your custom createUndoMove() of ...
2) Check your custom Variable listeners if ...
at ...