JBoss.orgCommunity Documentation
Solving a planning problem with Drools Planner consists out of 5 steps:
Model your planning problem as a class that implements the interface
Solution
, for example the class NQueens
.
Configure a Solver
, for example a first fit and tabu
search solver for any NQueens
instance.
Load a problem data set from your data layer, for example a 4 Queens
instance. Set it as the planning problem on the Solver
with
Solver.setPlanningProblem(...)
.
Solve it with Solver.solve()
.
Get the best solution found by the Solver
with
Solver.getBestSolution()
.
You can build a Solver
instance with the XmlSolverConfigurer
.
Configure it with a solver configuration XML file:
XmlSolverConfigurer configurer = new XmlSolverConfigurer();
configurer.configure("/org/drools/planner/examples/nqueens/solver/nqueensSolverConfig.xml");
Solver solver = configurer.buildSolver();
A solver configuration file looks something like this:
<?xml version="1.0" encoding="UTF-8"?>
<solver>
<!-- Define the model -->
<solutionClass>org.drools.planner.examples.nqueens.domain.NQueens</solutionClass>
<planningEntityClass>org.drools.planner.examples.nqueens.domain.Queen</planningEntityClass>
<!-- Define the score function -->
<scoreDrl>/org/drools/planner/examples/nqueens/solver/nQueensScoreRules.drl</scoreDrl>
<scoreDefinition>
<scoreDefinitionType>SIMPLE</scoreDefinitionType>
</scoreDefinition>
<!-- Configure the optimization algorithm(s) -->
<termination>
...
</termination>
<constructionHeuristic>
...
</constructionHeuristic>
<localSearch>
...
</localSearch>
</solver>
Notice the 3 parts in it:
Define the model
Define the score function
Configure the optimization algorithm(s)
We 'll explain these various parts of a configuration later in this manual.
Drools Planner makes it relatively easy to switch optimization algorithm(s) just by
changing the configuration. There's even a Benchmark
utility which allows you to
play out different configurations against each other and report the most appropriate configuration for your
problem. You could for example play out tabu search versus simulated annealing, on 4 queens and 64 queens.
As an alternative to the XML file, a solver configuration can also be configured with the
SolverConfig
API:
SolverConfig solverConfig = new SolverConfig(); solverConfig.setSolutionClass(NQueens.class); Set<Class<?>> planningEntityClassSet = new HashSet<Class<?>>(); planningEntityClassSet.add(Queen.class); solverConfig.setPlanningEntityClassSet(planningEntityClassSet); solverConfig.setScoreDrlList( Arrays.asList("/org/drools/planner/examples/nqueens/solver/nQueensScoreRules.drl")); ScoreDefinitionConfig scoreDefinitionConfig = solverConfig.getScoreDefinitionConfig(); scoreDefinitionConfig.setScoreDefinitionType( ScoreDefinitionConfig.ScoreDefinitionType.SIMPLE); TerminationConfig terminationConfig = solverConfig.getTerminationConfig(); // ... List<SolverPhaseConfig> solverPhaseConfigList = new ArrayList<SolverPhaseConfig>(); ConstructionHeuristicSolverPhaseConfig constructionHeuristicSolverPhaseConfig = new ConstructionHeuristicSolverPhaseConfig(); // ... solverPhaseConfigList.add(constructionHeuristicSolverPhaseConfig); LocalSearchSolverPhaseConfig localSearchSolverPhaseConfig = new LocalSearchSolverPhaseConfig(); // ... solverPhaseConfigList.add(localSearchSolverPhaseConfig); solverConfig.setSolverPhaseConfigList(solverPhaseConfigList); Solver solver = solverConfig.buildSolver();
It is highly recommended to configure by XML file instead of this API. To
dynamically configure a value at runtime, use the XML file as a template and extract the
SolverConfig
class with getSolverConfig()
to configure the dynamic value at
runtime:
XmlSolverConfigurer configurer = new XmlSolverConfigurer(); configurer.configure("/org/drools/planner/examples/nqueens/solver/nqueensSolverConfig.xml"); SolverConfig solverConfig = configurer.getSolverConfig(); solverConfig.getTerminationConfig().setMaximumMinutesSpend(userInput); Solver solver = solverConfig.buildSolver();
Look at a dataset of your planning problem. You 'll recognize domain classes in there, each of which is one of these:
A unrelated class: not used by any of the score constraints. From a planning standpoint, this data is obsolete.
A problem fact class: used by the score constraints, but does NOT
change during planning (as long as the problem stays the same). For example: Bed
,
Room
, Shift
, Employee
, Topic
,
Period
, ...
A planning entity class: used by the score constraints and changes
during planning. For example: BedDesignation
, ShiftAssignment
,
Exam
, ...
Ask yourself: What class changes during planning? Which class has variables
that I want the Solver
to choose for me? That class is a planning entity. Most use
cases have only 1 planning entity class.
In real-time planning, problem facts can change during planning, because the problem itself changes. However, that doesn't make them planning entities.
In Drools Planner all problems facts and planning entities are plain old JavaBeans (POJO's). You can load them from a database (JDBC/JPA/JDO), an XML file, a data repository, a noSQL cloud, ...: Drools Planner doesn't care.
A problem fact is any JavaBean (POJO) with getters that does not change during planning. Implementing the
interface Serializable
is recommended (but not required). For example in n queens, the columns
and rows are problem facts:
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 }
A problem fact class does not require any Planner specific code. For example, you can reuse your domain classes, which might have JPA annotations.
Generally, better designed domain classes lead to simpler and more efficient score constraints. Therefore,
when dealing with a messy legacy system, it can sometimes be worth it to convert the messy domain set into a
planner specific POJO set first. For example: if your domain model has 2 Teacher
instances
for the same teacher that teaches at 2 different departments, it's hard to write a correct score constraint that
is constrains a teacher's spare time.
Alternatively, you can sometimes also introduce a cached problem fact to enrich the domain model during planning without actually changing it.
A planning entity is a JavaBean (POJO) that changes during solving, for example a Queen
that changes to another row. A planning problem has multiple planning entities, for example for a single n
queens problem, each Queen
is a planning entity. But there's usually only 1 planning entity
class, for example the Queen
class.
A planning entity class needs to be annotated with the @PlanningEntity
annotation.
Each planning entity class has 1 or more planning variables. It usually also has 1 or
more defining properties. For example in n queens, a Queen
is defined by
it's Column
and has a planning variable Row
. This means that a Queen's
column never changes during solving, while it's row does change.
@PlanningEntity public class Queen { private Column column; // Planning variables: changes during planning, between score calculations. private Row row; // ... getters and setters }
A planning entity class can have multiple planning variables. For example, a Lecture
is
defined by it's Course
and it's index in that course (because 1 course has multiple
lectures). Each Lecture
needs to be scheduled into a Period
and a
Room
so it has 2 planning variables (period and room). For example: the course Mathematics
has 8 lectures per week, of which the first lecture is Monday morning at 08:00 in room 212.
@PlanningEntity public class Lecture { private Course course; private int lectureIndexInCourse; // Planning variables: changes during planning, between score calculations. private Period period; private Room room; // ... }
The solver configuration also needs to be made aware of each planning entity class:
<solver> ... <planningEntityClass>org.drools.planner.examples.nqueens.domain.Queen</planningEntityClass> ... </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 it's 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 historic data up to, but not including, the planning window (so it doesn't change when a planning entity changes) and let the score constraints take it into account.
Some optimization algorithms work more efficiently if they have an estimation of which planning entities are more difficult to plan. For example: in bin packing bigger items are harder to fit, in course scheduling lectures with more students are more difficult to schedule and in n queens the middle queens are more difficult.
Therefore, you can set a difficultyComparatorClass
to the
@PlanningEntity
annotation:
@PlanningEntity(difficultyComparatorClass = CloudAssignmentDifficultyComparator.class) public class CloudAssignment { // ... }
public class CloudAssignmentDifficultyComparator implements Comparator<Object> { public int compare(Object a, Object b) { return compare((CloudAssignment) a, (CloudAssignment) b); } public int compare(CloudAssignment a, CloudAssignment b) { return new CompareToBuilder() .append(a.getCloudProcess().getMinimalMultiplicand(), b.getCloudProcess().getMinimalMultiplicand()) .append(a.getCloudProcess().getId(), b.getCloudProcess().getId()) .toComparison(); } }
Alternatively, you can also set a difficultyWeightFactoryClass
to the
@PlanningEntity
annotation, so you have access to the rest of the problem facts from the
solution too:
@PlanningEntity(difficultyWeightFactoryClass = QueenDifficultyWeightFactory.class) public class Queen { // ... }
public interface PlanningEntityDifficultyWeightFactory { Comparable createDifficultyWeight(Solution solution, Object planningEntity); }
public class QueenDifficultyWeightFactory implements PlanningEntityDifficultyWeightFactory { public Comparable createDifficultyWeight(Solution solution, Object planningEntity) { NQueens nQueens = (NQueens) solution; Queen queen = (Queen) planningEntity; 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 .append(queen.getColumnIndex(), other.queen.getColumnIndex()) .toComparison(); } } }
None of the current planning variable state may be used to compare planning entities.
They are likely to be null
anyway. For example, a Queen
's
row
variable may not be used.
A planning variable is a property (including getter and setter) on a planning entity. It changes during
planning. For example, a Queen
's row
property is a planning variable. Note
that even though a Queen
's row
property changes to another
Row
during planning, no Row
instance itself is changed. A planning
variable points to a planning value.
A planning variable getter needs to be annotated with the @PlanningVariable
annotation.
Furthermore, it needs a @ValueRange*
annotation too.
@PlanningEntity public class Queen { private Row row; // ... @PlanningVariable @ValueRangeFromSolutionProperty(propertyName = "rowList") public Row getRow() { return row; } public void setRow(Row row) { this.row = row; } }
A planning value is a possible value for a planning variable. Usually, a planning value is problem fact, but it can also be any object, for example a double. Sometimes it can even be another planning entity.
A planning value range is the set of possible planning values for a planning variable. This set can be a
discrete (for example row 1
, 2
, 3
or
4
) or continuous (for example any double
between 0.0
and 1.0
). There are several ways to define the value range of a planning variable, each with
it's own @ValueRange*
annotation.
If null
is a valid planning value, it should be included in the value range and the
default way to detect uninitialized planning variables must be changed.
All instances of the same planning entity class share the same set of possible planning values for that planning variable. This is the most common way to configure a value range.
The Solution
implementation has property which returns a
Collection
. Any value from that Collection
is a possible planning value
for this planning variable.
@PlanningVariable @ValueRangeFromSolutionProperty(propertyName = "rowList") public Row getRow() { return row; }
public class NQueens implements Solution<SimpleScore> { // ... public List<Row> getRowList() { return rowList; } }
Each planning entity has it's own set of possible planning values for a planning variable. For example, if a teacher can never teach in a room that does not belong to his department, lectures of that teacher can limit their room value range to the rooms of his department.
@PlanningVariable @ValueRangeFromPlanningEntityProperty(propertyName = "possibleRoomList") public Room getRoom() { return room; } public List<Room> getPossibleRoomList() { return getCourse().getTeacher().getPossibleRoomList(); }
Never use this to enforce a soft constraint (or even a hard constraint when the problem might not have a feasible solution). For example, when a teacher can not teach in a room that does not belong to his department unless there is no other way, the teacher should not be limited in his room value range.
By limiting the value range specifically of 1 planning entity, you are effectively making a build-in hard constraint. This can be a very good thing, as the number of possible solutions is severely lowered. But this can also be a bad thing because it takes away the freedom of the optimization algorithms to temporarily break such a hard constraint.
A planning entity should not use other planning entities to determinate it's value range. It would only try to solve the planning problem itself and interfere with the optimization algorithms.
Some optimization algorithms work more efficiently if they have an estimation of which planning values are stronger, which means they are more likely to satisfy a planning entity. For example: in bin packing bigger containers are more likely to fit an item and in course scheduling bigger rooms are less likely to break the student capacity constraint.
Therefore, you can set a strengthComparatorClass
to the
@PlanningVariable
annotation:
@PlanningVariable(strengthComparatorClass = CloudComputerStrengthComparator.class) // ... public CloudComputer getCloudComputer() { // ... }
public class CloudComputerStrengthComparator implements Comparator<Object> { public int compare(Object a, Object b) { return compare((CloudComputer) a, (CloudComputer) b); } 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(); } }
Alternatively, you can also set a strengthWeightFactoryClass
to the
@PlanningVariable
annotation, so you have access to the rest of the problem facts from the
solution too:
@PlanningVariable(strengthWeightFactoryClass = RowStrengthWeightFactory.class) // ... public Row getRow() { // ... }
public interface PlanningValueStrengthWeightFactory { Comparable createStrengthWeight(Solution solution, Object planningValue); }
public class RowStrengthWeightFactory implements PlanningValueStrengthWeightFactory { public Comparable createStrengthWeight(Solution solution, Object planningValue) { NQueens nQueens = (NQueens) solution; Row row = (Row) planningValue; int distanceFromMiddle = calculateDistanceFromMiddle(nQueens.getN(), row.getIndex()); return new RowStrengthWeight(row, distanceFromMiddle); } // ... public static class RowStrengthWeight implements Comparable<RowStrengthWeight> { private final Row row; private final int distanceFromMiddle; public RowStrengthWeight(Row row, int distanceFromMiddle) { this.row = row; this.distanceFromMiddle = distanceFromMiddle; } public int compareTo(RowStrengthWeight other) { return new CompareToBuilder() // The stronger rows have a lower distance to the middle .append(other.distanceFromMiddle, distanceFromMiddle) // Decreasing (but this is debatable) .append(row.getIndex(), other.row.getIndex()) .toComparison(); } } }
None of the current planning variable state in any of the planning entities may be used to
compare planning values. They are likely to be null
anyway. For example, None of
the row
variables of any Queen
may be used to determine the strength of a
Row
.
A dataset for a planning problem needs to be wrapped in a class for the Solver
to
solve. You must implement this class. For example in n queens, this in the NQueens
class
which contains a Column
list, a Row
list and a Queen
list.
A planning problem is actually a unsolved planning solution or - stated differently - an uninitialized
Solution
. Therefor, that wrapping class must implement the Solution
interface. For example in n queens, that NQueens
class implements
Solution
, yet every Queen
in a fresh NQueens
class is
assigned to a Row
yet. So it's not a feasible solution. It's not even a possible solution.
It's an uninitialized solution.
You need to present the problem as a Solution
instance to the
Solver
. So you need to have a class that implements the Solution
interface:
public interface Solution<S extends Score> {
S getScore();
void setScore(S score);
Collection<? extends Object> getProblemFacts();
Solution<S> cloneSolution();
}
For example, an NQueens
instance holds a list of all columns, all rows and all
Queen
instances:
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 Solution
requires a score property. The score property is null
if
the Solution
is uninitialized or if the score has not yet been (re)calculated. The score
property is usually typed to the specific Score
implementation you use. For example,
NQueens
uses a SimpleScore
:
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 HardAndSoftScore
instead:
public class CurriculumCourseSchedule implements Solution<HardAndSoftScore> { private HardAndSoftScore score; public HardAndSoftScore getScore() { return score; } public void setScore(HardAndSoftScore score) { this.score = score; } // ... }
See the Score calculation section for more information on the Score
implementations.
All objects returned by the getProblemFacts()
method will be asserted into the drools
working memory, so the score rules can access them. For example, NQueens
just returns all
Column
and Row
instances.
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;
}
All planning entities are automatically inserted into the drools working memory. Do
not add them in the method getProblemFacts()
.
The method getProblemFacts()
is not called much: at most only once per solver phase per
solver thread.
A cached problem fact is a problem fact that doesn't exist in the real domain model, but is calculated
before the Solver
really starts solving. The method getProblemFacts()
has
the chance to enrich the domain model with such cached problem facts, which can lead to simpler and faster score
constraints.
For example in examination, a cache problem fact TopicConflict
is created for every 2
Topic
's which share at least 1 Student
.
public Collection<? extends Object> getProblemFacts() { List<Object> facts = new ArrayList<Object>(); // ... facts.addAll(calculateTopicConflictList()); // ... return facts; } private List<TopicConflict> calculateTopicConflictList() { List<TopicConflict> topicConflictList = new ArrayList<TopicConflict>(); for (Topic leftTopic : topicList) { for (Topic rightTopic : topicList) { if (leftTopic.getId() < rightTopic.getId()) { int studentSize = 0; for (Student student : leftTopic.getStudentList()) { if (rightTopic.getStudentList().contains(student)) { studentSize++; } } if (studentSize > 0) { topicConflictList.add(new TopicConflict(leftTopic, rightTopic, studentSize)); } } } } return topicConflictList; }
Any score constraint that needs to check if no 2 exams have a topic which share a student are being
scheduled close together (depending on the constraint: at the same time, in a row or in the same day), can
simply use the TopicConflict
instance as a problem fact, instead of having to combine every 2
Student
instances.
Most optimization algorithms use the cloneSolution()
method to clone the solution each
time they encounter a new best solution (so they can recall it later) or to work with multiple solutions in
parallel.
The NQueens
implementation only deep clones all Queen
instances.
When the original solution is changed during planning, by changing a Queen
, the clone stays
the same.
/**
* Clone will only deep copy the {@link #queenList}.
*/
public NQueens cloneSolution() {
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.clone());
}
clone.queenList = clonedQueenList;
clone.score = score;
return clone;
}
The cloneSolution()
method should only deep clone the planning
entities. Notice that the problem facts, such as Column
and Row
are normally not cloned: even their List
instances are
not cloned.
If you were to clone the problem facts too, then you'd have to make sure that the new planning entity
clones also refer to the new problem facts clones used by the solution. For example, if you 'd clone all
Row
instances, then each Queen
clone and the NQueens
clone itself should refer to the same set of new Row
clones.
Build a Solution
instance to represent your planning problem, so you can set it on the
Solver
as the planning problem to solve. For example in n queens, an
NQueens
instance is created with the required Column
and
Row
instances and every Queen
set to a different column
and every row
set to null
.
private NQueens createNQueens(int n) {
NQueens nQueens = new NQueens();
nQueens.setId(0L);
nQueens.setN(n);
List<Column> columnList = new ArrayList<Column>(n);
for (int i = 0; i < n; i++) {
Column column = new Column();
column.setId((long) i);
column.setIndex(i);
columnList.add(column);
}
nQueens.setColumnList(columnList);
List<Row> rowList = new ArrayList<Row>(n);
for (int i = 0; i < n; i++) {
Row row = new Row();
row.setId((long) i);
row.setIndex(i);
rowList.add(row);
}
nQueens.setRowList(rowList);
List<Queen> queenList = new ArrayList<Queen>(n);
long id = 0;
for (Column column : columnList) {
Queen queen = new Queen();
queen.setId(id);
id++;
queen.setColumn(column);
// Notice that we leave the PlanningVariable properties (row) on null
queenList.add(queen);
}
nQueens.setQueenList(queenList);
return nQueens;
}
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(CurriculumCourseSchedule schedule) { List<Course> courseList = schedule.getCourseList(); List<Lecture> lectureList = new ArrayList<Lecture>(courseList.size()); for (Course course : courseList) { for (int i = 0; i < course.getLectureSize(); i++) { Lecture lecture = new Lecture(); lecture.setCourse(course); lecture.setLectureIndexInCourse(i); // Notice that we leave the PlanningVariable properties (period and room) on null lectureList.add(lecture); } } schedule.setLectureList(lectureList); }
The Solver
implementation will solve your planning problem. It's build based from a
solver configuration, do not implement it yourself:
public interface Solver {
void setPlanningProblem(Solution planningProblem);
void solve();
Solution getBestSolution();
// ...
}
A Solver can only solve 1 problem instance at a time. A Solver
should only be accessed
from a single thread, except for the methods that are specifically javadocced as being thread-safe.
Solving a problem is quite easy once you have:
A Solver
build from a solver configuration
A Solution
that represents the planning problem instance
Just set the planning problem, solve it and extract the best solution:
solver.setPlanningProblem(planningProblem);
solver.solve();
Solution bestSolution = solver.getBestSolution();
For example in n queens, the method getBestSolution()
will return an
NQueens
instance with every Queen
assigned to a
Row
.
The solve()
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 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 setPlanningProblem()
will
be changed by the Solver
, but it do not mistake it for the best solution.
The Solution
instance returned by the method getBestSolution()
will
most likely be a clone of the instance given to the method setPlanningProblem()
, which means
it's a different instance.
The Solution
instance given to the method setPlanningProblem()
does
not need to be uninitialized. It can be partially or fully initialized, which is likely in repeated planning.
The environment mode allows you to detect common bugs in your implementation.
You can set the environment mode in the solver configuration XML file:
<solver>
<environmentMode>DEBUG</environmentMode>
...
</solver>
A solver has a single Random
instance. Some solver configurations use the
Random
instance a lot more than others. For example simulated annealing depends highly on
random numbers, while tabu search only depends on it to deal with score ties. The environment mode influences the
seed of that Random
instance.
There are 4 environment modes:
The trace mode is reproducible (see the reproducible mode) and also turns on all assertions (such as assert that the delta based score is uncorrupted) to fail-fast on rule engine bugs.
The trace mode is very slow (because it doesn't rely on delta based score calculation).
The debug mode is reproducible (see the reproducible mode) and also turns on most assertions (such as assert that the undo Move is uncorrupted) to fail-fast on a bug in your Move implementation, your score rule, ...
The debug mode is slow.
It's recommended to write a test case which does a short run of your planning problem with debug mode on.
The reproducible mode is the default mode because it is recommended during development. In this mode, 2 runs on the same computer will execute the same code in the same order. They will also yield the same result, except if they use a time based termination and they have a sufficiently large difference in allocated CPU time. This allows you to benchmark new optimizations (such as a score constraint change) fairly.
The reproducible mode is not much slower than the production mode.
In practice, this mode uses the default random seed, and it also disables certain concurrency optimizations (such as work stealing).
The production mode is the fastest and the most robust, but not reproducible. It is recommended for a production environment.
The random seed is different on every run, which makes it more robust against an unlucky random seed. An unlucky random seed gives a bad result on a certain data set with a certain solver configuration. Note that in most use cases the impact of the random seed is relatively low on the result (even with simulated annealing). An occasional bad result is far more likely caused by another issue (such as a score trap).