JBoss.orgCommunity Documentation

Chapter 4. Planner configuration

4.1. Overview
4.2. Solver configuration
4.2.1. Solver configuration by XML file
4.2.2. Solver configuration by Java API
4.3. Model your planning problem
4.3.1. Is this class a problem fact or planning entity?
4.3.2. Problem fact
4.3.3. Planning entity and planning variables
4.3.4. Planning value and planning value ranges
4.3.5. Planning problem and planning solution
4.4. Use the Solver
4.4.1. The Solver interface
4.4.2. Solving a problem
4.4.3. Environment mode: Are there bugs in my code?
4.4.4. Logging level: What is the Solver doing?

Solving a planning problem with Drools Planner consists out of 5 steps:

You can build a Solver instance with the XmlSolverFactory. Configure it with a solver configuration XML file:

        XmlSolverFactory solverFactory = new XmlSolverFactory();

        solverFactory.configure("/org/drools/planner/examples/nqueens/solver/nqueensSolverConfig.xml");
        Solver solver = solverFactory.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 -->
  <scoreDirectorFactory>
    <scoreDefinitionType>SIMPLE</scoreDefinitionType>
    <scoreDrl>/org/drools/planner/examples/nqueens/solver/nQueensScoreRules.drl</scoreDrl>
  </scoreDirectorFactory>

  <!-- Configure the optimization algorithm(s) -->
  <termination>
    ...
  </termination>
  <constructionHeuristic>
    ...
  </constructionHeuristic>
  <localSearch>
    ...
  </localSearch>
</solver>

Notice the 3 parts in it:

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);
        ScoreDirectorFactoryConfig scoreDirectorFactoryConfig = solverConfig.getScoreDirectorFactoryConfig();
        scoreDirectorFactoryConfig.setScoreDefinitionType(ScoreDirectorFactoryConfig.ScoreDefinitionType.SIMPLE);
        scoreDirectorFactoryConfig.setScoreDrlList(
                Arrays.asList("/org/drools/planner/examples/nqueens/solver/nQueensScoreRules.drl"));
        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:

        XmlSolverFactory solverFactory = new XmlSolverFactory();

        solverFactory.configure("/org/drools/planner/examples/nqueens/solver/nqueensSolverConfig.xml");
        SolverConfig solverConfig = solverFactory.getSolverConfig();
        solverConfig.getTerminationConfig().setMaximumMinutesSpend(userInput);
        Solver solver = solverConfig.buildSolver();

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.

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 its Column and has a planning variable Row. This means that a Queen's column never changes during solving, while its 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 its Course and its 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 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.

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 = 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();
    }
}

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.

Each planning entity has its 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

    @ValueRange(type = ValueRangeType.FROM_PLANNING_ENTITY_PROPERTY, planningEntityProperty = "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: Unless there is no other way, a teacher can not teach in a room that does not belong to his department. In this case, the teacher should not be limited in his room value range (because sometimes there is no other way).

A planning entity should not use other planning entities to determinate its value range. That would only try to make it solve the planning problem itself and interfere with the optimization algorithms.

Some use cases, such as TSP and Vehicle Routing, require chaining. This means the planning entities point to each other and form a chain.

A planning variable that is chained either:

Here are some example of valid and invalid chains:

Every initialized planning entity is part of an open-ended chain that begins from an anchor. A valid model means that:

The optimization algorithms and build-in MoveFactory's do chain correction to guarantee that the model stays valid:

For example, in TSP the anchor is a Domicile (in vehicle routing it is the vehicle):

public class Domicile ... implements Appearance {


    ...
    public City getCity() {...}
}

The anchor (which is a problem fact) and the planning entity implement a common interface, for example TSP's Appearance:

public interface Appearance {


    City getCity();
}

That interface is the return type of the planning variable. Furthermore, the planning variable is chained. For example TSP's Visit (in vehicle routing it is the customer):

@PlanningEntity

public class Visit ... implements Appearance {
    ...
    public City getCity() {...}
    @PlanningVariable(chained = true)
    @ValueRanges({
            @ValueRange(type = ValueRangeType.FROM_SOLUTION_PROPERTY, solutionProperty = "domicileList"),
            @ValueRange(type = ValueRangeType.FROM_SOLUTION_PROPERTY, solutionProperty = "visitList",
                    excludeUninitializedPlanningEntity = true)})
    public Appearance getPreviousAppearance() {
        return previousAppearance;
    }
    public void setPreviousAppearance(Appearance previousAppearance) {
        this.previousAppearance = previousAppearance;
    }
}

Notice how 2 value ranges need to be combined:

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 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();
    }
}

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 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;
        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.

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 environment mode allows you to detect common bugs in your implementation. It does not affect the logging level.

You can set the environment mode in the solver configuration XML file:


<solver>
  <environmentMode>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 best way to illuminate the black box that is a Solver, is to play with the logging level:

For example, set it to DEBUG logging, to see when the phases end and how fast steps are taken:

INFO  Solving started: time spend (0), score (null), new best score (null), random seed (0).
DEBUG     Step index (0), time spend (1), score (0), initialized planning entity (col2@row0).
DEBUG     Step index (1), time spend (3), score (0), initialized planning entity (col1@row2).
DEBUG     Step index (2), time spend (4), score (0), initialized planning entity (col3@row3).
DEBUG     Step index (3), time spend (5), score (-1), initialized planning entity (col0@row1).
INFO  Phase constructionHeuristic finished: step total (4), time spend (6), best score (-1).
DEBUG     Step index (0), time spend (10), score (-1),     best score (-1), accepted move size (12) for picked step (col1@row2 => row3).
DEBUG     Step index (1), time spend (12), score (0), new best score (0), accepted move size (12) for picked step (col3@row3 => row2).
INFO  Phase localSearch ended: step total (2), time spend (13), best score (0).
INFO  Solving ended: time spend (13), best score (0), average calculate count per second (4846).

All time spends are in milliseconds.

Everything is logged to SLF4J, which is a simple logging facade that can delegate any log 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're not using any logging framework yet, you can use Logback by adding this Maven dependency:


    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.x</version>
    </dependency>

Configure the logging level on the package org.drools.planner. For example:

In Logback, configure it in your logback.xml file:


<configuration>

  <logger name="org.drools.planner" level="debug"/>

  ...

<configuration>

In Log4J, configure it in your log4j.xml file:


<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">

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

  ...

</log4j:configuration>